JVM垃圾收集第二弹来了~

168 阅读4分钟

这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战

GC算法实现

1 OopMap

在进行可达性分析时,我们需要分析内存中所有对象的引用关系,找到不可达的对象,并标记为可回收。而在程序的运行过程中,内存中对象的引用关系是在不断变化的,这就有可能在我们分析可达性的这段时间内,对象的引用关系发生变化。所以,为了准确的分析出对象的引用关系,JVM 不得不停止所有 Java 的执行线程(Sun 将这个过程称之为 “Stop The World”)。即使是在号称(几乎)不会发生 STW 的 CMS 收集器中,在枚举根节点时,也会发生停顿。

在可达性分析时另外一个问题是,内存中的对象太多,如果要逐个检查对象的引用关系,会非常耗时。为了解决这个问题,在 HotSpot 虚拟机中,使用了一组 OopMap 的数据结构来记录对象内的偏移量上的类型,在 JIT 编译过程也会在特定位置记录下栈和寄存器中哪些位置是对象的引用。在发生 GC 时就可以直接扫描 OopMap 就可以直接得到对象的引用信息了。

2 Safepoint

其实就是使用 OopMap 把对象的引用关系保存了下来,但是这样就引发了另外一个问题。可能导致 OopMap 内容变化的指令非常多,我们不能为每个指令都创建一个 OopMap,这样 GC 的空间成本会提高。

所以 JVM 做了一种改进,只在程序运行期间的某些特定的位置记录 OopMap,这些特定的位置被称为“安全点(Safepoint)”。这样就解决了 OopMap 随时变化的问题,JVM 不再实时的关注内存中引用关系的变化,而是在安全点处获取当前的内存引用关系。

安全点的选定,即不能太少,导致 GC 等待时间太长,也不能太频繁,会导致频繁的 GC。最好是选定在某些执行时间比较长的指令处,比如方法调用、循环跳转、异常跳转等。

由于安全点不能随意选定,所以带来的新的问题是,在 GC 发生时,如何让所有的线程都运行到安全点的位置。 有一种称为“主动式中断(Voluntary Suspension)”的方式,它的基本原理是,当 GC 发生时,设置一个标志。所有的线程当运行到安全点的位置时,访问这个标志,如果为真则自己中断挂起。目前绝大多数 JVM 都是采用的这种方式。

还有另外一种几乎没有被采用过的“抢先式中断(Preemptive Suspension)”的方式。这种方式是,当 GC 发生时,中断所有线程,如果发现有线程不在安全点上,会再次恢复线程的运行,让它运行到安全点的位置再次中断。

3 Safe Region

使用安全点似乎已经解决了绝大部分问题,但是还有一个比较小的特例。比如,当某个线程处于 Sleep 或者 Blocked 状态时,不能够访问 GC 设置的标志,也没办法运行到安全点的位置(因为 Sleep 的线程无法运行)。而且,JVM 也不太可能等着这个线程恢复执行,运行到安全点的时候再次 GC。这种情况下当前 GC 就没办法执行。

这种情况下就需要使用“安全区域(Safe Region)”来解决。安全区域是指在一段代码片段之中,引用关系不会发生变化,在这段区域内随时都可以进行 GC。当代码执行到安全区域后,会标识自己进入到了安全区域,这样在进行 GC 时,JVM 就不需要处理这些处于安全区域内的线程了。当线程将要离开安全区域时,会去判断是否已经完成 GC,如果没有完成就中断自己,直到收到可以离开安全区域的信号,才可以继续执行。