android Dalvik虚拟机

转载自:https://blog.csdn.net/luoshengyang/article/details/41338251

简介

在android5.0中,ART运行时取代了Dalvik虚拟机。虽然Dalvik虚拟机不再使用,但是它曾经的作用是不可磨灭的。因此,在研究ART运行时的垃圾收集机制之前,先理解Dalvik虚拟机的垃圾收集机制也是很重要和有帮助的。因此,本文就对Dalvik虚拟机的垃圾收集机制进行简单介绍和指定学习计划。
这里写图片描述
Dalvik虚拟机用来分配对象的堆分为两部分,一部分叫Active Heap,另一部分叫做Zygote Heap。android系统的第一个Dalvik虚拟机是由Zygote进程创建的。应用进程是由Zygote进程fork出来的。也就是说,应用程序进程使用了一种写时拷贝技术来复制了Zygote进程的地址空间。这意味着一开始的时候,应用程序进程和Zygote进程共享了同一个用来分配对象的堆。然而,当Zygote进程或者应用程序进程对该堆进行写操作时,内核就会执行真正的拷贝操作,使得Zygote进程和应用程序进程分别拥有自己的一份拷贝。

拷贝是一件费时费力的事情。因此,为了尽量地避免拷贝,Dalvik虚拟机将自己的堆划分为两部分。事实上,Dalvik虚拟机的堆最初是只有一个的。也就是Zygote进程在启动过程中创建Dalvik虚拟机的时候,只有一个堆。但是当Zygote进程在fork第一个应用程序进程之前,会将已经使用了的那部分堆内存划分为一部分,还没有使用的堆内存划分为另外一部分。前者就称为Zygote堆,后者就称为Active堆。以后无论是Zygote进程,还是应用程序进程,当它们需要分配对象的时候,都在Active堆上进行。这样就可以使得Zygote堆尽可能少地被执行写操作,因而就可以减少执行写时拷贝的操作。在Zygote堆里面分配的对象其实主要就是Zygote进程在启动过程中预加载的类、资源和对象了。这意味着这些预加载的类、资源和对象可以在Zygote进程和应用程序进程中做到长期共享。这样既能减少拷贝操作,还能减少对内存的需求。

Dalvik虚拟机除了要给应用层分配对象之外,最重要的还是要对这些已经分配出去的对象进行管理,也就是要在对象不再被使用的时候,对其进行自动回收。自动回收对象的算法耳熟能详的Mark-Sweep算法。

Mark-Sweep算法主要分为两个阶段:Mark和Sweep。Mark阶段从对象的根集开始标记被引用的对象。标记完成后,就进入到Sweep阶段,而Sweep阶段所作的事情就是回收没有被标记的对象占用的内存。那我们怎么标记对象有没有被引用的呢?换句话说就是通过什么数据结构来描述对象有没有被引用。我们使用Heap Bitmap。Heap Bitmap的结构如图3所示:
这里写图片描述
从名字可以推断出,Heap Bitmap使用位图来标记对象是否被使用。如果一个对象被引用,那么在Bitmap中与它对应的那一位就会被设置为1。否则的话,就设置为0。
在图一中,我们使用了两个Bitmap来描述堆的对象,一个称为Live Bitmap,另一个称为Mark Bitmap。Live Bitmap用来标记上一次GC时被引用的对象,也就是没有被回收的对象,而Mark Bitmap用来标记当前GC有被引用的对象。有了这两个消息之后,我们就可以很容易地知道哪些对象是需要被回收的,即在Live Bitmap在标记位1,但是在Mark Bitmap中标记为0的对象。
在垃圾收集的Mark阶段,要求除了垃圾收集线程之外,其他的线程都停止,否则的话,就会可能导致不能正确的标记每一个对象。这种现象在垃圾收集算法中称为Stop the world,会导致程序中止执行,造成停顿的现象。为了尽可能地减少停顿,我们必须要允许在Mark阶段有条件的允许程序的其他线程执行。这种垃圾收集算法称为并行垃圾收集算法。
为了实现Concurrent GC,Mark阶段又划分为两个子阶段。第一个子阶段只负责标记根集对象。所谓的根集对象,就是指GC开始的瞬间,被全局变量、栈变量和寄存器等引用的对象。有了这些根集对象之后,我们就可以顺着它们找到其余的被引用变量。这个标记被根集对象引用的对象的过程就是第二个子阶段。在Concurrent GC,第一个子阶段是不允许垃圾收集线程之外的线程运行的,但是第二个子阶段是允许的。不过,在第二个子阶段执行的过程中,如果一个线程修改了对象,那么该对象必须要记录下来,因为他很有可能引用了新的对象。如果不这样的话,可能会导致被引用的对象还在使用然而却被回收。这种情况出现在只进行部分垃圾收集的情况,这时候Card Table的作用就是用来记录非垃圾收集堆对象对垃圾收集堆对象的引用。Dalvik虚拟机进行部分垃圾收集时,实际上就是只收集在Active堆上分配的对象。因此对Dalvik虚拟机来说,Card Table就是用来记录在Zygote堆上分配的对象对在Active堆上分配的对象的引用。
我们是不是想到再用一个Bitmap在描述上述第二个子阶段被修改的对象呢?虽然我们尽大努力减少了用来标记对象的Bitmap的大小,不过还是比较乐观的。因此,为了减少内存的消耗,我们使用另外一种技术来标记第二子阶段被修改的对象。这种技术使用到了一种称为Card Table的数据结构,如下图:
这里写图片描述

