Dalvik虚拟机垃圾收集(GC)过程分析.docx
Dalvik虚拟机垃圾收集(GC)过程分析前面我们分析了Dalvik虚拟机堆的创建过程,以及Java对象在堆上的分配过程。这些知识都是理解Dalvik虚拟机垃圾收集过程的基础。垃圾收集是一个复杂的过程,它要将那些不再被引用的对象进行回收。一方面要求Dalvik虚拟机能够标记出哪些对象是不再被引用的。另一方面要求Dalvik虚拟机尽快地回收内存,避免应用程序长时间停顿。本文就将详细分析Dalvik虚拟机是如何解决上述问题完成垃圾收集过程的。Dalvik虚拟机使用Mark-Sweep算法来进行垃圾收集。顾名思义,Mark-Sweep算法就是为Mark和Sweep两个阶段进行垃圾回收。其中,Mark阶段从根集(Root Set)开始,递归地标记出当前所有被引用的对象,而Sweep阶段负责回收那些没有被引用的对象。在分析Dalvik虚拟机使用的Mark-Sweep算法之前,我们先来了解一下什么情况下会触发GC。 Dalvik虚拟机在三种情况下会触发四种类型的GC。每一种类型GC使用一个GcSpec结构体来描述,它的定义如下所示:cpp view plain copy 在CODE上查看代码片派生到我的代码片struct GcSpec /* If true, only the application heap is threatened. */ bool isPartial; /* If true, the trace is run concurrently with the mutator. */ bool isConcurrent; /* Toggles for the soft reference clearing policy. */ bool doPreserve; /* A name for this garbage collection mode. */ const char *reason; ; 这个结构体定义在文件dalvik/vm/alloc/Heap.h中。 GcSpec结构体的各个成员变量的含义如下所示: isPartial: 为true时,表示仅仅回收Active堆的垃圾;为false时,表示同时回收Active堆和Zygote堆的垃圾。 isConcurrent: 为true时,表示执行并行GC;为false时,表示执行非并行GC。 doPreserve: 为true时,表示在执行GC的过程中,不回收软引用引用的对象;为false时,表示在执行GC的过程中,回收软引用引用的对象。 reason: 一个描述性的字符串。 Davlik虚拟机定义了四种类的GC,如下所示:cpp view plain copy 在CODE上查看代码片派生到我的代码片/* Not enough space for an "ordinary" Object to be allocated. */ extern const GcSpec *GC_FOR_MALLOC; /* Automatic GC triggered by exceeding a heap occupancy threshold. */ extern const GcSpec *GC_CONCURRENT; /* Explicit GC via Runtime.gc(), VMRuntime.gc(), or SIGUSR1. */ extern const GcSpec *GC_EXPLICIT; /* Final attempt to reclaim memory before throwing an OOM. */ extern const GcSpec *GC_BEFORE_OOM; 这四个全局变量声明在文件dalvik/vm/alloc/Heap.h中。 它们的含义如下所示: GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC。 GC_CONCURRENT: 表示是在已分配内存达到一定量之后触发的GC。 GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。 GC_BEFORE_OOM: 表示是在准备抛OOM异常之前进行的最后努力而触发的GC。 实际上,GC_FOR_MALLOC、GC_CONCURRENT和GC_BEFORE_OOM三种类型的GC都是在分配对象的过程触发的。 在前面一文,我们提到,Dalvik虚拟机在Java堆上分配对象的时候,在碰到分配失败的情况,会尝试调用函数gcForMalloc进行垃圾回收。 函数gcForMalloc的实现如下所示:cpp view plain copy 在CODE上查看代码片派生到我的代码片static void gcForMalloc(bool clearSoftReferences) . const GcSpec *spec = clearSoftReferences ? GC_BEFORE_OOM : GC_FOR_MALLOC; dvmCollectGarbageInternal(spec); 这个函数定义在文件dalvik/vm/alloc/Heap.cpp中。 参数clearSOftRefereces表示是否要对软引用引用的对象进行回收。如果要对软引用引用的对象进行回收,那么就表明当前内存是非常紧张的了,因此,这时候执行的就是GC_BEFORE_OOM类型的GC。否则的话,执行的就是GC_FOR_MALLOC类型的GC。它们都是通过调用函数dvmCollectGarbageInternal来执行的。 在前面一文,我们也提到,当Dalvik虚拟机成功地在堆上分配一个对象之后,会检查一下当前分配的内存是否超出一个阀值,如下所示:cpp view plain copy 在CODE上查看代码片派生到我的代码片void* dvmHeapSourceAlloc(size_t n) . HeapSource *hs = gHs; Heap* heap = hs2heap(hs); if (heap->bytesAllocated + n > hs->softLimit) . return NULL; void* ptr; if (gDvm.lowMemoryMode) . ptr = mspace_malloc(heap->msp, n); . else ptr = mspace_calloc(heap->msp, 1, n); . countAllocation(heap, ptr); . if (heap->bytesAllocated > heap->concurrentStartBytes) . dvmSignalCond(&gHs->gcThreadCond); return ptr; 这个函数定义在文件dalvik/vm/alloc/HeapSource.cpp中。 函数dvmHeapSourceAlloc成功地在Active堆上分配到一个对象之后,就会检查Active堆当前已经分配的内存(heap->bytesAllocated)是否大于预设的阀值(heap->concurrentStartBytes)。如果大于,那么就会通过条件变量gHs->gcThreadCond唤醒GC线程进行垃圾回收。预设的阀值(heap->concurrentStartBytes)是一个比指定的堆最小空闲内存小128K的数值。也就是说,当堆的空闲内不足时,就会触发GC_CONCURRENT类型的GC。 GC线程是Dalvik虚拟机启动的过程中创建的,它的执行体函数是gcDaemonThread,实现如下所示:cpp view plain copy 在CODE上查看代码片派生到我的代码片static void *gcDaemonThread(void* arg) dvmChangeStatus(NULL, THREAD_VMWAIT); dvmLockMutex(&gHs->gcThreadMutex); while (gHs->gcThreadShutdown != true) bool trim = false; if (gHs->gcThreadTrimNeeded) int result = dvmRelativeCondWait(&gHs->gcThreadCond, &gHs->gcThreadMutex, HEAP_TRIM_IDLE_TIME_MS, 0); if (result = ETIMEDOUT) /* Timed out waiting for a GC request, schedule a heap trim. */ trim = true; else dvmWaitCond(&gHs->gcThreadCond, &gHs->gcThreadMutex); . dvmLockHeap(); if (!gDvm.gcHeap->gcRunning) dvmChangeStatus(NULL, THREAD_RUNNING); if (trim) trimHeaps(); gHs->gcThreadTrimNeeded = false; else dvmCollectGarbageInternal(GC_CONCURRENT); gHs->gcThreadTrimNeeded = true; dvmChangeStatus(NULL, THREAD_VMWAIT); dvmUnlockHeap(); dvmChangeStatus(NULL, THREAD_RUNNING); return NULL; 这个函数定义在文件dalvik/vm/alloc/HeapSource.cpp中。 GC线程平时没事的时候,就在条件变量gHs->gcThreadCond上进行等待HEAP_TRIM_IDLE_TIME_MS毫秒(5000毫秒)。如果在HEAP_TRIM_IDLE_TIME_MS毫秒内,都没有得到执行GC的通知,那么它就调用函数trimHeaps对Java堆进行裁剪,以便可以将堆上的一些没有使用到的内存交还给内核。函数trimHeaps的实现可以参考前面一文。否则的话,就会调用函数dvmCollectGarbageInternal进行类型为GC_CONCURRENT的GC。 注意,函数gcDaemonThread在调用函数dvmCollectGarbageInternal进行类型为GC_CONCURRENT的GC之前,会先调用函数dvmLockHeap来锁定堆的。等到GC执行完毕,再调用函数dvmUnlockHeap来解除对堆的锁定。这与函数gcForMalloc调用函数dvmCollectGarbageInternal进行类型为GC_FOR_MALLOC和GC_CONCURRENT的GC是一样的。只不过,对堆进行锁定和解锁的操作是在调用堆栈上的函数dvmMalloc进行的,具体可以参考前面一文。 当应用程序调用System.gc、VMRuntime.gc接口,或者接收到SIGUSR1信号时,最终会调用到函数dvmCollectGarbage,它的实现如下所示:cpp view plain copy 在CODE上查看代码片派生到我的代码片void dvmCollectGarbage() if (gDvm.disableExplicitGc) return; dvmLockHeap(); dvmWaitForConcurrentGcToComplete(); dvmCollectGarbageInternal(GC_EXPLICIT); dvmUnlockHeap(); 这个函数定义在文件dalvik/vm/alloc/Alloc.cpp中。 如果Davik虚拟机在启动的时候,通过-XX:+DisableExplicitGC选项禁用了显式GC,那么函数dvmCollectGarbage什么也不做就返回了。这意味着Dalvik虚拟机可能会不支持应用程序显式的GC请求。 一旦Dalvik虚拟机支持显式GC,那么函数dvmCollectGarbage就会先锁定堆,并且等待有可能正在执行的GC_CONCURRENT类型的GC完成之后,再调用函数dvmCollectGarbageInternal进行类型为GC_EXPLICIT的GC。 以上就是三种情况下会触发四种类型的GC。有了这个背景知识之后 ,我们接下来就开始分析Dalvik虚拟机使用的Mark-Sweep算法,整个过程如图1所示:图1 Dalvik虚拟机垃圾收集过程 Dalvik虚拟机支持非并行和并行两种GC。在图1中,左边是非并行GC的执行过程,而右边是并行GC的执行过程。它们的总体流程是相似的,主要差别在于前者在执行的过程中一直是挂起非GC线程的,而后者是有条件地挂起非GC线程。 由前面的分析可以知道,无论是并行GC,还是非并行GC,它们都是通过函数dvmCollectGarbageInternal来执行的。函数dvmCollectGarbageInternal的实现如下所示:cpp view plain copy 在CODE上查看代码片派生到我的代码片void dvmCollectGarbageInternal(const GcSpec* spec) . if (gcHeap->gcRunning) . return; . gcHeap->gcRunning = true; . dvmSuspendAllThreads(SUSPEND_FOR_GC); . if (!dvmHeapBeginMarkStep(spec->isPartial) . dvmAbort(); dvmHeapMarkRootSet(); . if (spec->isConcurrent) . dvmClearCardTable(); dvmUnlockHeap(); dvmResumeAllThreads(SUSPEND_FOR_GC); . dvmHeapScanMarkedObjects(); if (spec->isConcurrent) . dvmLockHeap(); . dvmSuspendAllThreads(SUSPEND_FOR_GC); . dvmHeapReMarkRootSet(); . dvmHeapReScanMarkedObjects(); dvmHeapProcessReferences(&gcHeap->softReferences, spec->doPreserve = false, &gcHeap->weakReferences, &gcHeap->finalizerReferences, &gcHeap->phantomReferences); . dvmHeapSweepSystemWeaks(); . dvmHeapSourceSwapBitmaps(); . if (spec->isConcurrent) dvmUnlockHeap(); dvmResumeAllThreads(SUSPEND_FOR_GC); . dvmHeapSweepUnmarkedObjects(spec->isPartial, spec->isConcurrent, &numObjectsFreed, &numBytesFreed); . dvmHeapFinishMarkStep(); if (spec->isConcurrent) dvmLockHeap(); . dvmHeapSourceGrowForUtilization(); . gcHeap->gcRunning = false; . if (spec->isConcurrent) . dvmBroadcastCond(&gDvm.gcHeapCond); if (!spec->isConcurrent) dvmResumeAllThreads(SUSPEND_FOR_GC); . . dvmEnqueueClearedReferences(&gDvm.gcHeap->clearedReferences); . 这个函数定义在文件dalvik/vm/alloc/Heap.cpp中。 前面提到,在调用函数dvmCollectGarbageInternal之前,堆是已经被锁定了的,因此这时候任何需要堆上分配对象的线程都会被挂起。注意,这不会影响到那些不需要在堆上分配对象的线程。 在图1中显示的GC流程中,除了第一步的Lock Heap和最后一步的Unlock Heap,都对应于函数dvmCollectGarbageInternal的实现,它的执行逻辑如下所示: 第1步到第3步用于并行和非并行GC: 1. 调用函数dvmSuspendAllThreads挂起所有的线程,以免它们干扰GC。 2. 调用函数dvmHeapBeginMarkStep初始化Mark Stack,并且设定好GC范围。关于Mark Stack的介绍,可以参考前面一文。 3. 调用函数dvmHeapMarkRootSet标记根集对象。 第4到第6步用于并行GC: 4. 调用函数dvmClearCardTable清理Card Table。关于Card Table的介绍,可以参考前面一文。因为接下来我们将会唤醒第1步挂起的线程。并且使用这个Card Table来记录那些在GC过程中被修改的对象。 5. 调用函数dvmUnlock解锁堆。这个是针对调用函数dvmCollectGarbageInternal执行GC前的堆锁定操作。 6. 调用函数dvmResumeAllThreads唤醒第1步挂起的线程。 第7步用于并行和非并行GC: 7. 调用函数dvmHeapScanMarkedObjects从第3步获得的根集对象开始,归递标记所有被根集对象引用的对象。 第8步到第11步用于并行GC: 8. 调用函数dvmLockHeap重新锁定堆。这个是针对前面第5步的操作。 9. 调用函数dvmSuspendAllThreads重新挂起所有的线程。这个是针对前面第6步的操作。 10. 调用函数dvmHeapReMarkRootSet更新根集对象。因为有可能在第4步到第6步的执行过程中,有线程创建了新的根集对象。 11. 调用函数dvmHeapReScanMarkedObjects归递标记那些在第4步到第6步的执行过程中被修改的对象。这些对象记录在Card Table中。 第12步到第14步用于并行和非并行GC: 12. 调用函数dvmHeapProcessReferences处理那些被软引用(Soft Reference)、弱引用(Weak Reference)和影子引用(Phantom Reference)引用的对象,以及重写了finalize方法的对象。这些对象都是需要特殊处理的。 13. 调用函数dvmHeapSweepSystemWeaks回收系统内部使用的那些被弱引用引用的对象。 14. 调用函数dvmHeapSourceSwapBitmaps交换Live Bitmap和Mark Bitmap。执行了前面的13步之后,所有还被引用的对象在Mark Bitmap中的bit都被设置为1。而Live Bitmap记录的是当前GC前还被引用着的对象。通过交换这两个Bitmap,就可以使得当前GC完成之后,使得Live Bitmap记录的是下次GC前还被引用着的对象。 第15步和第16步用于并行GC: 15. 调用函数dvmUnlock解锁堆。这个是针对前面第8步的操作。 16. 调用函数dvmResumeAllThreads唤醒第9步挂起的线程。 第17步和第18步用于并行和非并行GC: 17. 调用函数dvmHeapSweepUnmarkedObjects回收那些没有被引用的对象。没有被引用的对象就是那些在执行第14步之前,在Live Bitmap中的bit设置为1,但是在Mark Bitmap中的bit设置为0的对象。 18. 调用函数dvmHeapFinishMarkStep重置Mark Bitmap以及Mark Stack。这个是针对前面第2步的操作。 第19步用于并行GC: 19. 调用函数dvmLockHeap重新锁定堆。这个是针对前面第15步的操作。 第20步用于并行和非并行GC: 20. 调用函数dvmHeapSourceGrowForUtilization根据设置的堆目标利用率调整堆的大小。 第21步用于并行GC: 21. 调用函数dvmBroadcastCond唤醒那些等待GC执行完成再在堆上分配对象的线程。 第22步用于非并行GC: 22. 调用函数dvmResumeAllThreads唤醒第1步挂起的线程。 第23步用到并行和非并行GC: 23. 调用函数dvmEnqueueClearedReferences将那些目标对象已经被回收了的引用对象增加到相应的Java队列中去,以便应用程序可以知道哪些引用引用的对象已经被回收了。 以上就是并行和非并行GC的执行总体流程,它们的主要区别在于,前者在GC过程中,有条件地挂起和唤醒非GC线程,而后者在执行GC的过程中,一直都是挂起非GC线程的。并行GC通过有条件地挂起和唤醒非GC线程,就可以使得应用程序获得更好的响应性。但是我们也应该看到,并行GC需要多执行一次标记根集对象以及递归标记那些在GC过程被访问了的对象的操作。也就是说,并行GC在使用得应用程序获得更好的响应性的同时,也需要花费更多的CPU资源。 为了更深入地理解GC的执行过程,接下来我们再详细分析在上述的23步中涉及到的重要函数。 1. dvmSuspendAllThreads 函数dvmSuspendAllThreads用来挂起Dalvik虚拟机中除当前线程之外的其它线程,它的实现如下所示:cpp view plain copy 在CODE上查看代码片派生到我的代码片void dvmSuspendAllThreads(SuspendCause why) Thread* self = dvmThreadSelf(); Thread* thread; . lockThreadSuspend("susp-all", why); . dvmLockThreadList(self); lockThreadSuspendCount(); for (thread = gDvm.threadList; thread != NULL; thread = thread->next) if (thread = self) continue; /* debugger events don't suspend JDWP thread */ if (why = SUSPEND_FOR_DEBUG | why = SUSPEND_FOR_DEBUG_EVENT) && thread->handle = dvmJdwpGetDebugThread(gDvm.jdwpState) continue; dvmAddToSuspendCounts(thread, 1, (why = SUSPEND_FOR_DEBUG | why = SUSPEND_FOR_DEBUG_EVENT) ? 1 : 0); unlockThreadSuspendCount(); for (thread = gDvm.threadList; thread != NULL; thread = thread->next) if (thread = self) continue; /* debugger events don't suspend JDWP thread */ if (why = SUSPEND_FOR_DEBUG | why = SUSPEND_FOR_DEBUG_EVENT) && thread->handle = dvmJdwpGetDebugThread(gDvm.jdwpState) continue; /* wait for the other thread to see the pending suspend */ waitForThreadSuspend(self, thread); . dvmUnlockThreadList(); unlockThreadSuspend(); . 这个函数定义在文件dalvik/vm/Thread.cpp中。 在以下三种情况下,当前线程需要挂起其它线程: 1. 执行GC。 2. 调试器请求。 3. 发生调试事件,如断点命中。 而且,上述三种情况有可能是同时发生的。例如,一个线程在分配对象的过程中发生GC的同时,调试器也刚好请求挂起所有线程。这时候就需要保证每一个挂起操作都是顺序执行的,即要对挂起线程的操作进行加锁。这是通过调用函数函数lockThreadSuspend来实现的。 函数lockThreadSuspend尝试获取gDvm._threadSuspendLock锁。如果获取失败,就表明有其它线程也正在请求挂起Dalvik虚拟机中的线程,包括当前线程。每一个Dalvik虚拟机线程都有一个Suspend Count计数,每当它们都挂起的时候,对应的Suspend Count计数就会加1,而当被唤醒时,对应的Suspend Count计数就会减1。在获取gDvm._threadSuspendLock锁失败的情况下,当前线程按照一定的时间间隔检查自己的Suspend Count,直到自己的Suspend Count等于0,并且能成功获取gDvm._threadSuspendLock锁为止。这样就可以保证每一个挂起Dalvik虚拟机线程的请求都能够得到顺序执行。 从函数lockThreadSuspend返回之后,就表明当前线程可以执行挂起其它线程的操作了。它首先要做的第一件事情是遍历Dalvik虚拟机线程列表,并且调用函数dvmAddToSuspendCounts将列表里面的每一个线程对应的Suspend Count都增加1,但是除了当前线程之外。注意,当挂起线程的请求是调试器发出或者是由调试事件触发的时候,Dalvik虚拟机中的JDWP线程是不可以挂起的,因为JDWP线程挂起来之后,就没法调试了。在这种情况下,也不能将JDWP线程对应的Suspend Count都增加1。 通过调用函数dvmJdwpGetDebugThread可以获得JDWP线程句柄,用这个句柄和Dalvik虚拟机线程列表中的线程句柄相比,就可以知道当前遍历的线程是否是JDWP线程。同时,通过参数why可以知道当挂起线程的请求是否是由调试器发出的或者由调试事件触发的, 注意,在增加被挂起线程的Suspend Count计数之前,必须要获取gDvm.threadSuspendCountLock锁。这个锁的获取和释放可以通过函数lockThreadSuspendCount和unlockThreadSuspendCount完成。 将被挂起线程的Suspend Count计数都增加1之后,接下来就是等待被挂起线程自愿将自己挂起来了。这是通过函数waitForThreadSuspend来实现。当一个线程自愿将自己挂起来的时候,会将自己的状态设置为非运行状态(THREAD_RUNNING),这样函数waitForThreadSuspend通过不断地检查一个线程的状态是否处于非运行状态就可以知道它是否已经挂起来了。 那么,一个线程在什么情况才会自愿将自己挂起来呢?一个线程在执行的过程中,会在合适的时候检查自己的Suspend Count计数。一旦该计数值不等于0,那么它就知道有线程请求挂起自己,因此