垃圾回收分为两步:1)判定对象是否存活。2)将“消亡”的对象进行内存回收。
1 判定对象存活
可达性分析算法:通过一系列“GC Roots”对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走的路径为“引用链”,如果某个对象到“GC Roots”没有任何引用链相连,则判定该对象“消亡”。
强引用 | 正常引用,可达性分析搜索的只有强引用 |
软引用 | 关联还有用,但非必须的对象。在系统将要发生内存溢出时,会把这些对象列入回收范围。 |
弱引用 | 比软引用更弱,只能生存到下次垃圾收集发生之前。 |
虚引用 | 最弱,无法通过虚引用来取得一个对象实例。唯一作用是在则会个对象被回收时收到一个系统通知。 |
表 Java 的引用类型
1.1 三色标记
图 可达性分析算法“三色标记”演示过程
1.1.1“对象消失”
垃圾回收器与用户线程并发执行,若以下两个条件同时成立,对象消失必然发生。
- 用户线程将黑色对象指向一个白色对象。
- 用户删除所有从灰色对象到该白色对象的引用。
注意:对于在并发期间新建的对象,JVM会把其标记为黑色或灰色。
1.1.2 写屏障
写屏障是一段嵌入在对象引用赋值操作中的代码逻辑(类似AOP)。JVM通过写屏障实现两种方式来解决上面“对象消失”的问题。
增量更新 | 破坏第1个条件。当黑色对象插入新指向到白色对象时,写屏障将新插入引用记录下来,等并发扫描结束,再将记录过的引用关系中黑色对象集作为根,重新扫描一次。 效率更低。 |
原始快照 | 破坏第2个条件。当灰色对象要删除指向白色对象的引用关系时,写屏障将要删除的引用关系记录下来,等并发扫描结束,再将记录过的引用关系中白色对象集作为根,重新扫描一次。 会产生浮动垃圾。 |
表 “对象消失”的解决方案
1.2 GC Roots 对象
主要有:1) 栈中引用的对象。2)本地方法栈中引用的对象。3)方法区中静态属性引用的对象及常量引用的对象。4)JVM 内部的引用。5)被同步锁持有的对象等。
获取GC Roots 集必须在一个能保障一致性的快照中才能进行,因此需要STW(Stop The Word)。
1.2.1 栈帧
栈帧是单个线程在方法调用时在栈中分配的内存区域,用于存储方法的执行状态。每个方法从调用到执行完成,对应一个栈帧的入栈和出栈。
图 栈帧内存布局
局部变量表 | 存储方法的参数、局部变量以及部分中间结果。 以变量槽(Slot)为基本单位,每个Slot占用4个字节。对于8个字节的变量,占用两个连续的Slot。 索引分配: 非静态方法第0位Slot存储this引用。 方法参数从第1位依次存储。 局部变量按声明顺序分配Slot。 |
操作数栈 | 存储方法执行过程中的操作数。 |
动态链接 | 存储指向运行时常量池中该方法的符号引用。 |
方法返回地址 | 存储方法退出后需要返回到的调用者位置。 |
附加信息 | 行号表、局部变量表描述符等。 |
表 栈帧内存组成
1.2.2 OopMap
收集线程需要遍历方法栈中每一个栈帧,来收集被引用的变量。如果对栈帧的局部变量表进行全表扫描,很耗时。
OopMap(ordinary object pointer Map)普通对象地图,用于描述栈帧中对象引用的位置。它通常是一个位图,每个位对应局部变量表中一个槽位。1表示该槽位有对象引用,0表示没有。例如,假设局部变量表一共有8个槽位,其中只有第1个及第3个槽位有对象引用,则OopMap表示为10100000。
一个栈帧包含多个OopMap。
1.2.3 安全点
引发引用关系变化的指令很多,无法为每一条指令都生成对应的OopMap,只会在某些位置生成,这些位置被称为安全点。
安全点选择的原则:平衡线程响应速度和性能开销。
安全点常用位置:方法调用、循环末尾、异常处理路径等。
主动式中断 | 主流方案,当需要GC Roots收集时,JVM在内存中设置一个标识位,用户线程每次到达安全点都会轮询标识位,如果需要中断,则主动挂起。 |
被动式中断 | 通过操作系统信号强制中断线程,如果有用户线程未到达安全点,则恢复该线程,让其到达安全点后再中断。 |
表 安全点的实现方案
缺陷:
1)本地方法无法设置安全点。
2)对于未插入安全点但需要长时间执行的指令(如循环),如果在循环提中未插入安全点,则需要等待循环完成才能到达安全点。
3)如果线程在安全点被阻塞或sleep,因为其被唤醒的时间不能确定,JVM无法等到该线程到达安全点。
4)如果为线程阻塞或sleep指定插入安全点,则需要插入安全点的地方会增加,会加重程序的负担。
1.2.5 安全区域
在某段代码片段中,引用关系不会发生变化,这个区域任何地方开始收集GC Roots都是安全的,这个区域称为安全区域。
当用户线程执行到安全区域时,会标识自己已进入安全区域。这段时间里JVM要进行GC Roots收集就可不必中断在安全区域内的线程,当线程要离开安全区域时,会先检测JVM是否完成了GC Roots枚举,如果完成,则线程继续执行,否则等待。
安全区域的应用场景:
- 本地代码的执行,当用户线程进入本地方法时就标识自己进入了安全区域。
- 统一管理线程多种阻塞状态,只有线程处于阻塞状态,即视为进入安全区域。
- 避免“长时间无安全点”的僵局,如在循环体中没有安全点,则标识为进入了安全区域。
安全区域是对安全点的必要补充。
1.3 对象回收判定
要正式宣告对象死亡,最少要经历两次标记过程:
- 可达性分析后进行第1次标记。
- 对上面标记的对象进行筛选,如果这些对象实现了finalize()方法,则会调用这个方法,这是对象唯一次复活的机会,否则宣告对象死亡。