前言

笔者在准备面试的过程中,更多接触的是JVM虚拟机相关问题,Android虚拟机接触的较少,但是前几天在面试过程中,遇到了相关Android虚拟机的问题,因为缺乏相关方面的知识,导致一时回答并不理想,所以笔者希望借此文进行学习和复盘

一、Android虚拟机

虽然Android程序是使用Java语言开发的,当然,现在也可以使用kotlin语言。但是实际上我们开发出来的Android程序并不能运行在JVM上,而是只能运行在一个类似JVM的Android虚拟机上。Android虚拟机有两种,分别是Dalvik虚拟机和ART虚拟机。

分类

Android虚拟机大致分为以下两类

  • Dalvik,在Android 5.0版本前居多
  • ART, 在Android 5.0版本后正式全面引进

演变历程

  • Android 1.0,使用Dalvik作为Android虚拟机运行环境。
  • Android 2.2,Google在Andriod虚拟机中加入了JIT编译器(Just-In-Time Compiler)。
  • Android 4.4,Google带来了全新的虚拟机运行环境ART,此时ART和Dalvik是共存的,用户可以在两者之间进行选择。
  • Android 5.0,ART全面取代了Dalvik成为了Android虚拟机运行环境,至此Dalvik退出历史舞台。

二、Dalvik

1.概念

关于Dalvik虚拟机的概念,可以查看下面这段话

