JVM / DVM / ART虚拟机

Posted by chchaooo on January 5, 2019

JVM

Java虚拟机(Java Virtual Machine,缩写为JVM)是一种能够运行Java bytecode的虚拟机 JVM有三个概念:规范,实现和实例。

  • JVM规范是一个正式描述JVM实现所需要的文档,具有单个规范确保所有实现是可互操作的。
  • JVM实现是一种满足JVM规范要求的计算机程序。
  • JVM的实例是在执行编译成Java字节码的计算机程序的过程中运行的实现。

JVM上的垃圾回收机制可以参考这里:

Dalvik

Dalvik是Google专门为Android操作系统开发的虚拟机。它支持.dex(即“Dalvik Executable”)格式的Java应用程序的运行。.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Dalvik由Dan Bornstein编写,名字来源于他的祖先曾经居住过的小渔村达尔维克(Dalvík),位于冰岛。

JVM vs DVM

  • JAVA虚拟机运行的是JAVA字节码,Dalvik虚拟机运行的是Dalvik字节码。JAVA程序经过编译,生成JAVA字节码保存在class文件中,JVM通过解码class文件中的内容来运行程序。而DVM运行的是Dalvik字节码,所有的Dalvik字节码由JAVA字节码转换而来,并被打包到一个DEX(Dalvik Executable)可执行文件中,DVM通过解释DEX文件来执行这些字节码。
  • dex文件格式相对来说更加的紧凑。

jar文件以class为区域进行划分,在连续的class区域中会包含每个class中的常量,方法,字段等等。而dex文件按照类型(例如:常量,字段,方法)划分,将同一类型的元素集中到一起进行存放。这样可以更大程度上避免重复,减少文件大小。 两种文件格式的对比如下图所示: class文件格式:

  • JVM基于栈,DVM基于寄存器
    • 基于栈的架构具有更好的可移植性,因为其实现不依赖于物理寄存器
    • 基于栈的架构通常指令更短,因为其操作不需要指定操作数和结果的地址
    • 基于寄存器的架构通常运行速度更快,因为有寄存器的支撑
    • 基于寄存器的架构通常需要较少的指令来完成同样的运算,因为不需要进行压栈和出栈

Dalvik vs ART

从Android 5.0(Lollipop)开始,Android Runtime(下文简称ART)代替了原先的Dalvik,成为Android系统上新的虚拟机。Dalvik虚拟机是google在2008年跟随Android系统一起发布的。当时的移动设备的系统内存只有64M左右,CPU频率在250~500MHz之间。到如今硬件水平已经有了巨大的提升,智能手机内存已经普遍达到了6G左右,CPU频率也达到了2GHz甚至更高。因此,Dalvik虚拟机被淘汰也是情理之中的事情。

Dalvik之所以要被ART替代包含下面几个原因:

  • Dalvik的JIT执行方式,性能弱于本地机器码的执行
  • Dalvik的垃圾回收机制不够好,会导致卡顿。

ART虚拟机对上面的几点分别做了改进。

  • AOT编译:Ahead-of-time(AOT)是相对于Just-in-time(JIT)而言的。JIT是在运行时要进行字节码到本地机器码的编译,这也是为什么Java普遍被认为效率比C++差的原因(多以个执行过程)。而AOT就是向C++编译过程靠拢的一项技术:当APK在安装的时候,系统会通过一个名称为dex2oat的工具将APK中的dex文件编译成包含本地机器码的oat文件存放下来。这样做之后,在程序执行的时候,就可以直接使用已经编译好的机器码以加快效率。
  • 垃圾回收的改进:
    • 将GC的停顿由2次改成1次
    • 在仅有一次的GC停顿中进行并行处理来减少时间
    • 引入stick CMS来提升垃圾回收的效率
    • 对于后台进程的内存在垃圾回收过程进行压缩以解决碎片化的问题

AOT

由于硬件的发展,硬件资源不再紧缺。ART虚拟机引入AOT机制本质来说,是以空间换时间的方式,来加速App运行,代价是App的存储占用空间会增大10~20%,安装时间也会增长。

下面描述了Dalvik虚拟机和ART虚拟机在安装apk时的区别:

  • 在Dalvik虚拟机上,APK中的Dex文件在安装时会被优化成odex文件,在运行时,会被JIT编译器编译成native代码。
  • 在ART虚拟机上安装时,Dex文件会直接由dex2oat工具翻译成oat格式的文件,oat文件中既包含了dex文件中原先的内容,也包含了已经编译好的native代码。

GC

