HotSpot 虚拟机的算法实现

一、概述

HotSpot 虚拟机通过 GC Roots 枚举判定待回收的对象,通过安全点和安全区域确定 GC 的触发点,最后通过各种不同的回收算法完成垃圾回收。

二、GC Roots 枚举过程

GC Roots 枚举最大的困难点在于:检查范围比较大,并且必须在内存快照中进行,保证一致性,而且时间要求比较敏感。

在生产环境中,即使不考虑其它部分内存,仅仅 Java 堆内存就可达几百兆甚至上G,在此范围内完成 GC Roots 确定是一件很困难的事情;同时,在进行 GC Roots 枚举时,必须保证一致性,即所有正在运行的程序必须停顿,不能出现正在枚举 GC Roots,而程序还在跑的情况,这会导致 GC Roots 不断变化,产生数据不一致导致统计不准确的情况;最后,由于所有工作线程必须停顿以完成 GC 过程,在大并发高访问量情况下,这个时间必须非常短。

三、准确式 GC

为解决上述问题,HotSpot 采用了一种 “准确式 GC” 的技术;该技术主要功能就是让虚拟机可以准确的知道内存中某个位置的数据类型是什么;比如某个内存位置到底是一个整型的变量,还是对某个对象的 reference;这样在进行 GC Roots 枚举时,只需要枚举 reference 类型的即可。

在能够准确地确定 Java 堆和方法区等 reference 准确位置之后,HotSpot 就能极大地缩短 GC Roots 枚举时间。

四、OopMap

当类加载完成后,HotSpot 就将对象内存布局之中什么偏移量上数值是一个什么样的类型的数据这些信息存放到 OopMap 中;在 HotSpot 的 JIT 编译过程中,同样会插入相关指令来标明哪些位置存放的是对象引用等,这样在 GC 发生时,HotSpot 就可以直接扫描 OopMap 来获取这些信息,从而进行 GC Roots 枚举,下图展示了 JTI 编译的 OopMap 指令:

OopMap

五、安全点(Safepoint)

Safepoint:会导致 OopMap 内容变化的指令非常多,如果为每一条指令都生成对应的 OopMap,那么将需要大量的额外空间,这样对导致 GC 成本很高,所以 HotSpot 只在 “特定位置” 记录这些信息,这些位置被称为 安全点(Safepoint)。

并非程序在任意时刻都可以停顿下来进行 GC,而只有程序到达 安全点(Safepoint) 以后才可以停顿下来进行 GC;所以安全点既不能太少,以至于 GC 过程等待程序到达安全点的时间过长,也不能太多,以至于 GC 过程带来的成本过高。

六、安全点上停止线程方式

由于在 GC 过程中必须保证程序已执行,那么也就是说 必须等待所有线程都到达安全点上方可进行 GC。

一般会有两种解决方案:

抢先式中断:不需要线程的执行代码去主动配合,当发生 GC 时,先强制中断所有线程,然后如果发现某些线程未处于安全点,那么将其唤醒,直至其到达安全点再次将其中断;这样一直等待所有线程都在安全点后开始 GC。

主动式中断:不强制中断线程,只是简单地设置一个中断标记,各个线程在执行时轮询这个标记,一旦发现标记被改变(出现中断标记)时,那么将运行到安全点后自己中断挂起;目前所有商用虚拟机全部采用主动式中断。

HotSpot 选定的标记位置与安全点位置是重合的,如下如图所示:

安全点与中断标记

七、安全区(Saferegion)

安全点的机制似乎已经完美的解决了 “什么时候以及何时开始 GC” 的问题,但是实际情况并非如此;安全点机制仅仅是保证了程序执行时不需要太长时间就可以进入一个安全点进行 GC 动作,但是当特殊情况时,比如线程休眠、线程阻塞等状态的情况下,显然 JVM 不可能一直等待被阻塞或休眠的线程正常唤醒执行;此时就引入了安全区的概念。

安全区(Saferegion):安全区域是指在一段区域内,对象引用关系等不会发生变化,在此区域内任意位置开始 GC 都是安全的;线程运行时,首先标记自己进入了安全区,然后在这段区域内,如果线程发生了阻塞、休眠等操作,JVM 发起 GC 时将忽略这些处于安全区的线程。当线程再次被唤醒时,首先他会检查是否完成了 GC Roots枚举(或这个GC过程),然后选择是否继续执行,否则将继续等待 GC 的完成。


HotSpot 虚拟机的算法实现
https://mritd.com/2016/03/24/algorithm-implementation-of-hotspot-vm/
作者
Kovacs
发布于
2016年3月24日
许可协议