Card Table是由Card组成,一个Card实际上就是一个字节,它的值要么是clean,要么是dirty。如果一个Card的值是Clean,就表示与它对应的对象在mark第二子阶段没有被程序修改过。否则,就意味着被程序修改过。对于这些被修改过的对象,需要在Mark第二子阶段结束之后,再次禁止垃圾收集线程之外的其他线程执行,以便垃圾收集线程再次根据Card Table记录的信息对被修改过的对象引用的其他对象进行重新标记。由于Mark 第二子阶段执行的时间不会太长,因此在该阶段被修改的对象不会很多,这样就可以保证第二次子阶段结束后,再次执行标记对象的过程是很快的,因而此时对程序造成的停顿非常小。

GC触发时机

1、调用函数dvmHeapSourceAlloc在java堆上分配指定大小的内存。如果分配成功,那么就将分配得到的地址直接返回给调用者了。函数dvmHeapSourceAlloc在不改变java堆当前大小前提下进行内存分配。函数dvmHeapSourceAlloc成功地在Active堆上分配到一个对象之后,就会坚持Active堆当前已经分配的内存是否大于预设的阈值。如果大于,那么就会通过条件变量gHs->gcThreadCond唤醒GC线程进行垃圾回收。预设的阈值是一个比指定的堆最小空闲内存小128K的数值,各手机厂商也会修改该值到一个合理的值。
2、如果上一步内存分配失败,这时候就需要一次GC。GC线程时Dalvik虚拟机启动的过程中创建的,它的执行体函数是gcDaemonThread。不过如果GC线程已经在运行中,即gDvm.gcHeap->gcRunning的值等于true,那么就直接调用函数dvmWaitForConcurrentGcToComplete等到GC执行完成。否则的话,就需要调用函数gcForMalloc来执行一次GC_FOR_MALLOC的GC了,参数false表示不要回收软引用对象引用的对象。
3、GC执行完毕之后,再次调用函数dvmHeapSourceAlloc尝试内存分配操作。如果分配成功,那么就将分配得到的地址直接返回给调用者了。
4、如果上一步内存分配失败,这个时候就要对Java堆进行扩容了。通过调用函数dvmHeapSourceAllocAndGrow尝试分配,这个函数会扩张堆。所以dvmHeapStartup的时候可以给一个比较小的初始堆,实在不够用再调用它进行扩张。
5、如果调用函数dvmHeapAourceAllocAndGrow分配内存成功,则直接将分配得到的地址直接返回给调用者了。
6、如果上一步内存分配还是失败,这个时候就需要回收软引用了。再次调用函数gcForMalloc来执行GC。参数true表示要回收软引用对象引用的对象。
7、GC执行完毕,再次调用函数dvmHeapSourceAllocAndGrow进行内存分配。这是最后一次努力了,如果失败,就抛出OOM。

通过这个流程可以看出,在对象的分配中会导致GC,第一次分配对象失败我们会出发GC但是不回收软引用,如果再次分配还是失败就会对堆进行扩容(在堆的大小还没有达到最大值Maximum Size的情况下),并再次尝试分配,如果还是分配失败,就会将软引用内存也给回收。
每次GC执行完成之后,都需要根据预先设置的目标堆利用率和已经分配出去的内存字节数计算得到理想的堆大小。已经分配出去的内存字节数只考虑在Active堆上分配出去的字节数。得到了Active堆已经分配出去的字节数currentHeapUsed之后,就可以调用函数getUtilizationTarget来计算Active堆的理想大小targetHeapSize了。计算出来的堆理想大小targetSize要满足空闲内存不能大于预先设定的最大值(hs->maxFree)以及不能小于预先设定的最小值(hs->minFree)。接下来同函数dvmHeapSourceAlloc成功地在Active堆上分配到一个对象一样,检查并行GC的触发条件。当Active堆heap当前已经分配的大小超过heap->concurrentStartBytes时,就会触发并行GC。计算并行GC触发条件时,需要用到CONCURRENT_MIN_FREE和CONCURRENT_START两个值,预设的CONCURRENT_MIN_FREE定义为256K,而CONCURRENT_START定义为128K。各厂商会根据手机设置合理的值如几百K。
由此我们知道,在预设情况下,当Active堆允许分配的内存小于256K时,禁止执行并行GC,而当Active堆允许分配的内存大于等于256K,并且剩余的空闲内存小于128K就会触发并行GC。

回收算法和内存碎片

由于Mark and Sweep算法的缺点,容易导致内存碎片,所以在这个算法下,当我们有大量不连续小内存的时候,再分配一个较大的对象时,还是会非常容易导致GC,比如我们分配一个图片内存的时候。所以对于Dalvik虚拟机的手机来说,我们首先要尽量避免频繁生成很多临时小变量,另一个又要尽量去避免产生很多长声明周期的大对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值