HotSpot 的算法实现 | 安全点、安全区域

639 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

上节已经分析了 GC 发生的区域,什么时候 GC 以及如何 GC 的理论基础,今天就看看 HotSpot 是如何实现的。

在可达性分析算法中我们知道根节点是 GC Roots,那么我们首先就要找到这些根节点(常量,类静态属性,栈帧中的变量等)而这些占据了数百兆的空间,要一个一个的检查这些引用,必然费时。

另外,可达性分析算法对执行时间的敏感还体现在 GC 停顿上,我们判断是否可达肯定是检查同一时刻的对象状态,不然的话对象状态可能会发生改变,所以不论是什么样的 GC 收集器,都会有停顿的时间,或长或短,Sun 公司称这种停顿为 stop-the-world。

遍历所有的 GC Roots 太费时怎么办?在 HotSpot 的实现中,是使用一组称为 OopMap 的数据结构来达到这个目的的,在类加载完成的时候,HotSpot 就把对象内的数据类型计算出来,然后记录下哪些位置是引用。这样 GC 在扫描时就可以直接得到 GC Roots 的信息了。

至于 GC Roots 在枚举对象是否可达的停顿上面,只能不断优化,至今不能消除,stop-the-world 必须发生。

我们使用 OopMap 来记录 GC Roots 从而来完成可达性分析,这是可以的,但是,难道说我们每执行一条指令都要来创建一个 OopMap 吗?(因为执行指令后引用的状态可能发生改变)显然不是。这太浪费时间了,那我们在什么时候才会使用 OopMap 来进行 GC Roots 的枚举呢。

答案就是在安全点才会进行进行 GC,也就是在安全点才会生成 OopMap 记录 GC Roots节点从而判断对象是否可达,进而进行 GC。

安全点的选取肯定是不能大多也不能太少,标准就是可以让程序一直安全的跑下去,不能让太多的死对象占据内存。遇到长时间执行的指令的时候就给它 GC 一下,类如方法的调用、循环跳转、异常跳转等,遇到类似的执行才会生成 Safepoint。

安全点也就是 GC 发生的位置,对于多线程程序来说,我不能一个线程在安全点要进行 GC 的时候其它的线程还在跑,必须要等待,或是说让其它线程也到安全点集合。

两种策略,一是抢占式中断,我先到安全点了,不管你现在在哪里,先给你中断,然后一看,噢原来你不在安全点,就恢复线程让你跑到安全点再 GC。

还有一种是主动式中断,需要 GC 的时候,不需要强制中断线程了,只需要在安全点设置一个轮询标识,线程只需要去轮询这个标识即可,线程到安全点了,自己主动中断,进而 GC。

到这里我们似乎解决了问题,多线程下也可以同时 GC 了,但是有个小小的问题,假如我有一个线程正好在 Sleep,它没有在运行,就不能去轮询 GC 标识了,我们难道要等到它运行再 GC,不可能的对吧!

反过来想一下就是,假如线程本身就不再执行,那何必去管它呢,因为它不可能使引用发生变化啊。故我们又定义了一个安全区域的概念,在这个代码片段之中发生 GC 都是可以的,因为引用不曾改变。这就是扩大版的安全点啊。

在线程执行到安全区域中的代码时,首先标识自己已经进入了安全区域,那样,当这段时间内 JVM 要发起 GC 时,就不管标识自己为安全区域状态的线程了。在线程要离开安全区域时,要检查系统是否已经完成了 GC,如果完成了,那就继续执行,否则就要等待 GC 结束的标识之后才可以离开安全区域。

总结一下,我们要开始 GC,使用可达性分析算法的时候,首先就要找到 GC Roots,然后完成 GC Roots 的遍历,确定要回收的对象,我们在确定 GC Roots 的时候借组与 OopMap 这个数据结构,但是不同的指令下可能会产生不同的 GC Roots,不能为每条指令都创建 OopMap,我们就确定一个安全点,只在安全点去枚举 GC Roots,但是,枚举的时候有些线程可能不再执行,等不到它走到安全点,所以就确定了一个安全区域,只要在这个区域中开始 GC 都是可以的。

到这里,我们就介绍了 HotSpot 中的发起内存回收的问题,但是虚拟机如何回收还没有说,如何回收需要看不同的 GC 收集器,不同的收集器有不同的性能和用武之地,今天就到这里,下次再说一说 GC 收集器。