DVM GC源码研究

基于KK的代码。本文主要是介绍dalvik GC的一些概况。

DalvikGC概要

Davik GC的主要对象

  • GCHeap : 管理GC内存的对象,全局只有一个,负责GC的分配和回收
  • HeapSource: 管理Heap的对象。在GC初始化时,分配一段连续的地址空间(maximumSize), 负责管理Heap的增长
  • Heap: 为分配准备的连续空间。HeapSource包含两个Heap
  • HeapBitmap: 对Heap做mark映射的bitmap对象。HeapSource包含两个bitmap:liveBits和markBits
  • GcMarkStack: 在Mark阶段使用到的堆栈

Dalvik GC的堆分布情况


GC的堆的创建

在GC初始化时,HeapSource会创建一段连续空间,作为堆来使用。
这是由dvmHeapSourceStartup完成的。
部分代码如下:
</pre><pre name="code" class="cpp">GcHeap* dvmHeapSourceStartup(size_t startSize, size_t maximumSize,
                             size_t growthLimit)
{
....
    /*
     * Allocate a contiguous region of virtual memory to subdivided
     * among the heaps managed by the garbage collector.
     */
    length = ALIGN_UP_TO_PAGE_SIZE(maximumSize); //获取的是堆的最大值
    base = dvmAllocRegion(length, PROT_NONE, gDvm.zygote ? "dalvik-zygote" : "dalvik-heap");
    //创建了一个连续地址空间。但是并没有实际的内存分配出来
    if (base == NULL) {
        return NULL;
    }

    /* Create an unlocked dlmalloc mspace to use as
     * a heap source.
     */
    //这个创建分配器
    msp = createMspace(base, kInitialMorecoreStart, startSize);
    if (msp == NULL) {
        goto fail;
    }
.....
    //初始化heap
    if (!addInitialHeap(hs, msp, growthLimit)) {
        LOGE_HEAP("Can't add initial heap");
        goto fail;
    }
....
}

//addInitalHeap
static bool addInitialHeap(HeapSource *hs, mspace msp, size_t maximumSize)
{
    assert(hs != NULL);
    assert(msp != NULL);
    if (hs->numHeaps != 0) {
        return false;
    }
    hs->heaps[0].msp = msp;
    hs->heaps[0].maximumSize = maximumSize;
    hs->heaps[0].concurrentStartBytes = SIZE_MAX;
    hs->heaps[0].base = hs->heapBase;
    hs->heaps[0].limit = hs->heapBase + maximumSize;
    hs->heaps[0].brk = hs->heapBase + kInitialMorecoreStart; //这是初次大小,以后按照这个递增
    hs->numHeaps = 1;
    return true;
}

这个函数只会在zygote中调用。dvm为zygote保留了一段连续地址空间,但是并没有分配内存,当需要的时候,使用dvmHeapSourceAllocAndGrow来增长。

App的内存堆布局

App的堆布局有所不同,如下图:


HeapSource是dalvik中管理堆对象的对象。HeapSource定义了成员Heap heaps[HEAP_SOURCE_MAX_HEAP_COUNT];

HEAP_SOURCE_MAX_HEAP_COUNT = 2。 heaps[0]永远是当前活动Heap, 即承担新对象分配的heap,而heap[1]则是zygote分配的对象。


事情是这样的,当zygote要fork一个新进程时,ZygoteInit.preFrok会在fork之前调用,该函数最终会调用到dalvik的dvmHeapSourceStartupBeforeFork, 然后调用addNewHeap。

addNewHeap会在原heap[0]的剩余空间创建一个新的heap,然后将heap[0] copy 到heap[1],新的heap copy到heap[0]。 然后再fork。


当然,这个过程只会在第一次fork时执行,此后就不会被执行了。


我们知道,zygote进程在启动后,会preload大量的classes, 资源,这些都会存放在zygote的space中,当zygote fork出新进程后,新进程就继承了这些资源。新进程的对象,则在新创建的heap上。


Dalvik GC 的Alloctor


dvm的GC Allocator是使用外部的dl malloc的 mspace来管理的。这个在libc中定义和实现。

在 dvm dvmHeapSourceStartup函数中调用create_mspace_with_base,在heap上传教对应的对象。

mspace的函数有

  • create_mspace_with_base
  • mspace_malloc/mspace_calloc
  • mspace_bulk_free
  • mspace_inspect_all

这些函数的用法和简单,具体的实现可以在看对应源码。


Dalvik GC过程


Dalvik GC的启动方式及入口

dvmCollectGarbageInternal 函数是所有类型GC的入口。

GC的启动方式有如下几种:

  1. GC_FOR_MALLOC: 当分配空间时,发现空间不足
  2. GC_CONCURRENT: 以并发方式启动GC
  3. GC_EXPLICIT: 通过system.runtime.gc调用的GC
  4. GC_BEFORE_OOM:OOM之前的GC

每种GC的处理方式又不同,根据它们的配置GcSpec数据的定义,可以用下表俩表示

并行清理软引用
FOR_MALLOC T F T
CONCURRENT T T T
EXPLICIT F T T
OOM F FF
说明 app heap被回收
zygote heap不回收 执行并行F时执行软引用

