枚举GC Roots的实现

枚举根节点

从可达性分析中从GC Roots节点找引用链这个操做为例,可做为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,如今不少应用仅仅方法区就有数百兆,若是要逐个检查这里面的引用,那么必然会消耗不少时间。另外,可达性分析对执行时间的敏感还体如今GC停顿上,由于这项分析工做必须在一个能确保一致性的快照中进行——这里“一致性”的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不能够出现分析过程当中对象引用关系还在不断变化的状况,该点不知足的话分析结果准确性就没法获得保证。这点是致使GC进行时必须停顿全部Java执行线程(Sun将这件事情称为“Stop The World”)的其中一个重要缘由,即便是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必需要停顿的。java

因为目前的主流Java虚拟机使用的都是准确式GC,因此当执行系统停顿下来后,并不须要一个不漏地检查完全部执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程当中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就能够直接得知这些信息了。安全

下面是HotSpot Client VM生成的一段String.hashCode()方法的本地代码,能够看到在0x026eb7a9处的call指令有OopMap记录,它指明了EBX寄存器和栈中偏移量为16的内存区域中各有一个普通对象指针(Ordinary Object Pointer)的引用,有效范围为从call指令开始直到0x026eb730(指令流的起始位置)+142(OopMap记录的偏移量)=0x026eb7be,即hlt指令为止。数据结构

[Verified Entry Point]
0x026eb730:mov%eax,-0x8000(%esp)
……
;ImplicitNullCheckStub slow case
0x026eb7a9:call 0x026e83e0
;OopMap{ebx=Oop[16]=Oop off=142}
;*caload
;-java.lang.String:hashCode@48(line 1489)
;{runtime_call}
0x026eb7ae:push$0x83c5c18
;{external_word}
0x026eb7b3:call 0x026eb7b8
0x026eb7b8:pusha
0x026eb7b9:call 0x0822bec0;{runtime_call}
0x026eb7be:hlt

安全点

在OopMap的协助下,HotSpot能够快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能致使引用关系变化,或者说OopMap内容变化的指令很是多,若是为每一条指令都生成对应的OopMap,那将会须要大量的额外空间,这样GC的空间成本将会变得很高。spa

实际上,HotSpot也的确没有为每条指令都生成OopMap,只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并不是在全部地方都能停顿下来开始GC,只有在到达安全点时才能暂停。Safepoint的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以至于过度增大运行时的负荷。因此,安全点的选定基本上是以程序“是否具备让程序长时间执行的特征”为标准进行选定的——由于每条指令执行的时间都很是短暂,程序不太可能由于指令流长度太长这个缘由而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,因此具备这些功能的指令才会产生Safepoint。线程

对于Sefepoint,另外一个须要考虑的问题是如何在GC发生时让全部线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。指针

这里有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)。code

抢先式中断不须要线程的执行代码主动去配合,在GC发生时,首先把全部线程所有中断,若是发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。如今几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。对象

而主动式中断的思想是当GC须要中断线程的时候,不直接对线程操做,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就本身中断挂起。轮询标志的地方和安全点是重合的,另外再加上建立对象须要分配内存的地方。blog

下面的test指令是HotSpot生成的轮询指令,当须要暂停线程时,虚拟机把0x160100的内存页设置为不可读,线程执行到test指令时就会产生一个自陷异常信号,在预先注册的异常处理器中暂停线程实现等待,这样一条汇编指令便完成安全点轮询和触发线程中断。事件

0x01b6d627:call 0x01b2b210;OopMap{[60]=Oop off=460}
;*invokeinterface size
;-Client1:main@113(line 23)
;{virtual_call}
0x01b6d62c:nop
;OopMap{[60]=Oop off=461}
;*if_icmplt
;-Client1:main@118(line 230x01b6d62d:test%eax,0x160100;{poll}
0x01b6d633:mov 0x50(%esp),%esi
0x01b6d637:cmp%eax,%esi

安全区域

使用Safepoint彷佛已经完美地解决了如何进入GC的问题,但实际状况却并不必定。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的信号为止。

相关文章
相关标签/搜索