垃圾回收技术已经出现很久了,可以追溯到20世纪60年代,在LISP语言中就开始进行应用,而后的Smalltalk,java,c#等语言更是一步一步地将其推向新的高潮。它广受技术专家的推崇,并被高度的评价,被认为是提高软件质量和生产力的一个有效的银弹,是一个具有革命性技术。由于计算机的内存资源总是有限的,为了不同的程序运行,必须把不需要使用的内存回收,以便重新使用。假如那一天计算机的内存足够大,可以一年内创建的对象,所占用的内存都卓卓有余时,就没有必要使用这种回收技术了。在C++/C的世界里,没有回收技术,其实就是需要开发人员自己负责把它使用完成的内存,主动去删除它。在Dalvik虚拟机实现里,虽然它跟java虚拟机有本质上的区别,但在内存回收这一块,是没有区别的,可能使用垃圾回收的技术,就是一样的。因此,需要先学习计算领域里典型的垃圾回收算法。
在垃圾回收技术里,经典的算法主要有以下三种:引用计数、MarkSweep算法、SemiSpaceCopy算法。其它算法或者混合以上三种法来使用,根据不同的场合来选择不同的算法。
一、引用计数
这种技术非常简单,就是使用一个变量记录这块内存或者对象的使用次数。比如在COM技术里,就是使用引用计数来确认这个COM对象什么时候删除的。当一个COM对象给不同线程来使用时,由于不同的线程生命周期不一样,因此,没有办法知道这个COM对象到底在那个线程删除,只能使用引用计数来删除,否则还需要不同线程之间添加同步机制,这样是非常麻烦和复杂的,如果COM对象有很多,就变成基本上不能实现了。引用计数的优点是:在对象变成垃圾时,可以马上进行回收,回收效率和成本都是最低。因此,内存使用率最高,基本上没有时间花费,不需要把所有访问COM对象线程都停下来。缺点是:引用计数会影响执行效率,每引用一次都需要更新引用计数,对于COM对象那是人工控制的,因此次数很少,没有什么影响。但在Java里是由编译程序来控制的,因此引用次数非常多。另外一个问题就是引用计数不能解决交叉引用,或者环形引用的问题。比如在一个环形链表里,每一个元素都引用前面的元素,这样首尾相连的链表,当所有元素都变成不需要时,就没有办法识别出来,并进行内存回收。
二、Mark Sweep算法
标记-清除算法依赖于对所有存活对象进行一次全局遍历来确定哪此对象可以回收,遍历的过程从根出发,找到所有可到达对象,其它不可到达的对象就是垃圾对象,可被回收。正如其名称所暗示的那样,这个算法分为两大阶段:标记和清除。这种分步执行的思路构成了现代垃圾收集算法的思想基础。与引用计数算法不同的是,标记-清除算法不需要监测每一次内存分配和指针操作,只需要在标记阶段进行一次统计就行了。标记-清除算法可以非常自然的处理环形问题,另外在创建对象和销毁对象时少了操作引用计数值的开销。不过,标记-清除算法也有一个缺点,就是需要标记和清除阶段中把所有对象停止执行。在垃圾回收器运行过程中,应用程序必须暂时停止,并等到垃圾回收器全部运行完成后,才能重新启动应用程序运行。
下面就来先看看Dalvik虚拟机整个标记和清除中使用到那些函数,在文件alloc/MarkSweep.h里有函数如下:
1)调用函数dvmHeapBeginMarkStep来创建位图,并从对象位图里拷贝一份位图出来,以便后面对这个位图进行标记。
2)调用函数dvmHeapMarkRootSet对所有根对象进行标记。
3)调用函数dvmHeapScanMarkedObjects根据上一个函数给出的根对象位图,对每一个根相关的位图进行计算,如果这个根对象有被引用,就标记为使用。这个过程是递归调用的过程,从根开始不断重复地对子树进行标记的过程。
4)调用函数dvmHeapHandleReferences对JAVA类对象的引用类型进行处理。主要处理三个直接的了类:SoftReference,WeakReference,PhantomReference。SoftReference对象封装了对引用目标的“软引用”;WeakReference封装了对引用目标的“弱引用”;而PhantomReference封装了对引用目标的“影子引用。强引用禁止引用目标被垃圾收集,而软引用、弱引用和影子引用不禁止。
5)调用函数dvmHeapScheduleFinalizations对未曾标记的对象进行完成调用,让每一个对象最后删除动作可以运行,以便后面从内存里把对象删除,相当于对象的析构作用。
6)调用函数dvmHeapSweepUnmarkedObjects对未曾标记的对象进行清除操作,也就是删除没有再使用的对象。
7)调用函数dvmHeapFinishMarkStep对已经删除的对象进行内存回收,可以调用堆管理函数改变目前堆使用的内存,并整理内存,就可以得到更多空闲的内存了。
这个过程,就是Dalvik虚拟机的整个标记和删除的算法过程,实际的代码会相当复杂,算法上是很清楚的,就是细节、时间方面要求相当严格,否则乱删除还在使用的对象,就导致整个虚拟机运行出错。
-
通过上面的学习,了解了垃圾回收的原理和过程。那么Dalvik虚拟机是什么时候进行垃圾回收呢?要回答这个问题,那么得继续分析代码,继续进入下面的学习。其实,垃圾回收主要有两种方式,一种是虚拟机线程自动进行的,一种是手动进行的。现在先来学习自动进行的方式,所谓自动方式,就是虚拟机创建一个线程,这个线程定时进行。虚拟机在初始化时,就进行创建这个线程,如下的代码:
if(gDvm.zygote){
if(!dvmInitZygote())
gotofail;
}else{
if(!dvmInitAfterZygote())
gotofail;
}
在上面这段代码里调用函数dvmInitAfterZygote,在这个函数里就会调用函数dvmSignalCatcherStartup来创建垃圾回收线程,这个函数的代码如下:
booldvmSignalCatcherStartup(void)
{
gDvm.haltSignalCatcher=false;
if(!dvmCreateInternalThread(&gDvm.signalCatcherHandle,
"SignalCatcher", signalCatcherThreadStart,NULL))
returnfalse;
returntrue;
}
通过上面的这段代码,就可以看到线程运行函数是signalCatcherThreadStart,在这个函数里就会调用函数dvmCollectGarbage来进行垃圾回收。代码如下:
voiddvmCollectGarbage(bool collectSoftReferences)
{
dvmLockHeap();
LOGVV("ExplicitGC\n");
dvmCollectGarbageInternal(collectSoftReferences);
dvmUnlockHeap();
}
在这个函数主要通过锁来锁住多线程访问的堆空间相关对象,然后直接就调用函数dvmCollectGarbageInternal来进行垃圾回收过程了,也就调用上面标记删除算法的函数。
另一种方式通过调用运行库的GC来回收,如下:
/*
* public voidgc()
*
* Initiate agc.
*/
staticvoidDalvik_java_lang_Runtime_gc(constu4* args,JValue*pResult)
{
UNUSED_PARAMETER(args);
dvmCollectGarbage(false);
RETURN_VOID();
}
在这里也是调用函数dvmCollectGarbage来进行垃圾回收。手动的方式适合当需要内存,但线程又没有调用时进行。