在Dalvik虚拟机上,垃圾回收会造成两次停顿,第一次需要3~4毫秒,第二次需要5~6毫秒,虽然两次停顿累计也只有约10毫秒的时间,但是即便这样也是不能接受的。因为对于60FPS的渲染要求来说,每秒钟需要更新60次画面,那么留给每一帧的时间最多也就只有16毫秒。如果垃圾回收就造成的10毫秒的停顿,那么就必然造成丢帧卡顿的现象。

因此垃圾回收机制是ART虚拟机重点改进的内容之一。ART 有多个不同的 GC 方案,这些方案包括运行不同垃圾回收器。默认方案是 CMS(Concurrent Mark Sweep,并发标记清除)方案,里面包含多种回收机制:

  • sticky CMS:ART的不移动(non-moving )分代垃圾回收器。它仅扫描堆中自上次 GC 后修改的部分,并且只能回收自上次GC后分配的对象
  • partial CMS:仅仅对应用程序的堆进行垃圾回收,但是不处理Zygote的堆
  • full CMS: 会对应用程序和Zygote的堆都会进行垃圾回收

除CMS方案外,当应用将进程状态更改为察觉不到卡顿的进程状态(例如,后台或缓存)时,ART 将执行堆压缩。与 Dalvik 相比,ART CMS垃圾回收计划在很多方面都有一定的改善:

  • 与Dalvik相比,暂停次数2次减少到1次。Dalvik的第一次暂停主要是为了进行根标记。而在ART中,标记过程是并发进行的,它让线程标记自己的根,然后马上就恢复运行。
  • 与Dalvik类似,ART GC在清除过程开始之前也会暂停1次。两者在这方面的主要差异在于:在此暂停期间,某些Dalvik的处理阶段在ART中以并发的方式进行。这些阶段包括 java.lang.ref.Reference处理、系统弱引用清除(例如,jni全局弱引用等)、重新标记非线程根和卡片预清理。在ART暂停期间仍进行的阶段包括扫描脏卡片以及重新标记线程根,这些操作有助于缩短暂停时间。
  • 相对于Dalvik,ART GC改进的最后一个方面是粘性 CMS回收器增加了GC吞吐量。不同于普通的分代GC,粘性 CMS 不会移动。年轻对象被保存在一个分配堆栈(基本上是 java.lang. Object 数组)中,而非为其设置一个专用区域。这样可以避免移动所需的对象以维持低暂停次数,但缺点是容易在堆栈中加入大量复杂对象图像而使堆栈变长。

JIT的回归

在Android 5.0上,系统在安装APK时会直接将dex文件中的代码编译成机器码。我们应该知道,编译的过程是比较耗时的。因此,用过Android 5.0的用户应该都会感觉到,在这个版本上安装应用程序明显比之前要慢了很多。

编译一个应用程序已经比较耗时,但如果系统中所有的应用都要重新编译一遍,那等待时间将是难以忍受的。但不幸的事,这样的事情却刚好发生了之所以发生这个问题,是因为:

  • 应用程序编译生成的OAT文件会引用Framework中的代码。一旦系统发生升级,Framework中的实现发生变化,就需要重新修正所有应用程序的OAT文件,使得它们的引用是正确的,这就需要重新编译所有的应用
  • 出于系统的安全性考虑,自2015年8月开始,Nexus设备每个月都会收到一次安全更新

要让用户每个月都要忍受一次这么长的等待时间,显然是不能接受的。由此我们看到,单纯的AOT编译存在如下两个问题:

  • 应用安装时间过长
  • 系统升级时,所有应用都需要重新编译

因此,为了解决上面提到的这些问题,在 Android 7.0 中,Google又为Android添加了即时 JIT 编译器。JIT和AOT的配合,是取两者之长,避两者之短:在APK安装时,并不是一次性将所有代码全部编译成机器码。而是在实际运行过程中,对代码进行分析,将热点代码编译成机器码,让它可以在应用运行时持续提升 Android 应用的性能。从Android 7.0开始,ART组合使用了AOT和JIT。并且这两者是可以单独配置的。例如,在Pixel设备上,相应的配置如下:

  • 最初在安装应用程序的时候不执行任何AOT编译。应用程序运行的前几次都将使用解释模式,并且经常执行的方法将被JIT编译。
  • 当设备处于空闲状态并正在充电时,编译守护进程会根据第一次运行期间生成的Profile文件对常用代码运行AOT编译。
  • 应用程序的下一次重新启动将使用Profile文件引导的代码,并避免在运行时为已编译的方法进行JIT编译。在新运行期间得到JIT编译的方法将被添加到Profile文件中,然后被编译守护进程使用。

Reference