《深入理解Java虚拟机》第三章读书笔记(二)——HotSpot垃圾回收算法实现(OopMap,安全点安全区域,卡表,写屏障,三色标记算法)
前面《深入理解Java虚拟机》第三章读书笔记(一)——垃圾回收算法我们学习了垃圾回收算法理论知识,下面我们关注下HotSpot垃圾回收算法的实现,分为以下几部分
- 对象是垃圾的判断依据 GC Roots 是如何高效扫描的
- 如何解决跨代引用对象的垃圾回收问题
- 如何降低垃圾回收STW的时长——并发可达性分析
1.GC Roots 是如何高效扫描的
固定作为GC Roots的节点主要分布在全局性的引用(常量,静态属性)于栈帧本地变量表等,如何快速从方法区中获取这些节点呢?
HotSpot使用一组称为OopMap的数据结构来实现快速的扫描哪些地方存在对象引用——一旦类加载动作完成的实还,HotSpot就会在对象内什么偏移量上是什么类型的数据计算出来,对于即时编译,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。
根节点枚举的过程需要暂停用户线程(Stop the world 简称STW),这样可以扫描的过程在一个一致性快照中进行(用户线程都停止了不会该变对象的引用关系)
2.用户线程何时停止进行垃圾回收
2.1安全点
Oop Map 让HotSpot可以快速进行根节点枚举,但是用户线程可能正在运行改变引用关系的指令,如果为每一条指令都生成对应的Oop Map,那么将需要大量的空间。因此需要一个“特定的位置”,在这个位置引用关系不会再改变,可以维护Oop Map 并进行GC,这个位置称为——“安全点
”,它决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。
-
安全点
的选定不能太多,以至于增大运行时的负荷(太多意味着Oop Map的维护过于频繁),也不能太少导致垃圾收集器等待时间太长。安全点位置需要能让程序长时间执行(大部分指令的执行时间都很短),但是方法调用,循环跳转,异常跳转这种指令序列复用符合这个要求,具备这些功能的指令回产生安全点。 -
如何在垃圾回收是,让用户线程跑到最近的安全点,然后停顿下来
-
主动式中断:当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一 个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他 需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新 对象。
由于轮询操作在代码中会频繁出现,这要求它必须足够高效。HotSpot使用内存保护陷阱的方式, 把轮询操作精简至只有一条汇编指令的程度。线程执行到这个汇编指令的会产生一个自陷异常信号,然后在预先注册的异常处理器中挂起线程实现等待,这样仅通过一条汇编指令便完成安全点轮询和触发线程中断了。
-
抢先式中断
抢先式中断不需要线程的执行代码 主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。
-
2.2安全区域
安全点保证了用户线程在运行的时候,如何停止用户线程,让jvm进入垃圾回收状态。但是如果用户线程被阻塞而停止的时候呢?
如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到安全点
(Safe Point) 上。因此 JVM 引入了 安全区域
(Safe Region)。Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。线程在进入 Safe Region 的时候先标记自己已进入了 Safe Region,等到被唤醒时准备离开 Safe Region 时,先检查能否离开,如果 GC 完成了,那么线程可以离开,否则它必须等待直到收到安全离开的信号为止。
3.如何解决跨代引用对象的回收
1.记忆集与卡表
如果老年代引用了新生代的对象,回收新生代的时候,难道需要扫描全部老年代找出存在跨代引用的对象么?
垃圾收集器在新生代中建立了名为记忆集的对象,可以避免将整个老年代加入到GC Roots的扫描范围。记忆集是用于记录非收集区域指向收集区域的指针集合的抽象数据结构
。
为了减少记忆集的空间成本,收集器只需要记忆集判断出某一块非收集区域是否存在向收集区域的指针就可以了,并不记录所有跨代指针细节,因此记忆集的具体实现——“卡表
”只精确到一块内存区域(该区域内存在对象的跨代指针)。
2.写屏障维护卡表
卡表用于记录跨代指针,但是卡表中的元素何时进行维护,也就说出现跨代指针的时候如何记录在卡表中,跨代指针消除的时候如何清除卡表的内容?
hotSpot虚拟机使用写屏障进行维护,这个写屏障可以看作是赋值操作的AOP环形通知。有了写屏障之后,虚拟机会为赋值操作生成相应指令,进行维护卡表。
为了避免并发场景下,多线程操作卡表导致伪共享,虚拟机会先检查卡表是否未被标记,未被标记才会进行标记操作。
4.并发可达性分析——三色标记算法
可达性分析算法理论上必须在一个一致性快照中进行,一致性意味着需要冻结用户线程。在枚举GC Roots这个环节jvm使用OopMap让STW停顿时间减少,但是获得GC Roots之后继续遍历对象图的过程必然会随着堆越大而愈加耗时,导致停顿的时间更长。
那么如何减少这个停顿时间呢?——让可达性分析算法中的标记步骤可以和用户线程尽量并行,三色标记算法应运而生。
三色标记算法
三色是:黑色,白色,灰色。
把遍历对象图过程中遇到的对象,按照是否访问过这个条件标记成以下三种颜色:
- 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
上图描述了三色标记的流程。但是如果标记的时候用户线程在修改引用关系,导致对象图关系改变,可能导致出现错误。
-
错标:是垃圾的对象,没有被标记为垃圾
这种情况本应灰色的垃圾E,以及和它关联的对象 F,G都不会会被回收(E被视为和GC Roots关联导致错误的任务,G,F也不垃圾),这种称为浮动垃圾(不和GC Roots关联如同漂浮无依无靠的垃圾)浮动垃圾的问题影响不是很大,可能就是暂时的浪费一点内存,它肯定抗不过下一轮GC
-
错杀
这种情况十分严重,但是存在补救方法:
-
增量更新
当黑色对象插入指向白色对象的引用关系时,将这个插入的引用记录下来,并发扫描结束后再将这些及引用关系的黑色对象为根重新扫描一次。
例如D插入了对G的引用当并发扫描结束后,以D为根再次进行扫描,这时候G就会被标记为黑色,从而不被回收。
-
原始快照
当灰色对象要删除指向白色对象的引用关系时,就将删除的引用记录下来,并发扫描结束后,再将这些记录过引用关系的灰色对象为根,重新扫描一次。
例如E删除了对G的引用,但是记录下了E->G,并发扫描结束后,再扫描E并且结合E->G将G标黑,从而让G不被回收。
-