深入理解JVM(六) -- GC执行原则和方案
上篇文章中,我们了解了Java虚拟机垃圾回收的思路和策略,这篇文章我们将了解Java是如何实现高效的回收算法的。
我们需要了解,内存回收必须要保证“一致性”,意思就是在执行GC分析的时候,系统看起来要像是冻结在某一时间点上,不能出现在分析过程中,引用的情况还在发生变化,这样就无法进行分析,这是为什么在GC时必须要停顿所有的Java线程(官方称之为Stop The World),Java是通过引用计数器和可达性分析算法来判断一个对象是否可以被回收的,在进行可达性分析的时候,首先要枚举出所有的根结点GC-Roots,一个一个去挑选显然是不现实的,因为这会导致系统长时间的STW,而且,虚拟机是可以知道哪些地方存在存放着直接引用的,依靠使用一种叫做OOP的数据结构来达到这个目的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译的过程中,也会在特定的位置几下栈和寄存器中哪些位置是引用,这样在GC扫描时,就可以直接获取这些信息了。
上文也提到,并不会为每一条指令都创建OOP,因为这样会产生大量的内存消耗,得不偿失只会在特定位置创建,这个位置就是“安全点(Safe Point)”,程序只有在安全点才可以停下来执行GC,安全点设置的时候,为了追求合理(例如当需要执行GC时,不能让GC等候太久才进入安全点),这个标准就是“是否会让程序执行太久”,注意,这个判断条件不能以指令集的长度来判断,因为每条指令执行的时间都很短,其最明显的特征就是指令序列复用,例如方法跳转,循环跳转,异常跳转等,所以,具有这些功能的指令才会产生安全点。
对于安全点,另一个需要考虑的问题是如何在GC发生时,让所有的线程都跑到最近的安全点上停下来,这里有两个方案选择:一是抢先式中断,就是在GC发生时,将所有的线程中断,如果发现有线程中断的地方不在安全点上,就恢复该线程,现在几乎没有虚拟机采用这种抢先式中断;而是主动式中断,设置一个标志,在GC发生时,该标志为真,在每个安全点上去查看这个标志,当这个标志为真时,就自己挂起中断。
另外我们需要扩充一个安全域的概念,因为当有些线程正好处于Sleep或Block状态时,无法响应JVM的暂停请求,JVM也不可能等待CPU重新分配时间片,这就需要安全域来解决,安全域是指在一个代码片段中,引用关系不会发生变化,在这个区域中任何地方开始GC都是安全的,我们可以把安全域看作是对安全点的一个扩充。当线程执行到Safe Region时,就会标识自己已经进入SafeRegion,当需要执行GC时,不用管这种线程,当这种线程将要跳出SafeRegion时,会检查系统是否已经枚举完了根结点(或整个GC过程完成),如果没有,则必须等待到可以安全离开SafeRegion的信号为止。
可以看出,JVM在具体的实现细节上遇到了很多问题,所以我们要深入了解JVM,学习优秀的解决方案,不能只停留在表面。
本文我们了解了JVM在执行GC时的一些细节,下篇文章我们将会介绍几种流行的商用垃圾收集器。