Java系列:JVM中的OopMap(zz)

调用栈里的引用类型数据是GC的根集合(root set)的重要组成部分;找出栈上的引用是GC的根枚举(root enumeration)中不可或缺的一环。
java

JVM选择用什么方式会影响到GC的实现:android

若是JVM选择不记录任何这种类型的数据,那么它就没法区份内存里某个位置上的数据到底应该解读为引用类型仍是整型仍是别的什么。这种条件下,实现出来的GC就会是“保守式GC(conservative GC)”。在进行GC的时候,JVM开始从一些已知位置(例如说JVM栈)开始扫描内存,扫描的时候每看到一个数字就看看它“像不像是一个指向GC堆中的指针”。这里会涉及上下边界检查(GC堆的上下界是已知的)、对齐检查(一般分配空间的时候会有对齐要求,假如说是4字节对齐,那么不能被4整除的数字就确定不是指针),之类的。而后递归的这么扫描出去。算法

保守式GC的好处是相对来讲实现简单些,并且能够方便的用在对GC没有特别支持的编程语言里提供自动内存管理功能。Boehm-Demers-Weiser GC是保守式GC中的典型表明,能够嵌入到C或C++等语言写的程序中。编程

小历史故事: 
微软的JScript和早期版VBScript也是用保守式GC的;微软的JVM也是。VBScript后来改回用引用计数了。而微软JVM的后代,也就是.NET里的CLR,则改用了彻底准确式GC。 
为了遇上在一个会议上发布消息,微软最初的JVM原型只有一个月左右的时间从开工到达到符合Java标准。因此只好先用简单的办法来实现,也就天然选用了保守式GC。 
信息来源:Patrick Dussud在Channel 9的访谈,23分钟左右安全

保守式GC的缺点有: 
一、会有部分对象原本应该已经死了,但有疑似指针指向它们,使它们逃过GC的收集。这对程序语义来讲是安全的,由于全部应该活着的对象都会是活的;但对内存占用量来讲就不是件好事,总会有一些已经不须要的数据还占用着GC堆空间。具体实现能够经过一些调节来让这种无用对象的比例少一些,能够缓解(但不能根治)内存占用量大的问题。数据结构

二、因为不知道疑似指针是否真的是指针,因此它们的值都不能改写;移动对象就意味着要修正指针。换言之,对象就不可移动了。有一种办法能够在使用保守式GC的同时支持对象的移动,那就是增长一个间接层,不直接经过指针来实现引用,而是添加一层“句柄”(handle)在中间,全部引用先指到一个句柄表里,再从句柄表找到实际对象。这样,要移动对象的话,只要修改句柄表里的内容便可。可是这样的话引用的访问速度就下降了。Sun JDK的Classic VM用过这种全handle的设计,但效果实在算不上好。编程语言

因为JVM要支持丰富的反射功能,原本就须要让对象能了解自身的结构,而这种信息GC也能够利用上,因此不多有JVM会用彻底保守式的GC。除非真的是特别懒…oop

JVM能够选择在栈上不记录类型信息,而在对象上记录类型信息。这样的话,扫描栈的时候仍然会跟上面说的过程同样,但扫描到GC堆内的对象时由于对象带有足够类型信息了,JVM就可以判断出在该对象内什么位置的数据是引用类型了。这种是“半保守式GC”,也称为“根上保守(conservative with respect to the roots)”性能

为了支持半保守式GC,运行时须要在对象上带有足够的元数据。若是是JVM的话,这些数据可能在类加载器或者对象模型的模块里计算获得,但不须要JIT编译器的特别支持。优化

前面提到了Boehm GC,实际上它不但支持彻底保守的方式,也能够支持半保守的方式。GCJ和Mono都是以半保守方式使用Boehm GC的例子。

Google Android的Dalvik VM的早期版本也是使用半保守式GC的一个例子。不过到2009年中的时候Dalvik VM的内部版本就已经开始支持准确式GC了——代价是优化过的DEX文件的体积膨胀了约9%。 
其实许多较老的JVM都选择这种实现方式。

因为半保守式GC在堆内部的数据是准确的,因此它能够在直接使用指针来实现引用的条件下支持部分对象的移动,方法是只将保守扫描能直接扫到的对象设置为不可移动(pinned),而从它们出发再扫描到的对象就能够移动了。 
彻底保守的GC一般使用不移动对象的算法,例如mark-sweep。半保守方式的GC既可使用mark-sweep,也可使用移动部分对象的算法,例如Bartlett风格的mostly-copying GC。

半保守式GC对JNI方法调用的支持会比较容易:管它是否是JNI方法调用,是栈都扫过去…完事了。不须要对引用作任何额外的处理。固然代价跟彻底保守式同样,会有“疑似指针”的问题。

