HotSpot VM,相信所有Java程序员都知道,它是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。
前面从理论上介绍了对象存活判定算法和垃圾收集算法,而在HotSpot虚拟机上实现这些算法的时候必须对算法的执行效率有严格的考量才能保证虚拟机高效运行。
接下来说三点:
1、 枚举根节点
2、 安全点
3、 安全区域
1、 枚举根节点
以 可达性算法中从GC Roots节点找到引用链这个操作 为例来说:可以作为GC Roots的节点主要在全局性的引用(例如常量或静态类属性)和执行上下文(例如栈帧中的本地变量表)中,而很多的应用仅仅方法区就有数百兆,如果逐个检查这里的引用必然会消耗很多时间。另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项工作必须在一个能确保一致性的快照中进行,所谓“一致”是指在整个分析期间整个执行系统看起来就像被冻结在一个时间点上,不可能出现分析过程中对象引用关系不断变化的情况,如果不能保持这种一致性就无法保证结果的准确性。这是导致GC进行时必须停顿所有java执行线程的其中一个原因。即使是在号称几乎不会发生停顿的CMS收集器(一款垃圾收集器)中,枚举根节点时也是必须要停顿的。
由于目前的主流java虚拟机使用的都是准确式GC,所以当执行系统停顿下来之后,并不需要一个不漏的检查完所有执行上下文和全局的引用位置,因为虚拟机是有办法直接得知哪些地方存放着对象引用的。那么是怎么实现的呢?在HotSpot的实现中,是用一组称为OopMap的数据结构来达到这个目的的。在类加载完成的时候HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。
备注:
OopMap:帮助jvm得知哪些地方存放着对象引用
可以把OopMap简单理解成调试信息
在源码里每个变量都是有类型的,但是编译之后的代码就只有变量在栈上的位置了, OopMap就是一个附加的信息,告诉你栈上哪个位置本来是什么东西。这个信息是在JIT编译时跟机器码一起产生的,因为只有编译器知道源码跟产生的代码的对应关系。
JIT编译器(Just In Time Compiler 即时编译器 )
把java字节码(包括需要被解释的指令程序)转换成可以直接发送给处理器的指令程序。
内存储器(内存),通常也泛称为主存储器,它是相对于外存而言的。内存储器是计算机中重要的部件之一,它是与CPU进行沟通的桥梁。计算机中所有程序的运行都是在内存储器中进行的,因此内存储器的性能对计算机的影响非常大。其作用是用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。只要计算机在运行中,CPU就会把需要运算的数据调到内存中进行运算,当运算完成后CPU再将结果传送出来,内存的运行也决定了计算机的稳定运行。 内存是由内存芯片、电路板、金手指等部分组成的。
外存储器是指除计算机内存及CPU缓存以外的储存器,此类储存器一般断电后仍然能保存数据。常见的外存储器有硬盘、软盘、光盘、U盘等,容量一般比较大,缺点是读写速度慢。
缓存,就是数据交换的缓冲区(称作Cache),当某一硬件要读取数据时,会首先从缓存中查找需要的数据,如果找到了则直接执行,找不到的话则从内存中找。由于缓存的运行速度比内存快得多,故缓存的作用就是帮助硬件更快地运行。
因为缓存往往使用的是RAM(断电即掉的非永久储存),所以在用完后还是会把文件送到硬盘等存储器里永久存储。电脑里最大的缓存就是内存条了,最快的是CPU上镶的L1和L2缓存,显卡的显存是给显卡运算芯片用的缓存,硬盘上也有16M或者32M的缓存。
寄存器, 寄存器是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和地址。在中央处理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序计数器(PC)。在中央处理器的算术及逻辑部件中,存器有累加器(ACC)。
2、 安全点
在OopMap协助下,HotSpot可以准确地完成GC Roots的枚举。实际上,HotSpot没有为每条指令都生成OopMap,只是在特定位置记录这些信息。这些位置称为安全点(Safepoint)。即程序执行时并非在所有位置都能停下来GC,只有到达安全点才可以暂停。Safepoint的选定不能太少以至于让GC等待时间太长,也不能过于频繁以至于过分增大运行时的负荷。
safepoint指的特定位置主要有:
1、循环的末尾(防止大循环的时候一直不进入safepoint,而其他线程在等待它进入safepoint)
2、方法返回前
3、调用方法的call之后
4、抛出异常的位置
之所以选择这些位置作为safepoint的插入点主要考虑的是“避免程序长时间运行而不进入safepoint”。
对于safepoint另一个问题就是如何在GC发生时让所有线程都跑到最近的安全点上再停顿下来。有两种方案,抢占式中断和主动式中断。
其中抢占式中断不需要线程的执行代码主动去配合,在GC发生时,首先把线程中断,如果发现有线程中断的地方不再安全点上就恢复线程让他跑到安全点上。现在几乎没有虚拟机用这个方式。
主动式中断思想就是GC需要中断线程时不直接对线程进行操作,仅仅简单的设置一个标志(这个标志与安全点重合,另外再加上创建对象需要分配内存的地方)各个线程主动地去轮循这个标志,发现中断标志为真时就自己中断挂起。
3、 安全区域
safepoint机制保证了程序执行时在不太长的时间里就会遇到GC的safepoint。但是程序不执行(即没有分配CPU时间)的时候呢?典型例子就是线程处于Sleep状态或Block状态。这时线程无法响应JVM的中断请求走到安全点去中断挂起,JVM显然也不太可能等待线程重新被分配CPU时间,对于这种情况就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码中,引用关系不会发生变化,在这个区域的任意地方开始GC都是安全的。我们可以把Safe Region看做是被扩展了的Safepoint。当线程执行到Safe Region中的代码时,首先标识自己进入了safe Region 。当这段时间JVM要发起GC时,就不用管标识自己为SafeRegion状态的线程了。在线程要离开Safe Region时,它要检查系统是否已完成了根节点枚举(或者整个GC过程),如果完成了,线程继续执行,否则他就必须等待直到收到可以安全离开SafeRegion的信号为止。