Android内存管理源码分析

在Android中 ,实现了标注与清理(Mark and Sweep)和拷贝GC,但是具体使用什么算法是在编译期决定的,无法在运行的时候动态更换 – 至少在目前的版本上(4.2)还是这样。在Android的dalvik虚拟机源码的Android.mk文件(路径是/dalvik/vm/Dvm.mk)里,有类似代码清单14 - 5的代码,即如果在编译dalvik虚拟机的命令中指明了"WITH_COPYING_GC"选项,则编译"/dalvik/vm/alloc/Copying.cpp"源码 – 此是Android中拷贝GC算法的实现,否则编译"/dalvik/vm/alloc/HeapSource.cpp" – 其实现了标注与清理GC算法,也就是本节分析的重点。

代码清单14 - 5 编译器指定使用拷贝GC还是标注与清理GC算法

WITH_COPYING_GC := $(strip $(WITH_COPYING_GC))

ifeq ($(WITH_COPYING_GC),true)

LOCAL_CFLAGS += -DWITH_COPYING_GC

LOCAL_SRC_FILES += \

    alloc/Copying.cpp.arm

else

LOCAL_SRC_FILES += \

    alloc/DlMalloc.cpp \

    alloc/HeapSource.cpp \

    alloc/MarkSweep.cpp.arm

endif

注意本节中分析的Android源码,可以在网址:http://androidxref.com/source/xref/ 中在线浏览。

在Java中,对象是分配在Java内存堆之上的,当Java程序启动后,JVM会向操作系统申请保留一大块连续的内存。
在Android源码中,这个过程分为下面几步:

  1. dvmStartup函数(/dalvik/vm/Init.cpp:1376)解析完传入虚拟机的命令行参数,调用dvmGcStartup函数初始化GC组件。
  2. dvmGcStartup函数(/dalvik/vm/alloc/Alloc.cpp:30)负责初始化几个GC线程同步原语,再调用dvmHeapStartup函数初始化GC内存堆(即Java内存堆)。
  3. dvmHeapStartup函数(/dalvik/vm/alloc/Heap.cpp:75)则根据GC参数设置调用dvmHeapSourceStartup函数向操作系统申请一大块连续的内存空间,这个内存空间会自动增长,在默认设置中(/dalvik/vm/Init.cpp:1237),该内存堆的初始大小是2MB
    – 由gDvm.heapStartingSize指定,内存堆最大不超过16MB(Java程序用完这16MB内存就会导致OOM异常) –
    由gDvm.heapGrowthLimit指定,如果gDvm.heapGrowthLimit的值为0的话(即表示可以无限增长),则将最大值限定为gDvm.heapMaximumSize的值。申请完内存空间之后,初始化一个名为clearedReferences的队列(/dalvik/vm/alloc/Heap.cpp:98),这个队列将用在保存finalizable对象,以在另一个线程中执行它们的finalize函数。最后,dvmHeapStartup函数还要初始化数据结构Card
    Table(/dalvik/vm/alloc/Heap.cpp:100),如代码清单14 - 6。

代码清单14 - 6 dvmHeapStartup初始化GC内存堆

75 bool dvmHeapStartup()

76 {

77 GcHeap *gcHeap;

78

79 if (gDvm.heapGrowthLimit == 0) {

80 gDvm.heapGrowthLimit = gDvm.heapMaximumSize;

81 }

82

83 gcHeap = dvmHeapSourceStartup(gDvm.heapStartingSize,

84 gDvm.heapMaximumSize,

85 gDvm.heapGrowthLimit);

86 if (gcHeap == NULL) {

87 return false;

88 }

89 gcHeap->ddmHpifWhen = 0;

90 gcHeap->ddmHpsgWhen = 0;

91 gcHeap->ddmHpsgWhat = 0;

92 gcHeap->ddmNhsgWhen = 0;

93 gcHeap->ddmNhsgWhat = 0;

94 gDvm.gcHeap = gcHeap;

95

96 /* Set up the lists we'll use for cleared reference objects.

97 */

98 gcHeap->clearedReferences = NULL;

99

100 if (!dvmCardTableStartup(gDvm.heapMaximumSize, gDvm.heapGrowthLimit)) {

101 LOGE_HEAP("card table startup failed.");

102 return false;

103 }

104

105 return true;

106 }

