HotSpot算法细节实现----个人整理

前言:这一块内容比较晦涩,所以整理出一篇博客帮助自己深入理解。

① 根节点枚举

目前为止,所有的垃圾收集器(包括CMS,G1,ZGC)在初始标记这一阶段都是需要STW的,而如果这一步耗时过长,就无法满足所谓低延迟垃圾收集器的需求。
在HotSpot的解决方案里,使用一个叫做OopMap的数据结构解决上述问题。在类加载动作完成时,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在待定为止记录下栈和寄存器里哪些位置是引用。这样收集器在初始标记阶段就不用遍历GC Root了,而是可以直接获得这些信息。
HotSpot算法细节实现----个人整理_第1张图片

② 安全点

使用OopMap导致的问题:如果说导致引用关系变化,或者说导致OopMap内容变化的指令特别多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外存储空间。
实际上HotSpot也没有每条指令都生成OopMap,而是强制要求字节码执行到某一个安全点才能暂停生成OopMap。因此,安全点不能太少以至于让收集器等待时间过长,也不能太频繁以至于过分增大运行时内存负荷。
如果在垃圾收集发生时,让所有线程跑到安全点并停顿?答:主动式中断抢先式中断(篇幅原因,看书比较直接)。

③ 安全区域

个人理解,就是当出现线程无法执行的状态(如Sleep,Blocked),需要规定一段代码块,该代码块需要包含那些无法执行的线程当前停顿的位置。在这段代码块中,引用的关系不会发生变化,垃圾收集可安全执行,这段代码块就叫做安全区域。另外:只有当虚拟机已完成了根节点枚举,线程才可以离开安全区域。

④ 记忆集与卡表

这一部分算法主要是解决跨代引用的问题,避免把整个老年代加入GC Roots的扫描范围。
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。而卡表就是记忆集的一种实现,它定义了记忆集的记录精度,与堆内存的映射关系等。
HotSpot使用一个字节数组去实现卡表,数组上一个元素对应一块内存区域,其名为卡页,一个卡页的内存上可能有多个对象,只有其中一个对象存在跨代指针,就讲对应卡表标识位置1,称卡表变脏了。
在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块包含跨代指针,把它们加入GC Roots中一并扫描。

⑤ 写屏障

这一部分算法主要解决如何维护卡表(何时变脏,如何变脏)的问题。
何时变脏:有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻。
如何变脏:应用写屏障,类似在“引用类型字段赋值”这个动作做一个AOP切面。应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,虽然每次对引用进行更新,会产生额外的开销,不过这个开销与Minor GC时扫描整个老年代的代价相比还是低得多。

⑥ 并发的可达性分析

并发的可达性分析本身不难理解,类似图论中查看是否有可达的引用链,无的话则为不可达,便对不可达的对象进行回收。
问题:但像CMS,G1这类标记阶段是与用户线程同步进行的垃圾收集器,必须保障对象图的一致性问题,否则可能会出现对象可达,但依然被回收的情况。
(这里对象的三种颜色有各自的含义,可翻阅三色标记算法相关资料)
HotSpot算法细节实现----个人整理_第2张图片
如图所示,在并发标记过程中:
① 对象B已经扫描完毕
② 对象C指向对象D的引用被切断(用户线程干的)
③ 对象B有新的引用指向对象D(用户线程干的)
这样最后还是会把D回收了,而实际上D不应该被回收。
注意:
只有在②和③同时发生的情况下,才会出现“对象消失”的情况,所以只需要破坏其中一个条件,就可避免。
增量更新 -> 破坏②条件
原始快照 -> 破坏③条件

两种解决方案的具体实现可翻阅《深入理解JVM第三版》

你可能感兴趣的:(Java)