http://blog.csdn.net/caimouse/article/details/6940419
垃圾回收技术已经出现很久了,可以追溯到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 void gc()
*
* Initiate a gc.
*/
staticvoidDalvik_java_lang_Runtime_gc(constu4* args,JValue*pResult)
{
UNUSED_PARAMETER(args);
dvmCollectGarbage(false);
RETURN_VOID();
}
在这里也是调用函数dvmCollectGarbage来进行垃圾回收。手动的方式适合当需要内存,但线程又没有调用时进行。
现在开始学习虚拟机的初始化过程,先从dvmStartup函数开始,这个函数实现所有开始虚拟机的准备工作。
dvmAllocTrackerStartup函数初始化跟踪显示系统,跟踪系统主要用生成调试系统的数据包。
dvmGcStartup函数是用来初始化垃圾回收器。
dvmThreadStartup函数是初始化线程列表和主线程环境参数。
dvmInlineNativeStartup函数是分配内部操作方法的表格内存。
dvmVerificationStartup函数是初始化虚拟机的指令码相关的内容,以便检查指令是否正确。
dvmRegisterMapStartup函数是分配指令寄存器状态的内存。
dvmInstanceofStartup函数是分配虚拟机使用的缓存。
dvmClassStartup函数是初始化虚拟机最基本用的JAVA库。
dvmThreadObjStartup函数是初始化虚拟机进一步使用的JAVA类库线程类。
dvmExceptionStartup函数是初始化虚拟机使用的异常JAVA类库。
dvmStringInternStartup函数是初始化虚拟机解释器使用的字符串哈希表。
dvmNativeStartup函数是初始化本地方法库的表。
dvmInternalNativeStartup函数是初始化内部本地方法,建立哈希表,方便快速查找到。
dvmJniStartup函数是初始化JNI调用表,以便快速找到本地方法调用的入口。
dvmReflectStartup函数是缓存JAVA类库里的反射类。
接着把下面这些类先进行初始化,如下:
staticconst char*earlyClasses[] = {
"Ljava/lang/InternalError;",
"Ljava/lang/StackOverflowError;",
"Ljava/lang/UnsatisfiedLinkError;",
"Ljava/lang/NoClassDefFoundError;",
NULL
};
初始化这些类,就是调用函数dvmFindSystemClassNoInit来初始化。
接着调用dvmValidateBoxClasses函数来初始化JAVA基本类型库,如下:
staticconstchar*classes[] = {
"Ljava/lang/Boolean;",
"Ljava/lang/Character;",
"Ljava/lang/Float;",
"Ljava/lang/Double;",
"Ljava/lang/Byte;",
"Ljava/lang/Short;",
"Ljava/lang/Integer;",
"Ljava/lang/Long;",
NULL
};
这些类调用函数,不是上面使用系统函数来初始化,而是调用dvmFindClassNoInit来初始化。
调用dvmPrepMainForJni函数准备主线程里的解释栈可以调用JNI的方法;调用registerSystemNatives来注册JAVA库里的JNI方法;调用dvmCreateStockExceptions函数分配异常出错的内存;调用dvmPrepMainThread函数完成解释器主线程的初始化;调用dvmDebuggerStartup函数进行调试器的初始化;
最后调用dvmInitZygote或者dvmInitAfterZygote来初始化线程的模式,调用dvmCheckException函数检查是否有异常情况出现。
到这里就把整个虚拟机初始化流程完成。
在Dalvik虚拟机里,提供了一些JNI的调用测试函数,以便确认JNI的机制是否可以运行,JNI调用效率是否达到设计的目标,它是通过在registerSystemNatives函数初始化,然后调用jniRegisterSystemMethods函数来设置JNI函数。
JNI的测试函数代码如下:
/*
* JNI registration
*/
staticJNINativeMethodgMethods[] = {
/*name, signature, funcPtr */
{ "emptyJniStaticMethod0", "()V", emptyJniStaticMethod0 },
{ "emptyJniStaticMethod6", "(IIIIII)V",emptyJniStaticMethod6 },
{ "emptyJniStaticMethod6L",
"(Ljava/lang/String;[Ljava/lang/String;[[I"
"Ljava/lang/Object;[Ljava/lang/Object;[[[[Ljava/lang/Object;)V",
emptyJniStaticMethod6L },
};
这段代码是提供JNI测试函数的接口和相关实现的C函数入口。
intregister_org_apache_harmony_dalvik_NativeTestTarget(JNIEnv*env)
{
intresult = jniRegisterNativeMethods(env,
"org/apache/harmony/dalvik/NativeTestTarget",
gMethods,NELEM(gMethods));
这段代码把JNI调用接口设置到包org.apache.harmony.dalvik.NativeTestTarget下面,这样在Java的应用程序里就可以调用了。
if(result != 0) {
/*print warning, but allow to continue */
LOGW("WARNING:NativeTestTarget not registered\n");
(*env)->ExceptionClear(env);
}
return0;
}
/*
* public static voidemptyJniStaticMethod0()
*
* For benchmarks, a do-nothingJNI method with no arguments.
*/
staticvoidemptyJniStaticMethod0(JNIEnv*env, jclassclazz)
{
//This space intentionally left blank.
}
可见这些系统函数的代码,都空的结构,没有真实的代码运行,就可以用来测试JNI是否可以工作,测试JNI调用的时间需要多少,可以提供准确的时间。
联系客服