GC类型APP Heap不足并行清理软引用
FOR_MALLOC 是 
CONCURRENT
EXPLICIT
OOM

  1. AppHeap不足:描述了GC 引起的原因里面是否包含了App Heap不足,即便不是App Heap引起的,也会清理App Heap;
  2. 并行:是否以并行方式GC
  3. 清理引用:是否清理引用。

Dalvik GC使用的算法

DVM 使用mark-sweep算法。

HeapSource 定义了HeapBitmap liveBits和markBits来标记bitmap。

make sweep算法简介

简单的mark sweep算法 (2色法)


这个过程是:
  1. 新分配对象都是白色 (liveBits中做标记)
  2. 从root对象开始,遍历每个root对象,对每个对象做mark,使之变成黑色(markBits中做标记),同时将对象加入worklist中
  3. 遍历worklist中的对象的白色子对象,对每个白色子对象做mark,使之变成黑色,同时将该子对象加入worklist。
  4. worklist空后,就结束了mark过程
  5. 将所有白色对象清除(liveBits中有标记但是markBits中没有标记的对象)
  6. 黑色变为白色(用markBits替换liveBits)

增量mark-sweep算法 (3色法)


与2色法不同在于,当一个对象被mark后,不是变为黑色,而是变为灰色,这是该对象还在worklist中;当从worklist中取出该对象,并mark了它的所有子对象后,它才会变成黑色。

灰色表示对象本身已经被mark,但是其子对象没有被mark。

这种方法用于并行mark。方法是
  1. mark roots时,暂停所有java线程,完成roots mark后,重启所有java线程
  2. 并行进行mark栈中对象
  3. 当java线程修改对象后,将被修改对象重新标记为gray (write barrier)
  4. 栈空后,再次暂停所有java线程, 重新mark gray对象
  5. 重启java线程对象
  6. sweep

dalvik中的 mark-sweep算法实现

dvmHeapMarkRootSet

mark root对象,root对象包括:
  • 由root classloader定义的系统class
  • 对于每个线程:
    • interpreted stack, from top to "curFrame"
      • davik registers (args + local vars)
    • JNI local references
    • Automatic VM local reference (TrackedAlloc)
    • Associated Thread/VMThread object
    • ThreadGroups (could track & start with these instead of working upward from threads)
    • JNI global references
    • 内部string table
    • gDvm.outOfMemoryObj
    • Objects in debugger object registry

dvmHeapScanMarkedObjects

该函数首先调用
dvmHeapBitmapScanWalk(ctx->bitmap, scanBitmapCallback, ctx);
ctx是类型GcMarkContext, ctx->bitmap是heapsource的markBits,这是保存的都是root对象。

这种方法省去了worklist。
scanBitmapCallback函数扫描每个对象的子对象,然后将他们mark后加入到堆栈中。

然后调用 processMarkStack。该函数访问堆栈中的每个对象的子对象,然后再将子对象放入堆栈中。知道堆栈为空,就完成了扫描。
堆栈是用GcMarkStack对象实现的,该类用一个object* 数组来作为栈。

实现mark的是函数 markObjectNonNull。

dvmHeapProcessReferences

该函数主要负责处理各种引用,包括softRefereces, weakReferences, phatomReferences,
并且会调用finalize函数。

dvmHeapSweepUnmarkedObjects

该函数负责清除垃圾对象。最终负责释放对象的是函数dvmHeapSourceFreeList,也是用mspace来实现的。有兴趣的可以自己看看。

dvm 的并行GC

请考虑一个问题,如果当前正在  dvmHeapScanMarkedObjects,  其中某个对象(加入是A对象)已经变成黑色了,这时某个java线程修改了A的某个field,改变了它的reachable树。这个时候,我们需要知道这个A对象已经被修改了,当dvmHeapScanMarkedObjects完成后,我们需要再次对这些对象进行Remark。
那么,我们怎么知道那些对象被修改了呢?

需要两个东西:CardTable和write barrier。

cardtable
如下图:

内存被分为若干card (如512字节为一个card)
CardTable中每一个小格代表一个card(1字节)。如果被设置,表示对应的card内的对象被修改,需要重新mark
在iput-object这样的指令插入对card table的修改

DVM中 dvmCardTableStartup 初始化 card table。 每个card大小为GC_CARD_SIZE (1<<7)。catd table 为每个card映射一个byte,该byte值是GC_CARD_CLEAN或者GC_CARD_DIRTY
  1. dvmCardFromAddr: 从addr得到card的标志字节
  2. dvmMarkCard: 将card置脏
write barrier

dvmSetFieldObject: write barrier,调用dvmMarkCard,将card table置脏。这个函数在dvm内部和jni函数SetFieldObject中会被调用。

在虚拟机内部,则通过IPUT_OBJECT指令的汇编函数来实现:

.LOP_IPUT_OBJECT_finish:
....
    ldr     r2, [rSELF, #offThread_cardTable]  @ r2<- card table base                                                                                                                                               
...
    cmp     r0, #0                      @ stored a null reference?
    strneb  r2, [r2, r9, lsr #GC_CARD_SHIFT]  @ mark card if not

//r2地址最低字节正好是CARD_DIRTY值。
为了节省cpu,dvm故意将cardtable数组的前0x7f大小的位置空出,从0x7f后开始计算,于是,他就可以用cartable数组的地址的低8位(r2)来保存标志值了。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值