dvmHeapSourceStartup函数(/dalvik/vm/alloc/HeapSource.cpp:541)通过dvmAllocRegion函数向操作系统申请保留一大块连续的内存地址空间,其大小是内存堆最大可能的大小(/dalvik/vm/alloc/HeapSource.cpp:563),成功后,再根据内存堆的初始大小申请内存。如默认情况下,Java内存堆的初始大小是2MB,而最大能增长到16MB,那么一开始dvmHeapSourceStartup会申请16MB大小的地址空间,但一开始只分配2MB的内存备用。在底层内存实现上,Android系统使用的是dlmalloc实现-又叫msspace,这是一个轻量级的malloc实现。

除了创建和初始化用于存储普通Java对象的内存堆,Android还创建三个额外的内存堆:用来存放堆上内存被占用情况的位图索引"livebits"、在GC时用于标注存活对象的位图索引"markbits",和用来在GC中遍历存活对象引用的标注栈(Mark Stack)。

dvmHeapSourceStartup函数运行完成后,HeapSource、Heap、livebits、markbits以及mark stack等数据结构的关系如图 14 - 15所示。

Android内存管理源码分析_第1张图片

图 14 - 15 GC堆上HeapSource、Heap等数据结构的关系

其中虚拟机通过一个名为gHs的全局HeapSource变量来操控GC内存堆,而HeapSource里通过heaps数组可以管理多个堆(Heap),以满足动态调整GC内存堆大小的要求。另外HeapSource里还维护一个名为"livebits"的位图索引,以跟踪各个堆(Heap)的内存使用情况。剩下两个数据结构"markstack"和"markbits"都是用在垃圾回收阶段,后面会讲解。

而dvmAllocRegion函数(/dalvik/vm/Misc.cpp:612)则通过ashmem和mmap两个系统调用分配内存地址空间,其中ashmem是Android系统对Linux的一个扩展,而mmap则是Linux系统提供的系统调用,请读者自行搜索参阅相关文档了解其用法。
这些步骤做完之后,一个Android应用的内存情况如图 14 - 16所示,虚线是应用实际申请的地址空间范围,而实线部分则是已经分配的内存:

Android内存管理源码分析_第2张图片
图 14 - 16 GC向操作系统申请地址空间和内存

