枚举根节点
(如何枚举/知道/列出 所有GC-ROOTs)
从可达性分析中从GC Roots节点找引用链这个操作为例,可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。(由于逐个检查会消耗很多时间,所以下下段是这么解决的)
(这里说明GC停顿的理由)
另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行——这里“一致性”的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证。这点是导致GC进行时必须停顿所有Java执行线程(Sun将这件事情称为“Stop The World”)的其中一个重要原因,即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
由于目前的主流Java虚拟机使用的都是准确式GC,所以当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。
贴 准确式GC:
准确式内存管理(Exact Memory Management,也可以叫Non-Conservative/Accurate Memory Management)而得名,即虚拟机可以知道内存中某个位置的数据具体是什么类型。譬如内存中有一个32位的整数123456,它到底是一个reference类型指向123456的内存地址还是一个数值为123456的整数,虚拟机将有能力分辨出来,这样才能在GC(垃圾收集)的时候准确判断堆上的数据是否还可能被使用
有准确式内存管理,就能够做到准确式GC
准确式GC:是指在回收过程中能准确的识别和回收每一个无用对象的GC方式
(HotSpot 实现准确式GC,不逐个检查也可以枚举GC ROOTS的方法)
在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。(标识一下 特定位置 ,这个特定位置就是 安全点)
下面的代码清单3-3是HotSpot Client VM生成的一段String.hashCode()方法的本地代码,可以看到在0x026eb7a9处的call指令有OopMap记录,它指明了EBX寄存器和栈中偏移量为16的内存区域中各有一个普通对象指针(Ordinary Object Pointer)(Oop)的引用【 OopMap{ebx=Oop [16]=Oop off=142} 】,有效范围为从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中,对象的类型信息里有记录自己的OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。这些数据是在类加载过程中计算得到的。(来源我爱学Java之JVM中的OopMap)
还不理解多看几遍上面那段汇编指令说明:(再贴一遍)
可以看到在0x026eb7a9处的call指令有OopMap记录,它指明了EBX寄存器和栈中偏移量为16的内存区域中各有一个普通对象指针(Ordinary Object Pointer)(Oop)的引用【 OopMap{ebx=Oop [16]=Oop off=142} 】,有效范围为从call指令开始直到0x026eb730(指令流的起始位置)+142(OopMap记录的偏移量)=0x026eb7be,即hlt指令为止。
安全点(Safepoint)
在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。
实际上,HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。
(加强印象再贴一下 【在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用】)
(这段说明的是Safepoint如何选取)
Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。
所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。
(下面几段说明,要GC时如何让所有线程跑到安全点停顿下来)
对于Sefepoint,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。
这里有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)
其中抢先式中断不需要线程的执行代码主动去配合,在GC发生时
一. 首先把所有线程全部中断
二. 如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。
现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。
而主动式中断的思想是
当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。
轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
下面代码清单3-4中的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 23)
0x01b6d62d: test %eax,0x160100 ; {poll}
0x01b6d633: mov 0x50(%esp),%esi
0x01b6d637: cmp %eax,%esi
安全区域(Safe Region)
(安全区域出现的原因)
使用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的信号为止。
疑问:什么样的区域才会被认为是安全区域呢?换个说法就是,怎么样才知道这段代码片段的引用关系不会发生变化呢?
(书中作者的总结)
到此,笔者简要地介绍了HotSpot虚拟机如何去发起内存回收的问题,但是虚拟机如何具体地进行内存回收动作仍然未涉及,因为内存回收如何进行是由虚拟机所采用的GC收集器决定的,而通常虚拟机中往往不止有一种GC收集器。下面继续来看HotSpot中有哪些GC收集器。
(下一篇说的各个GC收集器内容)