与保守式GC相对的是“准确式GC”,原文能够是precise GC、exact GC、accurate GC或者type accurate GC。外国人也挺麻烦的,“准确”都统一不到一个词上⋯ 
是什么东西“准确”呢?关键就是“类型”,也就是说给定某个位置上的某块数据,要能知道它的准确类型是什么,这样才能够合理地解读数据的含义;GC所关心的含义就是“这块数据是否是指针”。 
要实现这样的GC,JVM就要可以判断出全部位置上的数据是否是指向GC堆里的引用,包括活动记录(栈+寄存器)里的数据。

有几种办法:

一、让数据自身带上标记(tag)。这种作法在JVM里不常见,但在别的一些语言实现里有体现。就不详细介绍了。打标记的方式在半保守式GC中却是更常见一些,例如CRuby就是用打标记的半保守式GC。CLDC-HI比较有趣,栈上对每一个slot都配对一个字长的tag来讲明它的类型,经过这种方式来减小stack map的开销;相似的实如今别的地方没怎么见过,你们通常都不这么取舍。 
二、让编译器为每一个方法生成特别的扫描代码。我还没见过JVM实现里这么作的,虽然说在别的语言实现里有见过。 
三、从外部记录下类型信息,存成映射表。如今三种主流的高性能JVM实现,HotSpot、JRockit和J9都是这样作的。其中,HotSpot把这样的数据结构叫作OopMap,JRockit里叫作livemap,J9里叫作GC map。Apache Harmony的DRLVM也把它叫GCMap。 
要实现这种功能,须要虚拟机里的解释器和JIT编译器都有相应的支持,由它们来生成足够的元数据提供给GC。 
使用这样的映射表通常有两种方式: 
一、每次都遍历原始的映射表,循环的一个个偏移量扫描过去;这种用法也叫“解释式”; 
二、为每一个映射表生成一块定制的扫描代码(想像扫描映射表的循环被展开的样子),之后每次要用映射表就直接执行生成的扫描代码;这种用法也叫“编译式”。

在HotSpot中,对象的类型信息里有记录本身的OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。因此从对象开始向外的扫描能够是准确的;这些数据是在类加载过程当中计算获得的。

能够把oopMap简单理解成是调试信息。 在源代码里面每一个变量都是有类型的,可是编译以后的代码就只有变量在栈上的位置了。oopMap就是一个附加的信息,告诉你栈上哪一个位置原本是个什么东西。 这个信息是在JIT编译时跟机器码一块儿产生的。由于只有编译器知道源代码跟产生的代码的对应关系。 每一个方法可能会有好几个oopMap,就是根据safepoint把一个方法的代码分红几段,每一段代码一个oopMap,做用域天然也仅限于这一段代码。 循环中引用多个对象,确定会有多个变量,编译后占据栈上的多个位置。那这段代码的oopMap就会包含多条记录。

每一个被JIT编译事后的方法也会在一些特定的位置记录下OopMap,记录了执行到该方法的某条指令的时候,栈上和寄存器里哪些位置是引用。这样GC在扫描栈的时候就会查询这些OopMap就知道哪里是引用了。这些特定的位置主要在: 
一、循环的末尾 
二、方法临返回前 / 调用方法的call指令后 
三、可能抛异常的位置

这种位置被称为“安全点”(safepoint)。之因此要选择一些特定的位置来记录OopMap,是由于若是对每条指令(的位置)都记录OopMap的话,这些记录就会比较大,那么空间开销会显得不值得。选用一些比较关键的点来记录就能有效的缩小须要记录的数据量,但仍然能达到区分引用的目的。由于这样,HotSpot中GC不是在任意位置均可以进入,而只能在safepoint处进入。 
而仍然在解释器中执行的方法则能够经过解释器里的功能自动生成出OopMap出来给GC用。

平时这些OopMap都是压缩了存在内存里的;在GC的时候才按需解压出来使用。 
HotSpot是用“解释式”的方式来使用OopMap的,每次都循环变量里面的项来扫描对应的偏移量。

对Java线程中的JNI方法,它们既不是由JVM里的解释器执行的,也不是由JVM的JIT编译器生成的,因此会缺乏OopMap信息。那么GC碰到这样的栈帧该如何维持准确性呢? 
HotSpot的解决方法是:全部通过JNI调用边界(调用JNI方法传入的参数、从JNI方法传回的返回值)的引用都必须用“句柄”(handle)包装起来。JNI须要调用Java API的时候也必须本身用句柄包装指针。在这种实现中,JNI方法里写的“jobject”实际上不是直接指向对象的指针,而是先指向一个句柄,经过句柄才能间接访问到对象。这样在扫描到JNI方法的时候就不须要扫描它的栈帧了——只要扫描句柄表就能够获得全部从JNI方法能访问到的GC堆里的对象。 
但这也就意味着调用JNI方法会有句柄的包装/拆包装的开销,是致使JNI方法的调用比较慢的缘由之一。

相关文章
相关标签/搜索