Dalvik是Google公司自己设计用于Android平台的虚拟机。它可以支持已转换为 .dex(即Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Dalvik经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。

观察以上这段话,同时结合一些博主的观点,笔者对DVM虚拟机进行整理如下:

  • Dalvik虚拟机运行的是Dalvik字节码,Dalvik字节码由Java字节码转换而来,并被打包到一个dex文件中。而JVM运行的是class文件或jar文件。
  • 加载速度快,dex相比于Jar文件会把所有包含的信息整合在一起,减少了冗余信息。这样就减少I/O操作,提高类的查找速度。
  • Dalvik虚拟机是基于寄存器,而JVM是基于栈(操作数栈)。虽然基于寄存器执行效率好,但是可移植性差,难跨平台。
  • Dalvik虚拟机允许在有限的内存中同时运行多个进程,每一个应用都运行在一个Dalvik虚拟机实例中,拥有独立的进程空间。
  • Dalvik虚拟机有共享机制,不同应用之间在运行时可以共享相同的类,拥有更高的效率。

2.Dalvik和JVM的区别

  • Dalvik 基于寄存器,而 JVM 基于栈

  • 基于寄存器的虚拟机对于更大的程序来讲,在它们编译的时候,花费的时间更短。

  • JVM字节码中,局部变量会被放入局部变量表中,继而被压入堆栈供操做码进行运算,固然JVM也能够只使用堆栈而不显式地将局部变量存入变量表中。

  • Dalvik字节码中,局部变量会被赋给65536个可用的寄存器中的任何一个,Dalvik指令直接操做这些寄存器,而不是访问堆栈中的元素。

3.Dalvik和JVM的区别

  • VM字节码由.class文件组成,每一个文件一个class。

  • JVM在运行的时候为每个类装载字节码。相反的,Dalvik程序只包含一个.dex文件,这个文件包含了程序中全部的类。

  • Java编译器建立了JVM字节码以后,Dalvik的dx编译器删除.class文件,从新把它们编译成Dalvik字节码,而后把它们写进一个.dex文件中。这个过程包括翻译、重构、解释程序的基本元素(常量池、类定义、数据段)。

  • 常量池描述了全部的常量,包括引用、方法名、数值常量等。类定义包括了访问标志、类名等基本信息。数据段中包含各类被VM执行的函数代码以及类和函数的相关信息(例如DVM所须要的寄存器数量、局部变量表、操做数堆栈大小),还有实例变量。

4.JIT(Just-In-Time Compile)

早期没有JIT的时候,虚拟机运行时,会通过解释器来解释字节码并将其翻译为机器码,逐条读入,逐条翻译,最后再执行,这样就比较慢,效率不高。针对上面这个问题,引进了JIT(即时编译器)技术。它是一种优化手段

JIT技术简单来说就是将解释过的机器码缓存起来,下次再执行时到这个方法的时候,则直接从缓存里面取出机器码来执行。减少了读取字节码和翻译字节码的操作。以此来提高效率。JIT技术的引入使得Dalvik的性能提升了3~6倍
不过要注意的是并不是所有执行过的代码对应的机器码都会被缓存起来。而是只有被认定为热点代码(Hot Spot Code) 的代码才会。这里所指的热点代码主要有两类,包括:

  • 被多次调用的方法
  • 被多次执行的循环体(虽然只是循环体被多次执行,但仍是将整个方法的机器码缓存起来)

JIT技术虽好,但是也是有缺点的:

  • 每次重新启动引用都需要重新编译
  • 运行时比较耗电

4.dex文件

  • dex是二进制文件,用于在Android虚拟机上执行。是通过把所有的class文件进行合并优化得到的。dex文件去除了class文件中的冗余信息(比如重复字符串),并且结构更加紧凑,因此在dex解析阶段可以减少I/O操作,提高类查找速度。
  • 它与.jar文件不同,.jar文件像是一个文件夹,里面的.class是单独的文件,各个class信息里面会出现重复的信息。而dex文件,则将所有的.class里面的信息整合在一起,去除掉里面的重复数据。
    在这里插入图片描述

但优化过程也会伴随着一些副作用,最经典的就是 Android 65535 问题。关于65536问题的介绍可看后面

5.odex文件

odex是从apk提取出dex文件并通过优化后得到的产物,它被保存到data/dalvik-cache目录下。原apk文件中的classes.dex可以保留也可以删除,甚至有时候会留下残缺的dex文件。
系统在首次启动时,需要对预置的apk进行安装,此时需要将dex从apk文件中解压出来放到data/app文件夹中。

在Dalvik虚拟机中,会通过dexopt来对dex进行优化,生成odex文件,并将其保存到手机的VM缓存文件夹data/dalvik-cache下(注意,这边生成的odex文件后缀依然是dex )。它是一个dey文件,里面仍然还是字节码。

在ART虚拟机上,同样会在首次进入系统的时候使用dexopt工具来对dex进行优化,不过此时的优化是将dex字节码翻译成本地机器码。并保存在data/dalvik-cache下。

一般情况下,在Android系统进行编译的时候,预处理提取Odex文件的话,将会大大优化系统首次启动时间。

6.65536问题

Android 应用 (APK) 文件包含Dalvik 可执行文件 (DEX)文件形式的可执行字节码文件,其中包含用于运行应用的编译代码。Dalvik Executable 规范将单个 DEX文件中可以引用的方法总数限制为 65,536,包括 Android 框架方法、库方法和你自己代码中的方法。由于 65,536 等于 64 X1024,因此此限制称为“64K 参考限制”。

三、ART

3.1概念

ART虚拟机在Android 5.0开始替换Dalvik虚拟机。其处理应用程序执行的方式不同于Dalvik虚拟机,它不使用JIT而是使用了AOT(Ahead-Of-Time),也就是提前编译技术。并且对垃圾收集器也进行了改进和优化,当然也还包括了其他的优化。

在Android官方的五层架构中,ART虚拟机位于第三层,和Native Lib 位于同一层,如图中黄色小方块的位置
在这里插入图片描述

3.2AOT(Ahead-Of-Time)预先编译技术

ART 引入了预先编译机制,可提高应用的性能。ART 还具有比 Dalvik 更严格的安装时验证。

AOT也就是提前编译技术。简单来说就是提前将字节码转换成本地机器码,然后存储在本地磁盘上,运行时可以直接执行,避免了Dalvik时期的应用运行时再来解释字节码。运行时效率大大提高。

在Android 7.0 之前,Android系统安装应用的时候,会进行一次预编译,将字节码预先编译成本地机器码,生成.oat文件,并存储在本地磁盘上,也就是AOT技术。这样在应用每次运行时就不需要重新编译,可以直接使用编译好本地机器码,运行效率大大提升。但是这也使得安装应用的时间大大增加,于是在Android7.0,又重新引进了JIT技术,形成JIT/AOT混合编译模式,这种混合编译的特点是:

  • 应用在安装的时候,不进行AOT预编译。
  • 应用运行时这直接通过解释器翻译字节码为机器码然后执行。(在应用运行期间使用了JIT技术)并同时记录热点代码信息到profile文件中。
  • 手机进入空闲或充电状态的时候,系统会扫描APP目录下的profile文件,并通过AOT对热点代码进行编译。
    下一次启动时,会根据profile文件来运行已编译好的机器码,避免在运行时对已经变过的方法又进行了JIT编译。
    应用运行期间会持续对热点代码进行记录,以方便在空闲或充电时进行AOT,以此循环。

使用了JIT来对AOT进行补充,可以提升运行时性能,节省存储空间,加快应用运行速度。

3.3 垃圾回收方面的优化

关于垃圾回收机制,ART垃圾回收机制思想上和JVM主流虚拟机的垃圾回收机制有许多相似之处,所以没有了解过垃圾回收机制的小伙伴可以选择有限了解一下JVM的垃圾回收机制,再来观看和学习这部分的相关内容

垃圾回收判定

在垃圾回收判定上,ART虚拟机采用的是可达性分析算法,这一点和JVM虚拟机相同

垃圾回收算法

JVM虚拟机上存在四种垃圾回收算法

  • 标记-清除算法
  • 标记-整理算法
  • 标记-复制算法
  • 分代算法

而在ART虚拟机上,要分那种两种情况进行分析(应用位于前台/后台),我们这里优先阐述应用位于前台的场景:

应用位于前台时,Art采用了两种算法,标记-复制算法,标记-整理算法

关于标记-复制算法,其历史和对半复制的雏形模型这里不作过多阐述,本文重点阐述标记-整理算法在ART虚拟机上的应用

标记-复制算法,坏处在于两点:
1.可能缩小内存的实际使用空间
2.特定场景下,可能为了清除几个没用的对象,要移动大量存活对象

为此,ART虚拟机进行了如下优化
ART采用的是该算法优化过后的版本,把内存划分为多个区域(官方说法叫做Region),一个区域大小为256KB,如下图所示。
在这里插入图片描述
在这里插入图片描述
该做法的好处如下:

  • 当一个区域没有垃圾的时候,就可以不进行垃圾清理。
  • 当一个区域因为只有一两个垃圾而要进行垃圾清理的时候,代价也不会太过于高昂,因为一个区域大小才256KB,本来存储的对象就不多,因为一两个垃圾而复制三四个对象还是可以接受的,这就和在家里打扫卫生时因为扫把够不着椅子底下的灰尘,从而把椅子移开后才进行清理一样可以令人接受。

对于图中大方块下方标明的英文名称,我们可以做解释如下:

  • 当一个区域有垃圾,需要被Evacuated的时候,Art则将该区域命名为Evacuated Region。

  • 当一个区域没有垃圾,不需要被Evacuated的时候,Art则将该区域命名为Unevacuated Region。

  • 当一个区域没有存储对象的时候,Art则将该区域命名为Unused Region。

  • 当一个区域原先为Unused Region,但是要作为其它Evacuated Region中存活对象复制目的地的时候,Art则将该区域命名为Evacuation Region。

上图中的对象颜色并不都一样,深绿色是来标明老年代中的存活对象,浅绿色是来标明新生代中的存活对象,红色是来标明待清理的垃圾,此外,老年代和新生代都聚集在各自的区域,并没有出现老年代和新生代混合在一个区域的情况,这样做是有原因的。

关于老年代和新生代的划分,商业虚拟机采用的理论如下:

​ “当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。

2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

​这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。

​ 在Java堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域 ——因而才有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算法”“标记-整理算法”等针对性的垃圾收集算法。这里笔者提前提及了一些新的名词,它们都是本章的重要角色,稍后都会逐一登场,现在读者只需要知道,这一切的出现都始于分代收集理论。

​ 把分代收集理论具体放到现在的商用Java虚拟机里,设计者一般至少会把Java堆划分为新生代 (Young
Generation)和老年代(Old Generation)两个区域。顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。”

​ 同样地,Art也采用了这种分代收集理论,分为Major GC和Full GC(GC为Garbage Collection的简称),在Minor GC中只对新生代进行可达性算法分析,在Full GC中才对新生代和老年代一起进行可达性算法分析。

分代理论存在的问题及ART解决方案

​ 把对象单纯分为新生代和老年代还存在着一个问题,老年代可能持有新生代的引用,而在Minor GC中Art只对新生代进行可达性算法分析,这样可能会导致只被老生代持有的新生代被Art误判为垃圾,举一个栗子,假设有一个老年代X持有了新生代Y的引用,且Y的引用只被X所持有,也就是说,只存在由X出发到Y的路径,那么Art在Minor GC由于不对X进行可达性算法分析,会判定Y不可达,从而误判Y为垃圾,

​ 这就是所谓的跨代引用假说,因此,为了解决这问题,Art引入了Remember Set来记录老年代对新生代的引用。

​ 下面我继续引用《深入理解Java虚拟机》来对跨代引用假说和Remember Set进行介绍。

​ “跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

​ 这其实是可根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。举个例子,如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了。

​ 依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。 ”

ART Full GC

​ 准确来说,Art采用的并不是Full GC算法,因为根据谷歌的说法,Art采用的是经过优化的Full GC算法,全称叫2-phase full-heap GC cycles,但后文为了介绍方便,仍采用Full GC的说法,稍微有点英文基础的读者看到算法的全称就应该知道,该算法分为两阶段,如图所示,第一阶段使用可达性算法分析来判断对象是否存活,第二阶段就是根据区域中的存活对象数量判断是否需要进行Evacuated。(ps:Full GC未优化的版本就包含垃圾判断和垃圾回收)
在这里插入图片描述

Art垃圾回收算法的并发性

上面所介绍的垃圾回收算法具有并发性,也就是说垃圾回收线程是与主线程并发进行的,在一个垃圾回收周期只有一次短暂的GC暂停,时间为几毫秒,所以用户大多数情况下是无法感知的,并不会出现”stop the world“现象。

需要注意的是,这里GC暂停时间笔者理解为垃圾收集时候造成的暂停,也就是说,垃圾收集算法会造成STW现象,但是垃圾回收则不会

相关概念(垃圾收集/垃圾回收)的区分,可以看这篇博客
JVM中的STW和CMS

关于GC暂停的定义可以看这篇文章
GC暂停

ART在应用位于后台时的垃圾回收算法

前面说过Art采用了两种垃圾回收算法。

​ 当应用仍在前台运行,与用户进行交互的时候,Art采用的就是上面所介绍的算法。

​ 而当应用在后台运行时,于用户不可见的时候,Art采用的就是另一种算法,下面简单引用安卓官网的内容进行简单介绍。

​ “ART 仍然支持的另一个 GC 方案是 CMS(并发标记清除)。此 GC 方案还支持压缩,但不是以并发方式。在应用进入后台之前,它会避免执行压缩,应用进入后台后,它会暂停应用线程以执行压缩。如果对象分配因碎片而失败,也必须执行压缩操作。在这种情况下,应用可能会在一段时间内没有响应。”

3.4开发和调试方面的优化

采样分析器

ART 提供了大量功能来优化应用开发和调试。具体如下:

早期Dalvik采用traceview进行性能观测,现在的Android一般采用Profile(CPU性能观测器)进行分析

优化了异常和崩溃报告中的诊断详细信息

当发生运行时异常时,ART 会为您提供尽可能多的上下文和详细信息。ART 会提供 java.lang.ClassCastException、java.lang.ClassNotFoundException 和 java.lang.NullPointerException 的更多异常详细信息。(较高版本的 Dalvik 会提供 java.lang.ArrayIndexOutOfBoundsException 和 java.lang.ArrayStoreException 的更多异常详细信息,这些信息现在包括数组大小和越界偏移量;ART 也提供这类信息。)

ART 还通过纳入 Java 和原生堆栈信息,在应用原生代码崩溃报告中提供更实用的上下文信息。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