1. DVM启动流程
android开机后,加载init.rc文件,会启动app_process进程,在对应app_main.cpp的main()中,调用AndroidRuntime.cpp
base/cmds/app_process/app_main.cpp
1 |
|
1.1 AndroidRuntime启动
base/core/jni/AndroidRuntime.cpp
AndroidRuntime类主要做了以下几件事情:
- 调用startVM创建一个Dalvik虚拟机,JNI_CreateJavaVM真正创建并初始化虚拟机实例
- 调用startReg注册Android核心类的JNI方法
- 通过Zygote进程进入Java层
在JNI中,dvmCreateJNIEnv为当前线程创建和初始化一个JNI环境,即一个JNIEnvExt对象。最后调用dvmStartup来初始化前面创建的Dalvik虚拟机实例。函数dvmInitZygote调用了系统的setpgid来设置当前进程,即Zygote进程的进程组ID。这一步完成后,Dalvik虚拟机的创建和初始化工作就完成了
start()
1 | void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote) |
1.2 决定启动Art还是Dalvik
1 | JniInvocation jni_invocation; |
JniInvocation这个结构是在/libnativehelper/目录下定义的。对于虚拟机的选择也就是在这里确定的。persist.sys.dalvik.vm.lib属性的值实际上是so文件的路径,可能是libdvm.so,也可能是libart.so,前者是Dalvik虚拟机的实现,而后者就是ART虚拟机的实现。
1.3 重点分析startVM()
删除了部分代码
1 | int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote) |
2.Davilk内存
2.1 内存分配
源码中heap parameter的位置
frameworks/native/build/phone-hdpi-1024-dalvik-heap.mk
1 | PRODUCT_PROPERTY_OVERRIDES += \ |
如果依照如下设定1
2
3dalvik.vm.heapstartsize=8m
dalvik.vm.heapgrowthlimit=64m
dalvik.vm.heapsize=256m
- 每开启一个App,就会划出8m空间给它。如果使用过程超出8m,就会再次增加8m。但是只能增加7次空间,因为最大的空间为64m。超过了就会产生OOM。
- heapsize=256m,单个虚拟机分配的最大内存,相当于可以运行4个64m的应用。超过内存的部分就会进行内存回收,一方面是强制关闭一些应用,另一方面是加载新应用划分新的内存,这时候设备就会出现卡顿。
- 有些人喜欢用小widget,比如每个只占3-5m,如果按照8m去分配,每个小应用都会多浪费3m的内存。这种情况就可以把heapstartsize改小,可以增加可用内存。
内存分配流程
首先判断一下需要申请的size是不是过大,如果申请的size超过了堆的最大限制,则转入步骤6
尝试分配,如果成功则返回,失败则转入步骤3
- 判断是否gc正在进行垃圾回收,如果正在进行则等待回收完成之后,尝试分配。如果成功则返回,失败则转入步骤4
- 自己启动gc进行垃圾回收,这里gcForMalloc的参数是false。所以不会回收软引用,回收完成后尝试分配,如果成功则返回,失败则转入步骤5
- 调用dvmHeapSourceAllocAndGrow尝试分配,这个函数会扩张堆。所以heap startup的时候可以给一个比较小的初始堆,实在不够用再调用它进行扩张
- 进入回收软引用阶段,这里gcForMalloc的参数是ture,所以需要回收软引用。然后调用dvmHeapSourceAllocAndGrow尝试分配,如果失败则抛出OOM。
重要调优参数
dalvik.vm.heapstartsize
堆分配的初始大小,调整这个值会影响到应用的流畅性和整体ram消耗。这个值越小,系统ram消耗越慢,但是由于初始值较小,一些较大的应用需要扩张这个堆,从而引发gc和堆调整的策略,会应用反应更慢。相反,这个值越大系统ram消耗越快,但是程序更流畅。
dalvik.vm.heapgrowthlimit
极限堆大小,dvm heap是可增长的,但是正常情况下dvm heap的大小是不会超过dalvik.vm.heapgrowthlimit的值。如果受控的应用dvm heap size超过该值,则将引发oom。
dalvik.vm.heapsize
使用大堆时,极限堆大小。一旦dalvik heap size超过这个值,直接引发oom。在android开发中,如果要使用大堆,需要在manifest中指定android:largeHeap为true。这样dvm heap最大可达dalvik.vm.heapsize。
[dalvik.vm.heaptargetutilization]: [0.75]
可以设定内存利用率的百分比,当实际的利用率偏离这个百分比的时候,虚拟机会在GC的时候调整堆内存大小,让实际占用率向个百分比靠拢。
2.2 内存管理
Dalvik虚拟机使用常用的Mark-Sweep(标记回收)算法,该算法分Mark阶段(标记出活动对象)、Sweep阶段(回收垃圾内存)和可选的Compact阶段(减少堆中的碎片)。
垃圾收集的第一步是标记出活动对象。当进行垃圾收集时,需要停止Dalvik虚拟机的运行(除垃圾收集外),因此垃圾收集又被称作STW(stop-the-world)。Dalvik虚拟机在运行过程中要维护一些状态信息,这些信息包括:每个线程所保存的寄存器、Java类中的静态字段、局部和全局的JNI引用,JVM中的所有函数调用会对应一个相应C的栈帧。每一个栈帧里可能包含对对象的引用,比如包含对象引用的局部变量和参数。所有这些引用信息被加入到一个根集合中,然后从根集合开始,递归查找可以从根集合出发访问的对象。因此,Mark过程又叫做追踪,追踪所有可被访问的对象。
垃圾收集的第二步就是回收内存。在Mark阶段通过markBits位图可以得到所有可访问的对象集合,而liveBits位图表示所有已经分配的对象集合。通过比较liveBits位图和markBits位图的差异就是所有可回收的对象集合。Sweep阶段调用free来释放这些内存给堆。
在底层内存实现上,Android系统使用的是msspace,这是一个轻量级的malloc实现。除了创建和初始化用于存储普通Java对象的内存堆,Android还创建三个额外的内存堆:
- “livebits”(用来存放堆上内存被占用情况的位图索引)
- “markbits”(在GC时用于标注存活对象的位图索引)
- “markstack”(在GC中遍历存活对象引用的标注栈)
虚拟机通过一个名为gHs的全局HeapSource变量来操控GC内存堆,而HeapSource里通过heaps数组可以管理多个堆(Heap),以满足动态调整GC内存堆大小的要求。另外HeapSource里还维护一个名为”livebits”的位图索引,以跟踪各个堆(Heap)的内存使用情况。剩下两个数据结构”markstack”和”markbits”都是用在垃圾回收阶段。
上图中”livebits”维护堆上已用的内存信息,而”markbits”这个位图索引则指向存活的对象。 A、C、F、G、H对象需要保留,因此”markbits”分别指向他们(最后的H对象尚在标注过程中,因此没有指针指向它)。而”markstack”就是在标注过程中跟踪当前需要处理的对象要用到的标志栈,此时其保存了正在处理的对象F、G和H。
3.Dalvik GC
Davlik上的垃圾回收主要是在下面的这些时机会触发:
- 堆中无法再创建对象的时候
- 堆中的内存使用率超过阈值的时候
- 程序通过Runtime.gc()主动GC的时候
- 在OOM发生之前的时候
不同时机下GC的策略是有区别的,在Heap.h中定义了这四种GC的策略:
1 | / Heap.h |
GC_FOR_MALLOC、GC_CONCURRENT和GC_BEFORE_OOM三种类型的GC都是在分配对象的过程触发的。而并发和非并发GC的区别主要在于,前者在GC过程中,有条件地挂起和唤醒非GC线程,而后者在执行GC的过程中,一直都是挂起非GC线程的。并行GC通过有条件地挂起和唤醒非GC线程,就可以使得应用程序获得更好的响应性。但是同时并行GC需要多执行一次标记根集对象以及递归标记那些在GC过程被访问了的对象的操作,所以也需要花费更多的CPU资源
3.1Davilk GC策略
因为GC_CONCURRENT和GC_FOR_MALLOC是由系统触发的,所以应用是无法减少这两种类型的GC事件。但需要减少这两种GC事件是,可以通过配置dalvik的系统属性或者修改dalvik的GC算法来实现。
在一次GC后,根据当前Heap中已分配的内存大小除以dalvik.vm.heaputilization(0.75),得到一个目标值。
如果目标值不在(已分配的值+dalvik.vm.heapminfree)到(已分配的值+dalvik.vm.heapmaxfree)这个区间,即取区间边界值做为目标值(运行一段时间后第1步得到的目标值肯定会超过这个范围)。
虚拟机记录这个目标值,当做当前允许总的可以分配到的内存。同时根据目标值减去固定值(200~500K),当做触发GC_CONCURRENT事件的阈值。
当下一次分配内存,分配成功时。重新计算已分配的内存大小;若有达到GC_CONCURRENT的阈值,则产生GC。
当下一次分配内存,开始分配失败时。则会产生GC_FOR_ALLOC事件,释放内存;然后再尝试分配。
可以通过调整dalvik.vm.heapminfree 和dalvik.vm.heapmaxfree属性的值,减少GC_FOR_ALLOC和GC_CONCURRENT的次数,如果这两个值设置的过大,则会导致一次GC的时间过长,从而会看到明显的卡顿现象,设置的值既要使GC的次数减少,也不能是一次GC的时间过长。
在有的平台上,可以通过代码对单个应用的dalvik的属性进行设置,以减少对全局设置对系统的影响。
可以再App里面通过如下的方式对当前的App的dalvik属性设置:1
2
3
4
5
6import dalvik.system.VMRuntime;
import android.os.SystemProperties;
VMRuntime.getRuntime().setTargetHeapUtilization(0.75f);
VMRuntime.getRuntime().setTargetHeapMinFree(2*1024*1024);
VMRuntime.getRuntime().setTargetHeapConcurrentStart(8*1024*1024);
如果想通过系统进行控制,也可以在framework里面的ActivityThread的handleBindApplication函数里面进行设置
1 | import dalvik.system.VMRuntime; |
3.2 GC日志分析
接下来,我们来学会如何分析Android虚拟机中的GC日志,日志如下:
1 | D/dalvikvm(7030):GC_CONCURRENT freed 1049K, 60% free 2341K/9351K, external 3502K/6261K, paused 3ms 3ms |
GC_CONCURRENT是当前GC时的类型,GC日志中有以下几种类型:
- GC_CONCURRENT:当应用程序中的Heap内存占用上升时(分配对象大小超过384k),避免Heap内存满了而触发的GC。如果发现有大量的GC_CONCURRENT出现,说明应用中可能一直有大于384k的对象被分配,而这一般都是一些临时对象被反复创建,可能是对象复用不够所导致的。
- GC_FOR_MALLOC:这是由于Concurrent GC没有及时执行完,而应用又需要分配更多的内存,这时不得不停下来进行Malloc GC。
- GC_EXTERNAL_ALLOC:这是为external分配的内存执行的GC。
- GC_HPROF_DUMP_HEAP:创建一个HPROF profile的时候执行。
- GC_EXPLICIT:显示调用了System.GC()。(尽量避免)
再回到上面打印的日志:
- freed 1049k 表明在这次GC中回收了多少内存。
- 60% free 2341k/6261K 表明回收后60%的Heap可用,存活的对象大小为2341kb,heap大小是9351kb。
- external 3502/6261K 是Native Memory的数据。存放Bitmap Pixel Data(位图数据)或者堆以外内存(NIO Direct Buffer)之类的。第一个值说明在Native Memory中已分配3502kb内存,第二个值是一个浮动的GC阈值,当分配内存达到这个值时,会触发一次GC。
- paused 3ms 3ms 表明GC的暂停时间,如果是Concurrent GC,会看到两个时间,一个开始,一个结束,且时间很短,如如果是其他类型的GC,很可能只会看到一个时间,且这个时间是相对比较长的。并且,越大的Heap Size在GC时导致暂停的时间越长。