当需要应用需要分配内存,即通过"new"关键字创建一个实例时,在Android源码的过程大致如下:
首先虚拟机在执行Java class文件时,遇到"new “或” newarray"指令(所有的Java字节指令码请参考维基百科:http://en.wikipedia.org/wiki/Java_bytecode_instruction_listings),表示要创建一个对象或者数组的实例,这里为了简单起见,我们只看新建一个对象实例的情形。
虚拟机的JIT编译器执行"new"指令,针对不同的CPU架构,"new"指令都有相应的机器码与其对应,如ARM架构,JIT执行/dalvik/vm/mterp/armv5te/OP_NEW_INSTANCE.S中的机器码;而x86架构,则是/dalvik/vm/mterp/x86/OP_NEW_INSTANCE.S中的机器码。"OP_NEW_INSTANCE"函数的工作就是加载"new"指令的对象类型参数,获取对象需要占用的内存大小信息,然后调用"dvmAllocObject"分配必要的内存(/dalvik/vm/mterp/armv5te/OP_NEW_INSTANCE.S:29),当然还会处理必要的异常。
dvmAllocObject函数(/dalvik/vm/alloc/Alloc.cpp:181)调用dvmMalloc根据对象大小分配内存空间,成功后,调用对象的构造函数初始化实例(/dalvik/vm/alloc/Alloc.cpp:191)。
程序在运行的过程中不停的创建新的对象并消耗内存,直到GC内存用光,这时再要创建新对象时,就会触发GC线程启动垃圾回收过程,在Android源码中:
dvmMalloc函数(/dalvik/vm/alloc/Heap.cpp:333)直接将分配内存的工作委托给函数tryMalloc。
tryMalloc函数(/dalvik/vm/alloc/Heap.cpp:178)首先尝试用dvmHeapSourceAlloc函数分配内存,如果失败的话,唤醒或创建GC线程执行垃圾回收过程,并等待其完成后重试dvmHeapSourceAlloc(/dalvik/vm/alloc/Heap.cpp:201);如果dvmHeapSourceAlloc再次失败,说明当前GC堆中大部分对象都是存活的,那么调用dvmHeapSourceAllocAndGrow(/dalvik/vm/alloc/Heap.cpp:222)尝试扩大GC内存堆 – 前面说过,一开始GC堆会根据初始大小向操作系统申请保留一块内存,如果这块内存用完了,GC堆就会再次向操作系统申请一块内存,直到用完限额。
dvmMalloc函数根据内存分配是否成功来执行相应的操作,如内存分配失败时,抛出OOM(Out Of Memory)异常(/dalvik/vm/alloc/Heap.cpp:383)。
Android源码中垃圾回收过程大致如下:
dvmCollectGarbageInternal函数(/dalvik/vm/alloc/Heap.cpp:440)开始垃圾回收过程,其首先调用dvmSuspendAllThreads(/dalvik/vm/Thread.cpp:2539)暂停系统中除与调试器沟通的其他所有线程(/dalvik/vm/alloc/Heap.cpp:462);
如果没有启用并行GC的话,虚拟机会提高GC线程的优先级,以防止GC线程被其它线程占用CPU。
接下来调用dvmHeapMarkRootSet函数(/dalvik/vm/alloc/Heap.cpp:488)来遍历所有可从GC Root访问到的对象列表,dvmHeapMarkRootSet函数(/dalvik/vm/alloc/MarkSweep.cpp:181)的注释中也列出了GC Root列表。其调用dvmVisitRoot遍历GC Roots,代码清单14 - 1是dvmVisitRoot的源码(/dalvik/vm/alloc/Visit.cpp:212),笔者在其中以注释的方式批注关键代码。完整的GC Root列表有兴趣的读者可以参阅链接:http://help.eclipse.org/indigo/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fconcepts%2Fgcroots.html。
代码清单14 - 7 在虚拟机中通过dvmVisitRoot遍历GC Roots

// visitor是一个回调函数,dvmHeapMarkRootSet传进来的是rootMarkObjectVisitors

// (位于/dalvik/vm/alloc/MarkSweep.cpp:145),这个回调函数的作用就是标注(Mark)

// 所有的GC root,并将它们的指针压入标注栈(Mark Stack)中。

// 第二个参数arg实际上是GcMarkContext对象,用于找到GC Roots后,回传给回调函数visitor

// 的参数。
 
void dvmVisitRoots(RootVisitor *visitor, void *arg)

{

assert(visitor != NULL);

// 所有已加载的类型都是GC Roots,这也意味着类型中所有的静态变量都是GC Roots

visitHashTable(visitor, gDvm.loadedClasses, ROOT_STICKY_CLASS, arg);
         
// 基本类型也是GC Roots,包括

// void, boolean, byte, short, char, int, long, float, double

visitPrimitiveTypes(visitor, arg);         

// 调试器对象注册表中的对象(debugger object registry),这些对象

// 基本上是调试器创建的,因此不能把它们当作垃圾回收了,否则调试器

// 就无法正常工作了。

if (gDvm.dbgRegistry != NULL) {

visitHashTable(visitor, gDvm.dbgRegistry, ROOT_DEBUGGER, arg);

}
       
// 所有interned的字符串,interned string是虚拟机中保证的只有唯一一份拷贝的字符串

if (gDvm.literalStrings != NULL) {

visitHashTable(visitor, gDvm.literalStrings, ROOT_INTERNED_STRING, arg);

}
        

// 所有的JNI全局引用对象(JNI global references),JNI全局引用对象是

// JNI代码中,通过NewGlobalRef函数创建的对象

dvmLockMutex(&gDvm.jniGlobalRefLock);

visitIndirectRefTable(visitor, &gDvm.jniGlobalRefTable,, ROOT_JNI_GLOBAL, arg);

dvmUnlockMutex(&gDvm.jniGlobalRefLock);     

// 所有的JNI局部引用对象(JNI local references)

// 关于JNI局部和全部变量的使用,可以参考下面的网页链接:

// http://journals.ecs.soton.ac.uk/java/tutorial/native1.1/implementing/refs.html

dvmLockMutex(&gDvm.jniPinRefLock);

visitReferenceTable(visitor, &gDvm.jniPinRefTable,, ROOT_VM_INTERNAL, arg);

dvmUnlockMutex(&gDvm.jniPinRefLock);
         
// 所有线程堆栈上的局部变量和其它对象,如线程本地存储里的对象等等

visitThreads(visitor, arg);         

// 特殊的异常对象,如OOM异常对象需要在内存不够的时候创建,为了防止内存不够而无法创建

// OOM对象,因此虚拟机会在启动时事先创建这些对象。

(*visitor)(&gDvm.outOfMemoryObj,, ROOT_VM_INTERNAL, arg);

(*visitor)(&gDvm.internalErrorObj,, ROOT_VM_INTERNAL, arg);

(*visitor)(&gDvm.noClassDefFoundErrorObj,, ROOT_VM_INTERNAL, arg);

}

dvmHeapMarkRootSet是执行标注过程的主要代码,在前文说过,通常的实现会在对象实例前面放置一个对象头,里面会存放是否标注过的标志,而在Android系统里,采取的是分离式策略,而是将标注用的标志位放到HeapSource里的"markbits"这个位图索引结构,笔者猜测这么做的目的是为了节省内存。图 14 - 17是dvmHeapMarkRootSet函数快要标注完存活对象时(正在标注最后一个对象H),GC内存堆的数据结构。

Android内存管理源码分析_第3张图片

图 14 - 17 GC执行完标注过程后的HeapSource结构

其中"livebits"位图索引还是维护堆上已用的内存信息;而"markbits"这个位图索引则指向存活的对象,在图 14 - 17中, A、C、F、G、H对象需要保留,因此"markbits"分别指向他们(最后的H对象尚在标注过程中,因此没有指针指向它);而"markstack"就是在标注过程中跟踪当前需要处理的对象要用到的标志栈了,此时其保存了正在处理的对象F、G和H。

在标注(Mark)过程中,调用dvmHeapScanMarkedObjects和dvmHeapProcessReferences函数(/dalvik/vm/alloc/MarkSweep.cpp:776)将实现了finalizer的对象添加到finalizer对象队列中,以便在下次GC中执行这些对象的finalize函数。
标识出所有的垃圾内存之后,调用dvmHeapSweepSystemWeaks和dvmHeapSweepUnmarkedObjects(/dalvik/vm/alloc/MarkSweep.cpp:902)等函数清理内存,但并不压缩内存,这是因为Android的GC是基于dlmalloc之上实现的,GC将所有的内存分配和释放的操作都转交给dlmalloc来处理。在这个过程中, Android系统不做压缩内存处理,据说是为了节省执行的CPU指令,从而达到延长电池寿命的目的,因此dvmCollectGarbageInternal做了一个小技巧,调用dvmHeapSourceSwapBitmaps函数(/dalvik/vm/alloc/Heap.cpp:575)将"livebits"和"markbits"的指针互换,这样就不需要在清理完垃圾对象后再次维护"livebits"位图索引了,如图 14 - 18所示:

Android内存管理源码分析_第4张图片
图 14 - 18 GC清理完内存后堆上的数据结构

做完上面的操作之后,GC线程再通过dvmResumeAllThreads函数唤醒所有的线程(/dalvik/vm/alloc/Heap.cpp:624)。
虽然GC可以自动回收不再使用的内存,但有很多资源是虚拟机也无法管理的,如进程打开的数据库连接、网络端口以及文件等。针对这些资源,GC线程可以在垃圾回收过程中,标示出其是垃圾,需要释放,但是却不清楚如何释放它们,因此Java对象提供了一个名为finalize的函数,以便对象实现自定义的清除资源的逻辑。
如代码清单14 - 1是一个实现finalize函数的对象,在Java中,finalize对象定义在System.Object类中,即意味着所有对象都有这个函数,当子类重载了这个函数,即向虚拟机表明自己需要与其他类型区别对待。

代码清单14 - 8 实现finalize函数的简单对象

1    class DemoClass {

2     public int X;

3    

4     public void testMethod() {

5         System.out.println("X: " + new Integer(X).toString());

6     }

7    

8     @Override

9     protected void finalize () throws Throwable {

10         System.out.println("finalize函数被调用了!");

11         // 实现自定义的资源清除逻辑!

12         super.finalize();

13     }

14    }

一些有C++编程经验的读者可能很容易将finalize函数与析构函数对应起来,但是两者是完全不同的东西,在C++中,调用了析构函数之后,对象就被释放了,然而在Java中,如果一个类型实现了finalize函数,其会带来一些不利影响,首先对象的存活周期会更长,至少需要两次垃圾回收才能销毁对象;第二对象同时会延长其所引用到的对象存活周期。如代码清单14 - 2中(示例代码javagc-simple)在第3行创建并使用了DemoClass以在内存中生成一些垃圾,并执行三次GC。

代码清单14 - 9 实现finalize函数的简单对象

1    public class gcdemo {

2     public static void main(String[] args) throws Exception {

3         generateGarbage();

4         System.gc();

5         Thread.sleep(1000);

6    

7         System.gc();

8         Thread.sleep(1000);

9    

10         System.gc();

11         Thread.sleep(1000);

12     }

13    

14     public static void generateGarbage() {

15         DemoClass g = new DemoClass();

16         g.X =123;

17         g.testMethod();

18     }

19    }

连接好设备,打开logcat日志,并执行示例代码根目录中的run.sh,得到的输出类似图 14 - 8,每一行输出对应代码清单14 - 2中的一次System.gc调用,可以看到第一次GC过程中释放了223个对象,如果运行示例程序javagc,会发现第一次GC之后,DemoClass的finalize函数就会被调用 – 为了避免System.out.println中的字符串对象影响GC的输出,图 14 - 8是javagc-simple的输出结果。第二次GC过程中又释放了34个对象,其中就有DemoClass的实例,以及其所引用到的其它对象。这时所有垃圾对象都被回收了,因此在执行第三次GC过程时,没有回收到任何内存。

在这里插入图片描述
图 14 - 19 程序中使用了实现finalize函数对象之后实施三次GC的结果

前文讲到Android源码中通过dvmHeapScanMarkedObjects函数在GC堆上扫描垃圾对象,并将finalizable对象添加到finalize队列中,其具体过程如下:

  1. dvmHeapScanMarkedObjects函数(/dalvik/vm/alloc/MarkSweep.cpp:595)将所有识别出来的可以被GC
    Root引用的对象放到名为"mark stack"的堆栈中,再调用processMarkStack函数处理需要特殊处理的对象。

  2. processMarkStack函数(/dalvik/vm/alloc/MarkSweep.cpp:471)调用scanObject函数处理"mark
    stack"中的每个对象。

  3. scanObject函数(/dalvik/vm/alloc/MarkSweep.cpp:454)首先判断对象是保存Java类型信息的类型对象,还是数组对象,还是普通的Java对象,针对这三种对象进行不同的处理。由于finalize对象是普通的Java对象,因此这里我们只看相应的scanDataObject函数。

  4. scanDataObject函数(/dalvik/vm/alloc/MarkSweep.cpp:438)先扫描对象的各个成员,并标记其所有引用到的对象,最后调

  5. 用delayReferenceReferent函数根据对象的类型,将其放入相应的待释放队列中,如对象是fianlizeable对象的话,则放入finalizerReferences队列中(/dalvik/vm/alloc/MarkSweep.cpp:426);如对象是WeakReference对象的话,则将其放入weakReferences队列中(/dalvik/vm/alloc/MarkSweep.cpp:424)。

  6. dvmHeapProcessReferences函数(/dalvik/vm/alloc/MarkSweep.cpp#776)在垃圾对象收集完毕后,负责将finalize队列从虚拟机的native端传递到Java端。其调用enqueueFinalizerReferences函数通过JNI方式将finalize对象的引用传递到Java端的一个java.lang.ref.ReferenceQueue当中,详细的调用方式请参见enqueueFinalizerReferences函数(/dalvik/vm/alloc/MarkSweep.cpp:729)和enqueueReference函数(/dalvik/vm/alloc/MarkSweep.cpp:653)。

  7. 而在JVM虚拟机启动时,dvmStartup函数(/dalvik/vm/Init.cpp:1557)会在准备好Java程序运行所需的所有环境之后,调用dvmGcStartupClasses函数(/dalvik/vm/alloca/Alloc.cpp:71)启动几个与GC相关的后台Java线程
    ,这些线程在java.lang.Daemons中定义(/libcore/luni/src/main/java/java/lang/Daemons.java),其中一个线程就是执行java对象finalize函数的HeapWorker线程,之所以要将收集到的java
    finalize对象引用从虚拟机(native)一端传递到Java端,是因为finalize函数是由java语言编写的,函数里可能会用到很多java对象。这也是为什么如果对象实现了finalize函数,不仅会使其生命周期至少延长一个GC过程,而且也会延长其所引用到的对象的生命周期,从而给内存造成了不必要的压力。

感谢文章:http://www.cnblogs.com/killmyday/archive/2013/06/12/3132518.html的作者:donjuan

你可能感兴趣的:(Android)