本文介绍JVM垃圾回收算法的具体实现,介绍各个术语,并图文并茂介绍具体的实现细节。 垃圾回收判定及回收过程如下:
1653579682.png
1、可达性分析算法
通过GC Roots
的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,如果对象没有被引用链引用到,则判断对象为可回收对象。
2、GC Roots
根节点枚举要扫描的对象集,如下:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native)引用的对象
- Java虚拟机内部的引用
- 所有被同步锁(synchronized)持有的对象
- 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
3、根节点枚举
扫描GC Roots
集合查找引用链,这是Stop The World
的操作,即所有用户线程均要暂停,这是非常耗时的。即使号称停顿时间可控,或者(几乎)不会发生停顿的CMS、G1、ZGC等收集器,枚举根节点也是要暂停的。
可达性分析算法耗时最长的查找引用链的过程已经可以做到与用户线程一起并发,但根节点枚举始终还是必须在一个能保障一致性的快照中才得以进行。
使用OopMap
来快速找到虚拟机栈的引用。
4、OopMap
知道栈的哪个位置存放着对象引用。
5、安全点
在OopMap
的协助下,HotSpot可以快速准确地完成GC Roots
枚举。
执行指令后,在特定位置记录OopMap信息,这些位置成为安全点
。
- 抢先式中断 - 马上暂定所有用户线程 - 现在没有这么做的
- 主动式中断 - 线程去主动轮询一个标记,发现为真的时候就在最近的安全点上主动中断挂起。- 轮询操作精简为了一条汇编指令 test
6、安全区域
在这里引用关系不会发生改变,垃圾回收时不会管这些线程,代码执行到要出安全区域的时候才会判定是否需要暂停。
7、记忆集
存在问题: 在部分区域收集中,当存在跨代引用,例如老年代引用新生代,那么新生代就不能被回收。G1的的分区域收集也是如此。
经验法则分代假说:
1、弱分代假说: 绝大多数对象都是朝生夕灭的。
2、强分代假说: 熬过越多此垃圾收集过程的对象就越难以消亡。
3、跨代引用假说: 跨代引用相对于同代引用来说仅占极少数。
由以上假说可推理出,如果有跨代引用,则新生代晋升到老年代后,这种跨代引用也随即被消除了。所以可以不为了少量的跨代引用而扫描整个老年代,这种代价太昂贵,所以出现了记忆集
。
记忆集把老年代划分成若干个小块,标识出哪一块内存存在跨代引用,此后发生Minor GC的时候,只有这小部分才会被加入GC Roots进行扫描。
8、卡表
卡表
是记忆集
的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。
有三精度 1、字长精度 2、对象精度 3、卡精度 - 每个记录精确到一块内存区域,该区域内有对象含有跨代指针。
卡表最简单的形式可以是一个字节数组,可表示为CARD_TABLE[this address >> 9] = 0
,其中每一个元素是一个特定大小的内存块,成为卡页
。
9、卡页
hotSpot中每个卡页
都是512字节,每个卡页包含不止一个对象,只要有一个存在跨代指针,那么卡表的数组元素的值则为1,成为变脏,否则为0。垃圾回收时,筛选出变脏的元素,加入到GC Roots一起进行扫描。
10、写屏障
维护卡表状态
写屏障可以看作在虚拟机层面对"引用类型字段赋值"这个动作的AOP切面,在引用对象赋值时会产生一个环形通知,供程序执行额外的动作
G1之前都是写后屏障,在执行引用字段赋值后,会更新卡表状态。