前言
本文是介绍Android虚拟机与类加载机制。
目录
一、Android虚拟机
虚拟机是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。虚拟机有自己完善的硬件架构,如处理器、堆栈等,还具有相应的指令系统。
JVM虚拟机是什么
不是真实的物理机,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机。它没有寄存器,所以指令集是使用Java栈来存储中间数据,这样做的目的就是为了保持Java虚拟机的指令集尽量的紧凑,同时也便于JAVA虚拟机在那些只有很少通用寄存器的平台上实现。所以,JVM本质上就是一个程序。
Android虚拟机是什么
是Google公司设计的用于Android平台的虚拟机,根据移动设备的一些特性进行优化,最终形成了Android的虚拟机。Android虚拟机是面向Linux,嵌入式操作系统的虚拟机,主要负责生命周期管理、堆栈管理、线程管理、安全和线程管理,垃圾回收等。它是Android平台的重要组成部分,支持dex格式(Dalvik Executable)的Java应用程序的运行。dex格式是专门为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。从Android系统架构图知,Android虚拟机运行在Android的运行时库层。
android虚拟机特点
首先,android虚拟机体积小、占用内存空间小。移动设备无论是存储空间、内存资源还是电池资源都是有限的。其次,基于寄存器的指令集合使得android虚拟机性能更好、执行更高效。每个进程对应一个虚拟机。最后,android虚拟机支持dex可执行文件格式。
Android 虚拟机与JVM对比
1、Android虚拟机CPU指令基于寄存器的,而JVM基于栈。基于寄存器的虚拟机对于编译后变大的程序来说,在它们执行的时候,花费的时间更短。
2、Android虚拟机执行文件为.dex,而JVM运行java字节码。
3、Dalvik可执行文件体积小。Android SDK中有一个叫dx的工具负责将Java字节码转换为.dex文件。dx工具对Java类文件重新排列,消除在类文件中出现的所有冗余信息,避免虚拟机在初始化时出现反复的文件加载与解析过程。
注:基于栈的指令很紧凑,例如,Java虚拟机使用的指令只占一个字节,因而称为字节码。基于寄存器的指令由于需要指定源地址和目标地址,因此需要占用更多的指令空间。Dalvik虚拟机的某些指令需要占用两个字节。基于栈和基于寄存器的指令集各有优劣,一般而言,执行同样的功能,前者需要更多的指令(主要是load和store指令),而后者需要更多的指令空间。需要更多指令意味着要多占用CPU时间,而需要更多指令空间意味着数据缓冲(d-cache)更易失效。
Dalvik和ART
Android虚拟机分为Dalvik虚拟机和ART虚拟机。DVM本质也是一个Java虚拟机,默认使用CMS垃圾回收器,但是与JVM运行 Class 字节码不同,DVM 执行 Dex(Dalvik Executable Format) ——专为 Dalvik 设计的一种压缩格式。Dex 文件是很多 .class 文件处理压缩后的产物,最终可以在 Android 运行时环境执行。
android虚拟机进化史
android 1.1 Dalvik:解释器。最早的虚拟机,采用的是边编译,边执行。每次执行代码,都需要Dalvik将dex字节码转化为机器指令集合,然后交给CPU去执行。
android 2.2 Dalvik:解释器+JIT(just-in-time)。从android2.2之后,Dalvik虚拟机增加了JIT即时编译技术,主要为了提高android程序的运行效率。执行过程中,每遇到一个新的类别,都会被编译优化成相当精简的原生型指令码,下次执行到相同逻辑的时候,速度就会更快。
android 4.4 ART虚拟机(Android Runtime)
ART采用的是AOT模式,AOT(Ahead-of-time)即在应用安装的时候,dex文件就会被预先编译可执行文件,这个过程就叫做预编译。具体过程,安装APK的时候调用dex2oat,把.dex文件编译oat文件并保存到磁盘中,该文件采用的是成ELF文件格式,该文件格式为native code机器可以直接运行的格式,每次应用启动不用重新编译。所以每次应用启动的时候,启动速度有很大提升,增加了存储空间的使用,也是一种空间换时间的策略,但是安装过程中耗时增加。但是在运行时,执行的都是预选编译好的机器码,所有会快很多。Android 5.0之后系统虚拟机彻底切换为ART虚拟机。
android 7.0 AOT+解释执行+JIT
为了解决在art上的安装时间太长的问题,同时保证在启动APP的时候性能不变,从android7.0开始采用混合模式,即AOT+JIT+解释执行3种模式共存的方式。具体的工作过程如下,首先,在应用安装时dex文件不会被预先编译成机器码。然后,在App运行时,dex文件先通过解释器直接执行,热点函数会被识别并被JIT编译后存储在JIT code cache中并生成profile文件记录热点函数信息。最后当手机进入idle状态或者充电状态,系统扫描app目录下的profile文件进行AOT编译。在这种模式下,无论是首次安装还是APP启动都能够保证很好的效率。
dexopt与dex2aot
dexopt:在Dalvik中虚拟机在加载一个dex文件时,对 dex 文件 进行 验证 和 优化的操作,其对 dex 文件的优化结果 变成了 odex(Optimized dex) 文件,这个文件和 dex 文件很像,只是使用了一些优化操作码。
dex2oat:ART 预先编译机制,在安装时对 dex 文件执行AOT 提前编译操作,编译为OAT(实际上是ELF文件)可执行文件(机器码)。
二、类加载机制
任何一个 Java 程序都是由一个或多个 class 文件组成,在程序运行时,需要将 class 文件加载到 JVM 中才可以使 用,负责加载这些 class 文件的就是 Java 的类加载机制。ClassLoader 的作用简单来说就是加载 class 文件,提供给程序运行时使用。每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。
ClassLoader
是一个抽象类,而它的具体实现类主要有:
- BootClassLoader:用于加载Android Framework层class文件。
- PathClassLoader:用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex
- DexClassLoader:用于加载指定的dex,以及jar、zip、apk中的classes.dex。

