1.Dalvik 虚拟机
Dalvik 虚拟机(Dalvik Virtual Machine ),简称 Dalvik VM 或者 DVM。它是由 Dan Bomstein编写的,名字源于他的祖先居住过的名为Dalvik的小渔村。DVM是Google专门 为Android平台开发的虚拟机,它运行在Android运行时库中。需要注意的是DVM并不是 一个Java虚拟机(以下简称JVM),至于为什么,下文会给你答案。
1.DVM与JVM的区别
DVM之所以不是一个JVM,主要原因是DVM并没有遵循JVM规范来实现,DVM 与JVM主要有以下区别。
1.基于的架构不同
JVM基于栈则意味着需要去栈中读写数据,所需的指令会更多,这样会导致速度变慢, 对于性能有限的移动设备,显然不是很适合的。DVM是基于寄存器的,它没有基于栈的虚 拟机在复制数据时而使用的大量的出入栈指令,同时指令更紧凑、更简洁。但是由于显式 指定了操作数,所以基于寄存器的指令会比基于栈的指令要大,但是由于指令数量的减少, 总的代码数不会增加多少。
2 .执行的字节码不同
在Java SE程序中,Java类被编译成一个或多个.class文件,并打包成jar文件,而后 JVM会通过相应的.class文件和jar文件获取相应的字节码。执行顺序为.java文件->.class 文件->.jar文件,而DVM会用dx工具将所有的.class文件转换为一个.dex文件,然后DVM 会从该.dex文件读取指令和数据。执行顺序为.java文件->.class文件->.dex文件。
如图11-1所示,.jar文件里面包含多个.class文件,每个.class文件里面包含了该类的 常量池、类信息、属性等。当JVM加载该.jar文件的时候,会加载里面的所有的.class文件, JVM的这种加载方式很慢,对于内存有限的移动设备并不合适。而在.apk文件中只包含了 一个.dex文件,这个.dex文件将所有的.class里面所包含的信息全部整合在一起了,这样再 加载就加快了速度。.class文件存在很多的冗余信息,dex工具会去除冗余信息,并把所有 的.class文件整合到.dex文件中,减少了 I/O操作,加快了类的査找速度。
3.DVM允许在有限的内存中同时运行多个进程
DVM经过优化,允许在有限的内存中同时运行多个进程。在Android中的每一个应用 都运行在一个DVM实例中,每一个DVM实例都运行在一个独立的进程空间中,独立的 进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
4.DVM由Zygote创建和初始化
我们在第2章学习过Zygote,它是一个DVM进程,同时也用来创建和初始化DVM 实例。每当系统需要创建一个应用程序时,Zygote就会fock自身,快速地创建和初始化一 个DVM实例,用于应用程序的运行。对于一些只读的系统库,所有的DVM实例都会和 Zygote共享一块内存区域,节省了内存开销。
5.DVM有共享机制
DVM拥有预加载一共享的机制,不同应用之间在运行时可以共享相同的类,拥有更高 的效率。而JVM机制不存在这种共享机制,不同的程序,打包以后的程序都是彼此独立的, 即便它们在包里使用了同样的类,运行时也都是单独加载和运行的,无法进行共享。
6.DVM早期没有使用JIT编译器
JVM使用了 JIT编译器(Just In Time Compiler,即时编译器),而DVM早期没有使用 JIT编译器。早期的DVM每次执行代码,都需要通过解释器将dex代码编译成机器码,然 后交给系统处理,效率不是很高。为了解决这一问题,从Android 2.2版本开始DVM使用 了 J1T编译器,它会对多次运行的代码(热点代码)进行编译,生成相当精简的本地机器 码(Native Code),这样在下次执行到相同逻辑的时候,直接使用编译之后的本地机器码, 而不是每次都需要编译。需要注意的是,应用程序每一次重新运行的时候,都要重做这个 编译工作,因此每次重新打开应用程序,都需要JIT编译。
2.DVM 架构
DVM的源码位于dalvik/目录下,Android 8.0中的DVM源码的部分目录说明如表11-1 所示。
表11-1 DVM的源码目录
目录/文件 | 说 明 |
dexdump | 生成dex文件的反编译査看工具,主要用来査看编译出来的代码的正确性和结构 |
dexgen | dex代码生成器项目 |
docs | DVM相关帮助文档 |
dx | Java字节码转换为DVM机器码的工具 |
libdex | 生成主机和设备处理dex文件的库 |
tools | 一些编译和运行相关的工具 |
Android.mk | 虚拟机编译的makefile配置文件 |
MODULE LICENSE APACHE2 | APACHE2版权声明文件 |
NOTICE | 虚拟机源码版权注意事项文件 |
其中,dalvik/libdex会被编译成libdex.a静态库,作为dex工具使用;dalvik/dexdump 是.dex文件的反编译工具,DVM架构如图11-2所示。
图11-2 DVM架构
从图11-2可以看出,首先Java编译器编译的.class文件经过DX工具转换为.dex文 件,.dex文件由类加载器处理,接着解释器根据指令集对Dalvik字节码进行解释、执行, 最后交于Linux处理。
3.DVM的运行时堆
DVM的运行时堆使用标记一清除(Mark-Sweep)算法进行GC,它由两个Space以及 多个辅助数据结构组成,两个Space分别是Zygote Space (Zygote Heap)和Allocation Space (Active Heap)o Zygote Space用来管理Zygote进程在启动过程中预加载和创建的各种对象, Zygote Space中不会触发GC,在Zygote进程和应用程序进程之间会共享Zygote Spaceo在 Zygote进程fork第一个子进程之前,会把Zygote Space分为两个部分,原来的已经被使用 的那部分堆仍旧叫Zygote Space,而未使用的那部分堆就叫Allocation Space,以后的对象 都会在Allocation Space JL进行分配和释放。Allocation Space不是进程间共享的,在每个进 程中都独立拥有一份。除了这两个Space,还包含以下数据结构。
- Card Table:用于DVM Concurrent GC,当第一次进行垃圾标记后,记录垃圾信息。
- Heap Bitmap:有两个Heap Bitmap, 一个用来记录上次GC存活的对象,另一个用 来记录这次GC存活的对象。
- Mark Stack: DVM的运行时堆使用标记一清除(Mark-Sweep)算法进行GC, Mark Stack就是在GC的标记阶段使用的,它用来遍历存活的对象。
4.DVM 的 GC 日志
在10.6.2节中提到了 Java虚拟机的GC日志。DVM和ART的GC日志与Java虚拟机 的日志有较大的区别。在DVM中每次垃圾收集都会将GC日志打印到logcat中,具体的 格式为:
D/dalvikvm: <GC_Reason> <2kmount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>
可以看到DVM的日志共有5个信息,其中GC Reason有很多种,这里将它单独拿出来进行介绍。
1.引起GC的原因
GC Reason就是引起GC的原因,有以下几种。
- GC_CONCURRENT:当堆开始填充时,并发GC可以释放内存。
- GC_FOR_MALLOC:当堆内存已满时,App尝试分配内存而引起的GC,系统必须 停止App并回收内存。
- GC_HPROF_DUMP_HEAP:当你请求创建HPROF文件来分析堆内存时出现的GCO
- GC_EXPLICIT:显式的GC,例如调用System.gc()(应该避免调用显式的GC,信 任GC会在需要时运行)。
- GC_EXTERNAL_ALLOC:仅适用于API级别小于等于10,且用于外部分配内存的 GCO
2.其他的信息
除了引起GC的原因,其他的信息如下。
- Amount freed:本次GC释放内存的大小。
- Heap stats:堆的空闲内存百分比(已用内存)/ (堆的总内存)。
- Extemal_memory_stats: API小于等于级别10的内存分配(已分配的内存)/ (引起 GC的阈值)。
- Pause time:暂停时间,更大的堆会有更长的暂停时间。并发暂停时间会显示两个暂 停时间,即一个出现在垃圾收集开始时,另一个出现在垃圾收集快要完成时。
3.实例分析
为了让大家更好地理解DVM的GC 0志,举一个具体的GC日志实例,如下所示:
D/dalvikvm: GC_CONCURRENT freed 2012K, 63% free 3213K/9291K, external 4501K/5161K, paused 2ms+2ms
这个GC日志的含义为:引起GC的原因是GC_CONCURRENT;本次GC释放的内存 为2012KB;堆的空闲内存百分比为63%,已用内存为3213KB,堆的总内存为9291KB; 暂停的总时长为4ms。
ART虚拟机
ART (Android Runtime)虚拟机是 Android 4.4 发布的,用来替换 Dalvik 虚拟机,Android 4.4默认采用的还是DVM,系统会提供一个选项来开启ART。在Android 5.0版本中默认采 用了 ART, DVM从此退出历史舞台。
1.ART与DVM的区别
ART和DVM的区别主要有如下4点。
- 从11.1节我们知道,DVM中的应用每次运行时,字节码都需要通过JIT编译器编译为机器码,这会使得应用程序的运行效率降低。而在ART中,系统在安装应用程序时会 进行一次AOT (ahead of time compilation,预编译),将字节码预先编译成机器码并存储在 本地,这样应用程序每次运行时就不需要执行编译了,运行效率会大大提升,设备的耗电 量也会降低。这就好比我们在线阅读漫画,DVM是我们阅读到哪就加载哪,ART则是直 接加载一章的漫画,虽然一开始加载速度有些慢,但是后续的阅读体验会很流畅。采用AOT 也会有缺点,主要有两个:第一个是AOT会使得应用程序的安装时间变长,尤其是一些复 杂的应用;第二个是字节码预先编译成机器码,机器码需要的存储空间会多一些。为了解 决上面的缺点,Android 7.0版本中的ART加入了即时编译器JIT,作为AOT的一个补充, 在应用程序安装时并不会将字节码全部编译成机器码,而是在运行中将热点代码编译成机 器码,从而缩短应用程序的安装时间并节省了存储空间。
- DVM是为32位CPU设 计的,而ART支持64位并兼容32位CPU,这也是DVM 被淘汰的主要原因之一。
- ART对垃圾回收机制进行了改进,比如更频繁地执行并行垃圾收集,将GC暂停 由2次减少为1次等。
- ART的运行时堆空间划分和DVM不同。
2.ART的运行时堆
与DVM的GC不同的是,ART采用了多种垃圾收集方案,每个方案会运行不同的垃圾收集器,默认是采用了 CMS( Concurrent Mark-Sweep)方案,该方案主要使用了 sticky-CMS 和partial-CMS。根据不同的CMS方案,ART的运行时堆的空间也会有不同的划分,默认 是由4个Space和多个辅助数据结构组成的,4个Space分别是Zygote Space、Allocation Space、Image Space 和 Large Object Spaceo Zygote Space、Allocation Space 和 DVM 中的作 用是一样的,Image Space用来存放一些预加载类,Large Object Space用来分配一些大对象 (默认大小为12KB),其中Zygote Space和Image Space是进程间共享的。采用标记一清除算法的运行时堆空间划分如图11-3所示。
除了这四个Space, ART的Java堆中还包括两个Mod Union Table, 一个Card Table, 两个 Heap Bitmap,两个 Object Map,以及三个 Object Stacko
3.ART 的 GC 日志
ART的GC H志与DVM不同,ART会为那些主动请求的垃圾收集事件或者认为GC 速度慢时才会打印GC日志。GC速度慢指的是GC暂停超过5ms或者GC持续时间超过 100mso如果App未处于可察觉的暂停进程状态,那么它的GC不会被认为是慢速的。
ART的GC H志具体的格式为:
I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>
下面对GC日志的组成部分进行介绍。
1. 引起GC原因
ART的引起GC原因(GC_Reason)要比DVM多一些,有以下几种。
- Concurrent:并发GC,不会使App的线程暂停,该GC是在后台线程运行的,并不 会阻止内存分配。
- Alloc:当堆内存已满时,App尝试分配内存而引起的GC,这个GC会发生在正在 分配内存的线程中。
- Explicit: App显示的请求垃圾收集,例如调用System.gc()o与DVM 一样,最佳做 法是应该信任GC并避免显式地请求GC,显式地请求GC会阻止分配线程并不必要 地浪费CPU周期。如果显式地请求GC导致其他线程被抢占,那么有可能会导致jank (App同一帧画了多次)。
- NativeAlloc: Native内存分配时,比如为Bitmaps或者RenderScript分配对象,这会 导致Native内存压力,从而触发GC。
- CollectorTransition:由堆转换引起的回收,这是运行时切换GC而引起的。收集器 转换包括将所有对象从空闲列表空间复制到碰撞指针空间(反之亦然)。当前,收集 器转换仅在以下情况下出现:在内存较小的设备上,App将进程状态从可察觉的暂 停状态变更为可察觉的非暂停状态(反之亦然)。
- HomogeneousSpaceCompact:齐性空间压缩是指空闲列表到压缩的空闲列表空间, 通常发生在当App已经移动到可察觉的暂停进程状态时。这样做的主要原因是减少 了内存使用并对堆内存进行碎片整理。
- DisableMovingGc:不是真正触发GC的原因,发生并发堆压缩时,由于使用了 GetPrimitiveArrayCritical,收集会被阻塞。在一般情况下,强烈建议不要使用 GetPrimitiveArrayCritical,因为它在移动收集器方面具有限制。
- HeapTrim:不是触发GC的原因,但是请注意,收集会一直被阻塞,直到堆内存整 理完毕。
2.垃圾收集器名称
GC_Name指的是垃圾收集器名称,有以下几种。
- Concurrent Mark Sweep (CMS): CMS收集器是一种以获取最短收集暂停时间为目 标的收集器,采用了标记一清除算法实现。它是完整的堆垃圾收集器,能释放除了 Image Space外的所有的空间。
- Concurrent Partial Mark Sweep:部分完整的堆垃圾收集器,能释放除了 Image Space 和Zygote Space外的所有空间。
- Concurrent Sticky Mark Sweep:粘性收集器,基于分代的垃圾收集思想,它只能释 放自上次GC以来分配的对象。这个垃圾收集器比一个完整的或部分完整的垃圾收 集器扫描得更频繁,因为它更快并且有更短的暂停时间。
- Marksweep + Semispace:非并发的GC,复制GC用于堆转换以及齐性空间压缩(堆 碎片整理)。
3.其他信息
- Objects freed:本次GC从非Large Object Space中回收的对象的数量。
- Size freed:本次GC从非Large Object Space中回收的字节数。
- Large objects freed:本次 GC 从 Large Object Space 中回收的对象的数量。
- Large object size freed:本次 GC 从 Large Object Space 中回收的字节数。
- Heap stats:堆的空闲内存百分比,即(已用内存)/ (堆的总内存)。
- Pause times:暂停时间,暂停时间与在GC运行时修改的对象引用的数量成比例。 目前,ART的CMS收集器仅有一次暂停,它岀现在GC的结尾附近。移动的垃圾 收集器暂停时间会很长,会在大部分垃圾回收期间持续出现。
4.实例分析
I/art : Explicit concurrent mark sweep GC free d 104710(7MB) AllocSpace objects,
21 (416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms
这个GC 日志的含义为引起GC原因是Explicit;垃圾收集器为CMS收集器;释放对 象的数量为104710个,释放字节数为7MB;释放大对象的数量为21个,释放大对象字节 数为416KB;堆的空闲内存百分比为33%,已用内存为25MB,堆的总内存为38MB; GC 暂停时长为1.230ms, GC总时长为67.216ms。
3.DVM 和ART的诞生
虽然DVM和ART的知识体系非常庞大,但是我们仍旧有必要了解DVM是怎么来的。 在2.1.5节中讲过init启动Zygote时会调用app main.cpp的main函数,如下所示:
frameworks/base/cmds/app_process/app_main.cpp
int main(int argc, char* const argv[]){
if (zygote) {//1
runtime.start (ncom.android. internal.os.Zygotelnit1', args, zygote); //2
else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
else {
fprintf(stderr, ''Error: no class name or --zygote supplied. \n");
app_usage();
LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
return 10;
}
}
在注释1处如果为ture,就说明当前程序运行在Zygote进程中,在注释2处调用 AppRuntime 的 start 函数,start 函数具体在 AppRuntime 的父类 AndroidRuntime 中实现,如 下所示:
frameworks/base/core/jni/AndroidRuntime.cpp
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote){
Jnilnvocation jni_invocation;
jni_invocation.Init(NULL);//1
JNIEnv* env;
//启动Java虚拟机
if (startVm(&mJavaVM, &env, zygote) != 0) (//2
return;
}
onVmCreated(env);
//为Java虚拟机注册JNI方法
if (startReg(env) < 0) {//3
ALOGE("Unable to register all android natives\n");
return;
}
}
在注释2处调用startVm函数来创建Java虚拟机,在注释3处调用startReg函数来为 Java虚拟机注册JNI方法。在注释1处调用了 jni invocation的Init函数:
libnativehelper/Jnilnvocation.cpp
bool Jniinvocation::Init(const char* library) (
#ifdef _ANDROID_
char buffer[PROP_VALUE_MAX];
#else
char* buffer = NULL;
#endif
library = GetLibrary(library, buffer);//l
const int KDlopenFlags = RTLD_NOW | RTLD_NODELETE;
handle_ = dlopen(library, kDlopenFlags);//2
if (handle_ == NULL) {
if (strcmp(library, kLibraryFallback) == 0) (
ALOGE("Failed to dlopen %s: %s", library, dlerror());
return false;
}
}
return true;
}
在注释1处调用了 GetLibrary函数:
libnativehelper/Jnilnvocation.cpp
#ifdef ANDROID//1
static char* kLibrarySystemProperty = "persist.sys.dalvik.vm.lib.2”;//2
static char* kDebuggableSystemProperty = "ro.debuggableM;
#endif
static const char* kLibraryFallback = "libart.so";
template<typename T> void UNUSED(const T&) (}
const char* Jnilnvocation::GetLibrary(const char* library, char* buffer) {
#ifdef ANDROID_
const char* default_library;
char debuggable[PROP_VALUE_MAX];
system_property_get(kDebuggableSystemProperty, debuggable);
if (strcmp(debuggable, "1") != 0) {//3
library = kLibraryFallback;//4
default_library = kLibraryFallback;//5
} else {
if (buffer != NULL) {
if (system_property_get(kLibrarySystemProperty, buffer) > 0) {//6
default_library = buffer;
} else {
default_library = kLibraryFallback;
}
} else {
default_library = kLibraryFallback;
}
}
#else
UNUSED(buffer);
const char* default_library = kLibraryFallback;//7
#endif
if (library = NULL) {
library = default_library;//8
}
return library;
}
注释1处代表在Android平台,注释2处的persist.sys.dalvik.vm.lib.2是一个系统属性, 它的取值可以为libdvm.so或者libart.so,值为libdvm.so说明当前用的是DVM,值为libart.so 说明当前用的是ARTO在注释3处如果debuggable不等于”1”,说明当前不是Debug模式 构建的,是不允许动态更改虚拟机动态库的。在注释4和注释5处将libart.so赋值给library 和default library0如果是Debug模式构建会在注释6处读取persist.sys.dalvik.vm.lib.2配置中是否有传入的参数buffer,如果有就将default library赋值为buffer,如果没有将 default library赋值为libart.so。在注释7处如果不是在Android平台,default library的值 为libart.so0在注释8处如果library为NULL就将default !ibrary赋值给library并返回该 library。这里我们知道Android 8.0如果不是Debug模式构建,只能返回libart.so。回到 Jnilnvocation的Init函数,注释1处的GetLibrary函数会返回libart.so或者libdvm.so,接着 在注释2处调用dlopen函数来加载libart.so或者libdvm.so。因此我们知道Jnilnvocation的Init函数的主要作用是初始化ART或者DVM的环境,初始化完毕后会调用startVm函数来 启动相应的虚拟机。讲到这里我们应该知道DVM和ART是如何诞生的了,没错,是在 Zygote进程中诞生的,这样Zygote进程就持有了 DVM或者ART的实例,此后Zygote进 程fork自身创建应用程序进程时,应用程序进程也得到了 DVM或者ART的实例,这样就 不需要每次启动应用程序进程都要创建DVM或者ART,从而加快了应用程序进程的启动 速度。
本章小结
本章介绍了 DVM和ART的基本原理、如何阅读它们的log和DVM,以及ART的诞生。阅读本章前请阅读第2章、第3章和第10章会有利于对本章的理解。DVM和ART的 知识体系完全可以写一本书,如果想要更多地了解它们请阅读专业的书籍和博客,博客推 荐老罗(罗升阳)的博客,里面有一系列文章专门介绍DVM和ART,虽然文章基于的Android 版本有些老,但仍具有参考价值。