通过之前的分析,GC 算法的实现流程简单的来说分为以下两步:
- 找到死掉的对象;
- 把它清了。
想要找到死掉的对象,我们就要进行可达性分析,也就是从 GC Root 找到引用链的这个操作。
也就是说,进行可达性分析的第一步,就是要枚举 GC Roots,这就需要虚拟机知道哪些地方存放着对象引用。如果每一次枚举 GC Roots 都需要把整个栈上位置都遍历一遍,那可就费时间了,毕竟并不是所有位置都存放在引用呀。所以为了提高 GC 的效率,HotSpot 使用了一种 OopMap 的数据结构,OopMap 记录了栈上本地变量到堆上对象的引用关系,也就是说,GC 的时候就不用遍历整个栈只遍历每个栈的 OopMap 就行了。
在 OopMap 的帮助下,HotSpot 可以快速准确的完成 GC 枚举了,不过,OopMap 也不是万年不变的,它也是需要被更新的,当内存中的对象间的引用关系发生变化时,就需要改变 OopMap 中的相应内容。可是能导致引用关系发生变化的指令非常之多,如果我们执行完一条指令就改下 OopMap,这 GC 成本实在太高了。
因此,HotSpot 采用了一种在 “安全点” 更新 OopMap 的方法,安全点的选取既不能让 GC 等待的时间过长,也不能过于频繁增加运行负担,也就是说,我们既要让程序运行一段时间,又不能让这个时间太长。我们知道,JVM 中每条指令执行的是很快的,所以一个超级长的指令流也可能很快就执行完了,所以 真正会出现 “长时间执行” 的一般是指令的复用,例如:方法调用、循环跳转、异常跳转等,虚拟机一般会将这些地方设置为安全点更新 OopMap 并判断是否需要进行 GC 操作。
此外,在进行枚举根节点的这个操作时,为了保证准确性,我们需要在一段时间内 “冻结” 整个应用,即 Stop The World(传说中的 GC 停顿),因为如果在我们分析可达性的过程中,对象的引用关系还在变来变去,那是不可能得到正确的分析结果的。即便是在号称几乎不会发生停顿的 CMS 垃圾收集器中,枚举根节点时也是必须要停顿的。这里就涉及到了一个问题:
我们如何让所有线程跑到最近的安全点再停顿下来进行 GC 操作呢?
主要有以下两种方式:
- 抢先式中断:
- 先中断所有线程;
- 发现有线程没中断在安全点,恢复它,让它跑到安全点。
- 主动式中断:(主要使用)
- 设置一个中断标记;
- 每个线程到达安全点时,检查这个中断标记,选择是否中断自己。
除此安全点之外,还有一个叫做 “安全区域” 的东西,一个一直在执行的线程可以自己 “走” 到安全点去,可是一个处于 Sleep 或者 Blocked 状态的线程是没办法自己到达安全点中断自己的,我们总不能让 GC 操作一直等着这些个 ”不执行“ 的线程重新被分配资源吧。对于这种情况,我们要依靠安全区域来解决。
安全区域是指在一段代码片段之中,引用关系不会发生变化,因此在这个区域中的任意位置开始 GC 都是安全的。
当线程执行到安全区域时,它会把自己标识为 Safe Region,这样 JVM 发起 GC 时是不会理会这个线程的。当这个线程要离开安全区域时,它会检查系统是否在 GC 中,如果不在,它就继续执行,如果在,它就等 GC 结束再继续执行。