Android VM 02.Davilk 启动 内存 GC

1. DVM启动流程

android开机后,加载init.rc文件,会启动app_process进程,在对应app_main.cpp的main()中,调用AndroidRuntime.cpp

base/cmds/app_process/app_main.cpp
1
2
3
4
5
6

if (zygote) {
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
}

1.1 AndroidRuntime启动

base/core/jni/AndroidRuntime.cpp

image

AndroidRuntime类主要做了以下几件事情:

  • 调用startVM创建一个Dalvik虚拟机,JNI_CreateJavaVM真正创建并初始化虚拟机实例
  • 调用startReg注册Android核心类的JNI方法
  • 通过Zygote进程进入Java层

在JNI中,dvmCreateJNIEnv为当前线程创建和初始化一个JNI环境,即一个JNIEnvExt对象。最后调用dvmStartup来初始化前面创建的Dalvik虚拟机实例。函数dvmInitZygote调用了系统的setpgid来设置当前进程,即Zygote进程的进程组ID。这一步完成后,Dalvik虚拟机的创建和初始化工作就完成了

start()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
/* start the virtual machine */
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
//启动虚拟机
if (startVm(&mJavaVM, &env, zygote) != 0) {
return;
}
onVmCreated(env);

/*
* Register android functions.
*/
if (startReg(env) < 0) {//注册JNI
ALOGE("Unable to register all android natives\n");
return;
}

}

1.2 决定启动Art还是Dalvik

1
2
JniInvocation jni_invocation;
jni_invocation.Init(NULL);

JniInvocation这个结构是在/libnativehelper/目录下定义的。对于虚拟机的选择也就是在这里确定的。persist.sys.dalvik.vm.lib属性的值实际上是so文件的路径,可能是libdvm.so,也可能是libart.so,前者是Dalvik虚拟机的实现,而后者就是ART虚拟机的实现。

image

1.3 重点分析startVM()

删除了部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
   int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote)
{

bool checkJni = false;
//控制Dalvik虚拟机在调用外部jni链接库的时候是否对其做安全性检验。可填写的值为true和false。若值为true,会增加程序的兼容性和稳定性,但也会增加其加载和执行的时间,推荐为false。
property_get("dalvik.vm.checkjni", propBuf, "");
if (strcmp(propBuf, "true") == 0) {
checkJni = true;
} else if (strcmp(propBuf, "false") != 0) {
/* property is neither true nor false; fall back on kernel parameter */
property_get("ro.kernel.android.checkjni", propBuf, "");
if (propBuf[0] == '1') {
checkJni = true;
}
}


//本参数控制Dalvik虚拟机的程序执行机制
property_get("dalvik.vm.execution-mode", propBuf, "");
//portable表示以兼容模式运行(脚本翻译模式),此模式下程序的兼容性最高,但其执行效率最低(程序优化度依赖于dalvik虚拟机版本)。官方默认此模式
if (strcmp(propBuf, "int:portable") == 0) {/
executionMode = kEMIntPortable;
//fast表示以快速自优化模式运行(脚本翻译和预优化混合),此模式下程序的兼容性很高,执行效率也比较高。因为此时dalvik虚拟机允许程序使用自己的预定义优化模式和代码(包括C/C++/汇编代码)。推荐使用。
} else if (strcmp(propBuf, "int:fast") == 0) {
executionMode = kEMIntFast;
//jit表示以Just-In-Time模式运行(JIT模式),此模式下程序的兼容性最差,但程序一旦加载后其运行效率最高(与C/C++直接编 写的程序效率无异),因为在此模式下dalvik虚拟机会预先将Java程序翻译成针对机器平台的本地语言(Native),同时完全允许代码中的所有预 优化和代码,允许所有不安全的非托管代码,同时不严谨的程序如果运行在JIT模式可能会造成内存泄露。
} else if (strcmp(propBuf, "int:jit") == 0) {
executionMode = kEMJitCompiler;
}

//本参数控制Dalvik虚拟机的堆栈记录调试文件。用于系统调试,一般用户对其调整无意义。
parseRuntimeOption("dalvik.vm.stack-trace-file", stackTraceFileBuf, "-Xstacktracefile:");



/*
* The default starting and maximum size of the heap. Larger
* values should be specified in a product property override.
*/
//参数控制Dalvik虚拟机在启动一个应用程序之后为其分配的初始堆栈大小
parseRuntimeOption("dalvik.vm.heapstartsize", heapstartsizeOptsBuf, "-Xms", "4m");
//参数控制单个Dalvik虚拟机可以分配到的最大堆栈量
parseRuntimeOption("dalvik.vm.heapsize", heapsizeOptsBuf, "-Xmx", "16m");
//指定标准app的最大堆栈,如果在mainfest中设置Android:largeheap=”true”时,应用会使用最大内存,超过这个值会有 OOM产生
parseRuntimeOption("dalvik.vm.heapgrowthlimit", heapgrowthlimitOptsBuf, "-XX:HeapGrowthLimit=");

//与-XX:HeapTargetUtilization结合使用,以确定堆需要增长时堆的增长量
parseRuntimeOption("dalvik.vm.heapminfree", heapminfreeOptsBuf, "-XX:HeapMinFree=");
parseRuntimeOption("dalvik.vm.heapmaxfree", heapmaxfreeOptsBuf, "-XX:HeapMaxFree=");
//为VM提供了关于应该允许托管堆满的程度的提示
parseRuntimeOption("dalvik.vm.heaptargetutilization",
heaptargetutilizationOptsBuf,
"-XX:HeapTargetUtilization=");

/*
* JIT related options.
*/
parseRuntimeOption("dalvik.vm.usejit", usejitOptsBuf, "-Xusejit:");
parseRuntimeOption("dalvik.vm.jitmaxsize", jitmaxsizeOptsBuf, "-Xjitmaxsize:");
parseRuntimeOption("dalvik.vm.jitinitialsize", jitinitialsizeOptsBuf, "-Xjitinitialsize:");
parseRuntimeOption("dalvik.vm.jitthreshold", jitthresholdOptsBuf, "-Xjitthreshold:");
property_get("dalvik.vm.usejitprofiles", useJitProfilesOptsBuf, "");
if (strcmp(useJitProfilesOptsBuf, "true") == 0) {
addOption("-Xjitsaveprofilinginfo");
}

parseRuntimeOption("dalvik.vm.jitprithreadweight",
jitprithreadweightOptBuf,
"-Xjitprithreadweight:");

parseRuntimeOption("dalvik.vm.jittransitionweight",
jittransitionweightOptBuf,
"-Xjittransitionweight:");

property_get("ro.config.low_ram", propBuf, "");
if (strcmp(propBuf, "true") == 0) {
addOption("-XX:LowMemoryMode");
}

parseRuntimeOption("dalvik.vm.gctype", gctypeOptsBuf, "-Xgc:");
parseRuntimeOption("dalvik.vm.backgroundgctype", backgroundgcOptsBuf, "-XX:BackgroundGC=");

/*
* Enable debugging only for apps forked from zygote.
* Set suspend=y to pause during VM init and use android ADB transport.
*/
if (zygote) {
addOption("-agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y");
}
//控制Dalvik虚拟机调试记录程序内部锁资源争夺的阈值,默认值是500。多用于程序的数据统计,对性能较调意义不大
parseRuntimeOption("dalvik.vm.lockprof.threshold",
lockProfThresholdBuf,
"-Xlockprofthreshold:");

if (executionMode == kEMIntPortable) {
addOption("-Xint:portable");
} else if (executionMode == kEMIntFast) {
addOption("-Xint:fast");
} else if (executionMode == kEMJitCompiler) {
addOption("-Xint:jit");
}

// If we are booting without the real /data, don't spend time compiling.
property_get("vold.decrypt", voldDecryptBuf, "");
bool skip_compilation = ((strcmp(voldDecryptBuf, "trigger_restart_min_framework") == 0) ||
(strcmp(voldDecryptBuf, "1") == 0));

// Extra options for boot.art/boot.oat image generation.
parseCompilerRuntimeOption("dalvik.vm.image-dex2oat-Xms", dex2oatXmsImageFlagsBuf,
"-Xms", "-Ximage-compiler-option");
parseCompilerRuntimeOption("dalvik.vm.image-dex2oat-Xmx", dex2oatXmxImageFlagsBuf,
"-Xmx", "-Ximage-compiler-option");
if (skip_compilation) {
addOption("-Ximage-compiler-option");
addOption("--compiler-filter=verify-none");
} else {
parseCompilerOption("dalvik.vm.image-dex2oat-filter", dex2oatImageCompilerFilterBuf,
"--compiler-filter=", "-Ximage-compiler-option");
}

// Make sure there is a preloaded-classes file.
if (!hasFile("/system/etc/preloaded-classes")) {
ALOGE("Missing preloaded-classes file, /system/etc/preloaded-classes not found: %s\n",
strerror(errno));
return -1;
}
addOption("-Ximage-compiler-option");
addOption("--image-classes=/system/etc/preloaded-classes");

// If there is a compiled-classes file, push it.
if (hasFile("/system/etc/compiled-classes")) {
addOption("-Ximage-compiler-option");
addOption("--compiled-classes=/system/etc/compiled-classes");
}

property_get("dalvik.vm.image-dex2oat-flags", dex2oatImageFlagsBuf, "");
parseExtraOpts(dex2oatImageFlagsBuf, "-Ximage-compiler-option");

// Extra options for DexClassLoader.
parseCompilerRuntimeOption("dalvik.vm.dex2oat-Xms", dex2oatXmsFlagsBuf,
"-Xms", "-Xcompiler-option");
parseCompilerRuntimeOption("dalvik.vm.dex2oat-Xmx", dex2oatXmxFlagsBuf,
"-Xmx", "-Xcompiler-option");
if (skip_compilation) {
addOption("-Xcompiler-option");
addOption("--compiler-filter=verify-none");

// We skip compilation when a minimal runtime is brought up for decryption. In that case
// /data is temporarily backed by a tmpfs, which is usually small.
// If the system image contains prebuilts, they will be relocated into the tmpfs. In this
// specific situation it is acceptable to *not* relocate and run out of the prebuilts
// directly instead.
addOption("--runtime-arg");
addOption("-Xnorelocate");
} else {
parseCompilerOption("dalvik.vm.dex2oat-filter", dex2oatCompilerFilterBuf,
"--compiler-filter=", "-Xcompiler-option");
}
parseCompilerOption("dalvik.vm.dex2oat-threads", dex2oatThreadsBuf, "-j", "-Xcompiler-option");
parseCompilerOption("dalvik.vm.image-dex2oat-threads", dex2oatThreadsImageBuf, "-j",
"-Ximage-compiler-option");

2.Davilk内存

2.1 内存分配

源码中heap parameter的位置

frameworks/native/build/phone-hdpi-1024-dalvik-heap.mk

1
2
3
4
5
6
PRODUCT_PROPERTY_OVERRIDES += \
dalvik.vm.heapstartsize=5m \ #初始化dalvik分配的内存大小。
dalvik.vm.heapsize=32m \#:在mainfest中设置android:largeheap=”true”时,应用的最大内存,超过这个值会有OOM产生。
dalvik.vm.heaptargetutilization=0.75 \
dalvik.vm.heapminfree=512k \
dalvik.vm.heapmaxfree=2m

如果依照如下设定

1
2
3
dalvik.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改小,可以增加可用内存。

内存分配流程

  1. 首先判断一下需要申请的size是不是过大,如果申请的size超过了堆的最大限制,则转入步骤6

  2. 尝试分配,如果成功则返回,失败则转入步骤3

  3. 判断是否gc正在进行垃圾回收,如果正在进行则等待回收完成之后,尝试分配。如果成功则返回,失败则转入步骤4
  4. 自己启动gc进行垃圾回收,这里gcForMalloc的参数是false。所以不会回收软引用,回收完成后尝试分配,如果成功则返回,失败则转入步骤5
  5. 调用dvmHeapSourceAllocAndGrow尝试分配,这个函数会扩张堆。所以heap startup的时候可以给一个比较小的初始堆,实在不够用再调用它进行扩张
  6. 进入回收软引用阶段,这里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阶段(减少堆中的碎片)。

Android内存管理原理

垃圾收集的第一步是标记出活动对象。当进行垃圾收集时,需要停止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”都是用在垃圾回收阶段。

image

上图中”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
2
3
4
5
6
7
8
9
10
11
12
13
14
/ Heap.h
//内存分配时,发现可用内存不够时触发的GC事件
/* Not enough space for an "ordinary" Object to be allocated. */
extern const GcSpec *GC_FOR_MALLOC;
//给Java层的class分配内存后,计算已分配的大小达到阈值(当前DVM heap size小一点)时会触发的GC事件。
/* Automatic GC triggered by exceeding a heap occupancy threshold. */
extern const GcSpec *GC_CONCURRENT;

//应用主动调用System.gc()产生的GC事件。
/* Explicit GC via Runtime.gc(), VMRuntime.gc(), or SIGUSR1. */
extern const GcSpec *GC_EXPLICIT;

/* Final attempt to reclaim memory before throwing an OOM. */
extern const GcSpec *GC_BEFORE_OOM;

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算法来实现。

  1. 在一次GC后,根据当前Heap中已分配的内存大小除以dalvik.vm.heaputilization(0.75),得到一个目标值。

  2. 如果目标值不在(已分配的值+dalvik.vm.heapminfree)到(已分配的值+dalvik.vm.heapmaxfree)这个区间,即取区间边界值做为目标值(运行一段时间后第1步得到的目标值肯定会超过这个范围)。

  3. 虚拟机记录这个目标值,当做当前允许总的可以分配到的内存。同时根据目标值减去固定值(200~500K),当做触发GC_CONCURRENT事件的阈值。

  4. 当下一次分配内存,分配成功时。重新计算已分配的内存大小;若有达到GC_CONCURRENT的阈值,则产生GC。

  5. 当下一次分配内存,开始分配失败时。则会产生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
6
import 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
2
3
4
5
6
7
import dalvik.system.VMRuntime; 
import android.os.SystemProperties;
import java.lang.*; ...
if( data.processName.equals("com.android.launcher")) {
VMRuntime.getRuntime().setTargetHeapUtilization(0.75f);
VMRuntime.getRuntime().setTargetHeapMinFree(2*1024*1024);
VMRuntime.getRuntime().setTargetHeapConcurrentStart(8*1024*1024); }

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时导致暂停的时间越长。

Dalvik配置与JVM

垃圾收集器与内存分配策略

gc heap parameter

理解Android虚拟机体系结构(转)

深入理解 Android(二):Java 虚拟机 Dalvik

Android上的Dalvik虚拟机