Dalvik虚拟机Java堆创建过程分析

721 查看

从前面Dalvik虚拟机垃圾收集机制简要介绍和学习计划一文可以知道,在Dalvik虚拟机中,Java堆实际上是由一个Active堆和一个Zygote堆组成的,如图1所示:

图1 Dalvik虚拟机的Java堆

        其中,Zygote堆用来管理Zygote进程在启动过程中预加载和创建的各种对象,而Active堆是在Zygote进程fork第一个子进程之前创建的。之后无论是Zygote进程还是其子进程,都在Active堆上进行对象分配和释放。这样做的目的是使得Zygote进程和其子进程最大限度地共享Zygote堆所占用的内存。

为了管理Java堆,Dalvik虚拟机需要一些辅助数据结构,包括一个Card Table、两个Heap Bitmap和一个Mark Stack。Card Table是为了记录在垃圾收集过程中对象的引用情况的,以便可以实现Concurrent G。图1的两个Heap Bitmap,一个称为Live Heap Bitmap,用来记录上次GC之后,还存活的对象,另一个称为Mark Heap Bitmap,用来记录当前GC中还存活的对象。这样,上次GC后存活的但是当前GC不存活的对象,就是需要释放的对象。Davlk虚拟机使用标记-清除(Mark-Sweep)算法进行GC。在标记阶段,通过一个Mark Stack来实现递归检查被引用的对象,即在当前GC中存活的对象。有了这个Mark Stack,就可以通过循环来模拟函数递归调用。

Dalvik虚拟机Java堆的创建过程实际上就是上面分析的各种数据结构的创建过程,它们是在Dalvik虚拟机启动的过程中创建的。接下来,我们就详细分析这个过程。

从前面Dalvik虚拟机的启动过程分析一文可以知道,Dalvik虚拟机在启动的过程中,会通过调用函数dvmGcStartup来创建Java堆,它的实现如下所示:

这个函数定义在文件dalvik/vm/alloc/Alloc.cpp中。

函数dvmGcStartup首先是分别初始化一个锁和一个条件变量,它们都是用来保护堆的并行访问的,接着再调用另外一个函数dvmHeapStartup来创建Java堆。

函数dvmHeapStartup的实现如下所示:

这个函数定义在文件dalvik/vm/alloc/Heap.cpp中。

gDvm是一个类型为DvmGlobals的全局变量,它通过各个成员变量记录了Dalvik虚拟机的各种信息。这里涉及到三个重要与Java堆相关的信息,分别是Java堆的起始大小(Starting Size)、最大值(Maximum Size)和增长上限值(Growth Limit)。在启动Dalvik虚拟机的时候,我们可以分别通过-Xms、-Xmx和-XX:HeapGrowthLimit三个选项来指定上述三个值。

Java堆的起始大小(Starting Size)指定了Davlik虚拟机在启动的时候向系统申请的物理内存的大小。后面再根据需要逐渐向系统申请更多的物理内存,直到达到最大值(Maximum Size)为止。这是一种按需要分配策略,可以避免内存浪费。在默认情况下,Java堆的起始大小(Starting Size)和最大值(Maximum Size)等于4M和16M。但是厂商会通过dalvik.vm.heapstartsize和dalvik.vm.heapsize这两个属性将它们设置为合适设备的值的。

注意,虽然Java堆使用的物理内存是按需要分配的,但是它使用的虚拟内存的总大小却是需要在Dalvik启动的时候就确定的。这个虚拟内存的大小就等于Java堆的最大值(Maximum Size)。想象一下,如果不这样做的话,会出现什么情况。假设开始时创建的虚拟内存小于Java堆的最大值(Maximum Size),由于实际情况是允许虚拟内存的大小是达到Java堆的最大值(Maximum Size)的,因此,当开始时创建的虚拟内存无法满足需求时,那么就需要重新创建另外一块更大的虚拟内存。这样就需要将之前的虚拟内存的内容拷贝到新创建的更大的虚拟内存去,并且还要相应地修改各种辅助数据结构。这样太麻烦了,而且效率也太低了。因此就在一开始的时候,就创建一块与Java堆的最大值(Maximum Size)相等的虚拟内存。

但是,Dalvik虚拟机又希望能够动态地调整Java堆的可用最大值,于是就出现了一个称为增长上限的值(Growth Limit)。这个增长上限值(Growth Limit),我们可以认为它是Java堆大小的软限制,而前面所描述的最大值(Maximum Size),是Java堆大小的硬限制。通过动态地调整增长上限值(Growth Limit),就可以实现动态调整Java堆的可用最大值,但是这个增长上限值必须要小于等于最大值(Maximum Size)。从函数dvmHeapStartup的实现可以知道,如果没有指定Java堆的增长上限的值(Growth Limit),那么它的值就等于Java堆的最大值(Maximum Size)。

事实上,在全局变量gDvm中,除了上面提到的三个信息之外,还有三种信息是与Java堆相关的,它们分别是堆最小空闲值(Min Free)、堆最大空闲值(Max Free)和堆目标利用率(Target Utilization)。这三个值可以分别通过Dalvik虚拟机的启动选项-XX:HeapMinFree、-XX:HeapMaxFree和-XX:HeapTargetUtilization来指定。它们用来确保每次GC之后,Java堆已经使用和空闲的内存有一个合适的比例,这样可以尽量地减少GC的次数。举个例子说,堆的利用率为U,最小空闲值为MinFree字节,最大空闲值为MaxFree字节。假设在某一次GC之后,存活对象占用内存的大小为LiveSize。那么这时候堆的理想大小应该为(LiveSize / U)。但是(LiveSize / U)必须大于等于(LiveSize + MinFree)并且小于等于(LiveSize + MaxFree)。

了解了这些与Java堆大小相关的信息之后,我们回到函数dvmGcStartup中,可以清楚看到,它先是调用函数dvmHeapSourceStartup来创建一个Java堆,接着再调用函数dvmCardTableStartup来为该Java堆创建一个Card Table。接下来我们先分析函数dvmHeapSourceStartup的实现,接着再分析函数dvmCardTableStartup的实现。

函数dvmHeapSourceStartup的实现如下所示: