Dalvik虚拟机中每一个方法都由一个称作Method的结构体来表示(包括JNI方法)。在这个结构体中,有一个指向所谓RegisterMap结构的指针:
struct Method { ClassObject* clazz; u4 accessFlags; u2 methodIndex; u2 registersSize; u2 outsSize; u2 insSize; const char* name; DexProto prototype; const char* shorty; const u2* insns; int jniArgInfo; DalvikBridgeFunc nativeFunc; bool fastJni; bool noRef; bool shouldTrace; const RegisterMap* registerMap; bool inProfile; };
在某些文章中只是大致提了一下RegisterMap是用来做精确垃圾收集的(Precise GC),但一直也没人具体解释过它是怎么生成的,具体代表了什么意思。这些天花了一些功夫,仔细看了Dalvik代码,大致了解了其原理,特此写篇文章记录一下,免得以后忘记。
首先,因为每一个Method结构体都包含一个指向RegisterMap结构体的指针,指向的内容只对这个Method有效,所以RegisterMap肯定是作用于这个特定方法的。也就是说,不同的方法,是有不同的RegisterMap的。
好,弄清了这点,接下来看看这个所谓RegisterMap结构的具体定义:
enum RegisterMapFormat { kRegMapFormatUnknown = 0, kRegMapFormatNone, kRegMapFormatCompact8, kRegMapFormatCompact16, kRegMapFormatDifferential, kRegMapFormatOnHeap = 0x80, }; struct RegisterMap { u1 format; u1 regWidth; u1 numEntries[2]; u1 data[1]; };
结构体的最开始的一个字节(format)用来指明该RegisterMap要用多少字节来表示方法内的指令地址。每一个方法都有由许多D指令组成的,有的负责的方法内含许多条指令;而也有很多简单的方法只包含几条指令。如果方法内指令的条数少于256的话,就可以只用一个字节来表示指令地址,这时format就会被设置成kRegMapFormatCompact8;但是,如果包含的指令条数超过了这个数字,就需要用两个字节来表示了,这时format就会被设置成kRegMapFormatCompact16。不过,最多也就只会用两个字节了,这里也就隐含了一个限制,就是每个方法内的指令条数不能超过65536(2^16)。同时,也可以对RegisterMap的数据进行压缩,从而减少存储空间,这样的话format就会被相应的设置成kRegMapFormatDifferential。最后,RegisterMap是需要算出来的,一共有两个地方可以获得这个RegisterMap。一是在Dex文件第一次被加载时,Dalvik虚拟机会对这个Dex文件做一次彻头彻尾的检查(Verify),在检查的过程中会顺带在堆中申请一段空间来存放这个RegisterMap。但是,验证的过程实在是太费时间了,如果每次加载都验证的话,那么程序启动的速度将会受到极大的影响,因此Dalvik在第一次加载一个Dex文件过后,会生成一个针对该Dex文件的优化文件,也就是ODex文件,在这个文件的末尾会记录下来计算过的RegisterMap。那么下次再加载这个Dex文件,就可以直接从ODex中读取计算好的数据了,不需要再重复计算了,这就是第二个可以获得RegisterMap的地方。对于第一种情况,RegisterMap是放在堆中的,因此format的最高位会被设置成1,也就是kRegMapFormatOnHeap所要代表的情况;而对于第二种情况,RegisterMap是直接从ODex文件中mmap到内存中的,所以format最高位是0。
接下来的一个字节(regWidth)用来指明到底需要多少字节来表示方法内各个寄存器的状态。对于一个方法来说,无论是其入参,还是自己定义的局部变量,都是使用了一个个虚拟寄存器。并且,每个方法所要使用的寄存器数目是不固定的。对于每一个虚拟寄存器,RegisterMap都需要使用一个比特位来表示其状态。所以,一共要占用多少字节来表示一个方法的虚拟寄存器的状态是不固定的,并且必须是一个字节(8比特位)的整数倍。举例来说,假设一个方法共使用了5个寄存器,那么就只要使用一个字节来表示就可以了;而如果用了10个的话,就需要两个字节表示。
下面的两个字节(numEntries[2])用来指明RegisterMap中保存了多少条寄存器状态的记录。numEntries[0]表示数字的低8位,而numEntries[1]表示数字的高8位。所以,一共16位,也就是说RegisterMap最多只能存放65536(2^16)条记录。
最后的data,就是存放实际数据的地方了。具体每一条的存放格式,是根据前面format和regWidth的不同取值而相应变化的。可以参看函数dvmGenerateRegisterMapV内的赋值代码(代码位于\dalvik\vm\analysis\RegisterMap.cpp内):
mapData = pMap->data; for (i = 0; i < (int) vdata->insnsSize; i++) { if (dvmInsnIsGcPoint(vdata->insnFlags, i)) { assert(vdata->registerLines[i].regTypes != NULL); if (format == kRegMapFormatCompact8) { *mapData++ = i; } else /*kRegMapFormatCompact16*/ { *mapData++ = i & 0xff; *mapData++ = i >> 8; } outputTypeVector(vdata->registerLines[i].regTypes, vdata->insnRegCount, mapData); mapData += regWidth; } }
代码逐一检查方法中的每条指令,并判断这条指令是否是一个GC安全点。Dalvik虚拟机其实是Java虚拟机的一个变种,其也有所谓的垃圾回收的机制。但垃圾回收并不是可以在每条指令都做的,Dalvik定义了,GC只可以在具有以下四个特性的指令上才可以进行(代码位于\dalvik\libdex\InstrUtils.cpp内):
#define VERIFY_GC_INST_MASK (kInstrCanBranch | kInstrCanSwitch |\ kInstrCanThrow | kInstrCanReturn)
所谓kInstrCanBranch表示指令是一个条件转移,类似if ... else ...语句;kInstrCanSwith表示指令类似于switch ... case ...语句;kInstrCanThrow表示指令会抛出异常;kInstrCanReturn表示一个类似返回的指令,如从一个函数返回。每一条指令,根据它们功能的不同具有不同的操作码(Operation Code),且都天生带上了这些定义的属性,不随参数的不同而变化(在\dalvik\libdex\InstrUtils.cpp中定义了一个全局变量gOpcodeFlagsTable,明确申明了每条指令的具体属性)。
如果这条指令是一个GC安全点的话,那么会首先写入一个该指令在方法中的下标。刚才说过了,如果方法内指令数目不超过256,则只需要一个字节;而如果方法很大,指令很多,就需要两个字节,并且低字节写入低8位,高字节写入高8位。
可以看出来,data段的数据长度,其实可以通过前面的format、regWidth和numEntries的具体数值计算出来的(如果没有压缩过的话):
1)如果format的值是kRegMapFormatCompact8的话,计算公式如下:
length(data) = (1 + regWidth) * numEntries
2)如果format的值是kRegMapFormatCompact16的话,则计算公式如下:
length(data) = (2 + regWidth) * numEntries
接下来,会调用outputTypeVector函数,代码如下:
static void outputTypeVector(const RegType* regs, int insnRegCount, u1* data) { u1 val = 0; int i; for (i = 0; i < insnRegCount; i++) { RegType type = *regs++; val >>= 1; if (isReferenceType(type)) val |= 0x80; if ((i & 0x07) == 7) *data++ = val; } if ((i & 0x07) != 0) { val >>= 8 - (i & 0x07); *data++ = val; } }
这个函数也很简单,会逐一检查方法中用到的每一个寄存器,判断寄存器保存值的类型是否是指向某个对象的引用类型,如果是的话,就将相应的比特位置1。如果寄存器数目超过了8的话,就相应的写下一个字节。
所以,经过以上分析,就很明白了,所谓的RegisterMap记录的就是某个方法内部,在每一个GC安全点上,所有使用到的寄存器中,到底有哪几个其值是指向某个对象的引用的。那么Dalvik虚拟机在做GC的时候,就可以读取这些寄存器的值,并且保证这些值指向的引用对象不能被垃圾回收。
但是,那到底是怎么知道某个寄存器中存放的值是否是指向某个对象的呢?这主要是通过所谓的代码验证(Verify)过程。验证过程中,会逐个类,逐个方法的检查所有的指令,并模拟所有指令对各个寄存器的操作,从而最终获得每个寄存器的使用状况信息。这一部分比较复杂,会专门写一篇博客进行具体分析。