1、枚举根节点
从可达性分析中从GC Roots
节点找引用链这个操作为例,可作为GC Roots
的节点主要在全局的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法就数百兆,如果逐个检查这里面的引用,必然消耗很多时间。
另外,可达性分析对执行时间的敏感还体现在GC
停顿上,因为这个分析必须在一个能确保一致性的快照中执行——指在整个分析过程中整个执行系统好像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化,否则分析的准确性就无法保证。
目前主流的Java虚拟机都是使用的准确式GC,所以当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应该有办法直接得知哪些地方存放着对象引用。在HotSpot
中,使用一组OopMap
的数据结构来达到这个目的,在类加载完成的时候,HotSpot
就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样GC在扫描的时候就可以直接指导这些信息。
2、安全点
在OopMap
协助下,HotSpot
可以快速准确完成GC Roots
枚举,但是OopMap
内容变化的指令非常多,如果为每一条指令都生成对应的OopMap
,那将需要大量的额外空间,这样GC成本就很高。
实际上,HotSpot
没有为每条指令都生成OopMap
,前面提到只是在“特定的位置”记录这些信息,这些位置称为安全点(Safepoint
),即程序执行时并非在所有地方都能停顿下来开始GC,只有到达安全点才能暂停。Safepoint
的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。
安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时候都很短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint
。
对于Safepoint
,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都跑到最近的安全点上再停顿下来。有两种方案:
- 抢先式中断(Preemptive Suspension):不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点,就恢复线程,让它跑到安全点。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。
- 主动式中断(Voluntary Suspension):当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
3、安全区域
使用Safepoint
机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint
。
如果程序不执行,就是没有分配CPU时间,典型的例子就是线程处于Sleep或者Blocked状态,这时候线程无法响应JVM的中断请求,走到安全的地方去中断挂起,JVM也不太可能等待线程重新分配CPU时间。对于这种情况,就需要安全区域(Safe Region
)来解决。
安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域中任何地方开始GC都是安全的。可以把Safe Region
看做是被扩展的Safepoint
。
在线程执行到Safe Region
中的代码时,首先标识自己进入了Safe Region
,那样,当这段时间JVM要发起GC时,就不用管标识自己为Safe Region
的线程了。在线程要离开Safe Region
时,它要检查是否已经完成根节点枚举(或者是整个GC过程),如果完成了,线程继续执行,否则就必须等待知道收到可以安全离开Safe Region
的信号为止。
该文章来源《深入理解Java虚拟机》