1 | public class DexClassLoader extends BaseDexClassLoader { |
可以看到创建 ClassLoader 需要接收一个 ClassLoader parent 参数。这个 parent 的目的就在于实现类加载的双亲委托。
双亲委托机制
某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。
- 避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
- 安全性考虑,防止核心API库被随意篡改。
1 | protected Class<?> loadClass(String name, boolean resolve) |
可以看到在所有父ClassLoader无法加载Class时,则会调用自己的 findClass 方法。 findClass 在ClassLoader中的定义为:
1 | protected Class<?> findClass(String name) throws ClassNotFoundException { |
其实任何ClassLoader子类,都可以重写 loadClass 与 findClass 。一般如果你不想使用双亲委托,则重写 loadClass 修改其实现。而重写 findClass 则表示在双亲委托下,父ClassLoader都找不到Class的情况下,定义自己如何去查找一个Class。而我们的 PathClassLoader 会自己负责加载 MainActivity 这样的程序中自己编写的类,利用双亲委托父ClassLoader加载Framework中的 Activity 。说明 PathClassLoader 并没有重写 loadClass ,因此我们可以来看看PathClassLoader中的 findClass 是如何实现的。
1 | public BaseDexClassLoader(String dexPath, File optimizedDirectory,String |
实现非常简单,从 pathList 中查找class。继续查看 DexPathList
1 | public DexPathList(ClassLoader definingContext, String dexPath, |

三、热修复原理
PathClassLoader 中存在一个Element数组,Element类中存在一个dexFile成员表示dex文件,即:APK中有X个 dex,则Element数组就有X个元素。

在 PathClassLoader 中的Element数组为:[patch.dex , classes.dex , classes2.dex]。如果存在Key.class位于 patch.dex与classes2.dex中都存在一份,当进行类查找时,循环获得 dexElements 中的DexFile,查找到了 Key.class则立即返回,不会再管后续的element中的DexFile是否能加载到Key.class了。
因此实际上,一种热修复实现可以将出现Bug的class单独的制作一份fix.dex文件(补丁包),然后在程序启动时,从 服务器下载fix.dex保存到某个路径,再通过fix.dex的文件路径,用其创建 Element 对象,然后将这个 Element 对 象插入到我们程序的类加载器 PathClassLoader 的 pathList 中的 dexElements 数组头部。这样在加载出现Bug的 class时会优先加载fix.dex中的修复类,从而解决Bug。
具体流程:(注意不同android版本有差异)
- 获取到当前应用的PathClassLoader;
- 反射获取父类BaseDexClassLoader的成员DexPathList对象pathList;
- 反射修改pathList的dexElements。把补丁包的dex文件转化为Element[],将转化得到的数组和获得pathList的dexElements的旧值进行合并,最后反射赋值给pathList的dexElements。