目录
  1. 1. ART 和 Dalvik
    1. 1.1. DVM 和 JVM 的区别
    2. 1.2. DVM的运行时堆
    3. 1.3. ART 与 DVM 的区别
    4. 1.4. Dexopt 和 DexAot
  2. 2. ClassLoader —— Java
    1. 2.1. 类加载的基本机制和过程
    2. 2.2. 双亲委托机制
    3. 2.3. 双亲委托机制的优点
    4. 2.4. 理解ClassLoader
  3. 3. ClassLoader —— Android
    1. 3.1. ClassLoader类型(系统类 和 自定义类)
重拾Android-Java进阶之深入理解ClassLoader类加载

第七天 深入理解ClassLoader类加载

ART 和 Dalvik

什么是Dalvik: Dalvik是谷歌公司自己设计用于Android平台的Java虚拟机。支持已转换为.dex(Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik应用设计的一种压缩格式,适合内存和处理器速度有限的系统。 **
DVM架构
**
什么是ART: Android Runtime,Android4.4中引入的 一个开发者选项,也是Android5.0及更高版本的默认模式(Dalvik从此退出历史舞台)。在应用安装的时候Ahead-Of-Time(AOT)预编译字节码到机器语言,这一机制叫Ahead-Of-Time(AOT)预编译。应用程序安装会变慢,但是执行将更有效率,启动更快。 * 在Dalvik下,应用运行需要解释执行,常用热点代码通过即时编译器(JIT)将字节码转换为机器码,运行效率低。而在ART环境中,应用在安装时,字节码预编译(AOT)成机器码,安装慢了,但运行效率会提高。 * ART占用空间比Dalvik大(字节码变为机器码),以“空间换时间”。 * 预编译也可以明显改善电池续航,因为应用程序每次运行时不用重复编译了,从而减少了CPU的使用频率,降低了能耗。 DVM是一个Java虚拟机吗?

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文件

 <center><a href="https://sm.ms/image/6MuT7FmaS5gBDdX" target="_blank"><img src="https://i.loli.net/2021/01/25/6MuT7FmaS5gBDdX.jpg"  width = "500" height = "600"></a></center>
<!-- ![执行的字节码不同.jpg](https://i.loli.net/2021/01/25/6MuT7FmaS5gBDdX.jpg) -->

如上图所示,.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创建和初始化

    Zygote是一个DVM进程,同时也是用来创建和初始化DVM实例的。每当系统需要创建一个应用程序时,Zygote就会fork自身,快速地创建和初始化一个DVM实例,用于应用程序的运行。对于一些只读的系统库,所有的DVM实例都会和Zygote共享一块内存区域,节省了内存开销。

  • 5、DVM 有共享机制

    DVM 拥有预加载——共享的机制,不同的应用之间在运行时可以共享相同的类,拥有更高的效率。而JVM机制不存在这种共享机制,不同的程序,打包以后的程序都是彼此独立的,即便它们在包里使用了同样的类,运行时也都是单独加载和运行的,无法进行共享。

  • 6、DVM 早期没有使用JIT编译器

    JVM使用了JIT编译器(Just In Time Compiler, 即时编译器),而DVM早期没有使用JIT编译器。早期的DVM每次执行代码,都需要通过解释器将dex代码编译成机器码,然后交给系统处理,效率不是很高。为了解决这一问题,从Android 2.2版本开始DVM使用了JIT编译器,它会对多次运行的代码(热点代码)进行编译,生成相当精简的本地机器码(Native Code),这样在下次执行到相同逻辑的时候,直接使用编译之后的本地机器码,而不是每次都需要编译。需要注意的是,应用程序每一次重新运行的时候,都要重做这个编译工作,因此每次重新打开应用程序,都需要JIT编译。

DVM的运行时堆

DVM的运行时堆使用 标记——清除 算法来进行GC,它由两个Space以及多个辅助数据结构组成,两个Space分别是 Zygote Space(Zygote Heap)Allocation Space(Active Heap)。Zygote Space 用来管理Zygote进程在启动过程中预加载和创建的各种对象,Zygote Space中不会触发GC,在Zygote进程和应用程序之间会共享Zygote Space。在Zygote进程fork第一个子进程之前,会把Zygote Space分为两个部分,原来的已经被使用的那部分堆仍叫Zygote Space,而未使用的那部分堆就叫Allocation Space,以后的对象都会在Allocation Space上进行分配和释放。Allocation Space 不是进程间共享的,在每个进程中都独立拥有一份。除了这两个Space,还包含以下数据结构。

  • Card Table:用于DVM Concurrent GC,当第一次进行垃圾标记后,记录垃圾信息。

  • Heap Bitmap:有两个Heap Bitmap,一个用来记录上次GC存活的对象,另一个用来记录这次GC存活的对象。

  • Mark Stack:DVM的运行时堆使用标记—清除(Mark-Sweep)算法进行GC,Mark Stack就是在GC的标记阶段使用的,它用来遍历存活的对象。

ART 与 DVM 的区别

  • (1)DVM中的应用每次运行时,字节码都需要通过JIT编译器编译为机器码,这会使得应用程序的运行效率降低。而在ART中,系统在安装应用程序时会进行一次AOT(ahead of time compilation,预编译),将字节码预先编译成机器码并存储在本地,这样应用程序每次运行时就不需要执行编译了,运行效率会大大提升,设备的耗电量也会降低。这就好比我们在线阅读漫画,DVM 是我们阅读到哪就加载哪,ART 则是直接加载一章的漫画,虽然一开始加载速度有些慢,但是后续的阅读体验会很流畅。采用AOT也会有缺点,主要有两个:第一个是AOT会使得应用程序的安装时间变长,尤其是一些复杂的应用;第二个是字节码预先编译成机器码,机器码需要的存储空间会多一些。为了解决上面的缺点,Android 7.0版本中的ART加入了即时编译器JIT,作为AOT的一个补充,在应用程序安装时并不会将字节码全部编译成机器码,而是在运行中将热点代码编译成机器码,从而缩短应用程序的安装时间并节省了存储空间。

*(2)DVM是为32位CPU设计的,而ART支持64位并兼容32位CPU,这也是DVM被淘汰的主要原因之一。

*(3)ART对垃圾回收机制进行了改进,比如更频繁地执行并行垃圾收集,将GC暂停由2次减少为1次等。

*(4)ART的运行时堆空间划分和DVM不同。

Dexopt 和 DexAot

ART机制:在安装时首先对dex文件进行Dexopt验证和优化,转化为odex文件,再进行AOT提前预编译操作,编译为AOT可执行文件(机器码)同时兼容Dalvik

Dalvik VM:安装时不处理,在运行时通过JIT进行解释执行,其解释执行的文件为 dexopt进行验证和优化过后的odex(Optimized dex)文件

ClassLoader —— Java

类加载的基本机制和过程

Java运行时,会根据类的完全限定名寻找并加载类,寻找的方式基本就是在系统类和指定的类路径中寻找,如果是class文件的根目录,则直接查看是否有对应的子目录及文件;如果是jar文件,则首先在内存中解压文件,然后再查看是否有对应的类。

类加载器ClassLoader就是加载其他类的类,它负责将字节码文件加载到内存,创建Class对象。负责加载类的类就是类加载器,它的输入是完全限定的类名,输出是Class对象。一般程序运行时,类加载有三个:

  • 启动类加载器(Bootstrap ClassLoader)

    C/C++代码实现的加载器,用于加载指定的JDK的核心类库,比如java.lang.、java.uti.等这些系统类。它用来加载以下目录中的类库:

    ■ $JAVA_HOME/jre/lib目录。

    ■ -Xbootclasspath参数指定的目录。

    Java 虚拟机的启动就是通过Bootstrap ClassLoader 创建一个初始类来完成的。由于Bootstrap ClassLoader是使用C/C++语言实现的,所以该加载器不能被Java代码访问到。需要注意的是,Bootstrap ClassLoader并不继承java.lang.ClassLoader。

  • 拓展类加载器(Extensions ClassLoader)

    Java中的实现类为ExtClassLoader,因此可以简称为ExtClassLoader,它用于加载Java的拓展类,提供除了系统类之外的额外功能。ExtClassLoader用来加载以下目录中的类库:

    ■ 加载$JAVA_HOME/jre/lib/ext目录。

    ■ 系统属性java.ext.dir所指定的目录。

  • 应用程序类加载器(Application ClassLoader)

    Java中的实现类为AppClassLoader,因此可以简称为AppClassLoader,同时它又可以称作System ClassLoader(系统类加载器),这是因为AppClassLoader可以通过ClassLoader的getSystemClassLoader方法获取到。它用来加载以下目录中的类库:

    ■ 当前程序的Classpath目录。

    ■ 系统属性java.class.path指定的目录。

这三个类加载器有一定的关系,但不是父子继承关系,而是父子委派关系,子ClassLoader有一个变量parent指向父ClassLoader,在子ClassLoader加载类时,一般会首先通过父ClassLoader加载,具体来说,在加载一个类时,基本过程是:

  • 1)判断是否已经加载过了,加载过了,直接返回Class对象,一个类只会被一个ClassLoader加载一次。

  • 2)如果没有被加载,先让父ClassLoader去加载,如果加载成功,返回得到的Class对象。

  • 3)在父ClassLoader没有加载成功的前提下,自己尝试加载类。

这个过程一般称作“双亲委派”模型,即优先让父ClassLoader去加载。为什么要先让父ClassLoader去加载呢?

【答】这样可以避免Java类库被覆盖的问题。比如,用户程序也定义了一个类java.lang.String,通过双亲委派,java.lang.String只会被 BootClassLoader加载,避免自定义的String覆盖Java类库的定义。

双亲委托机制

当某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才会自己去加载。

双亲委托模式

系统源码摘抄如下:

protected Class<?> loadclass(String name, boolean resolve) throws ClassNotFoundException {

//检查class是否有被加载
class c = findLoadedClass(name);
if(c == null){
long t0 = system.nanoTime();
try{
if(parent!=null){
//如果parent不为null,则调用parent的loadClass进行加载
c = parent.loadClass(name, false);
}else{
//parent为null,则调用BootClassLoader进行加载
c = findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){

}

if(c == null){
//如果都查询不到,那么就自己查找
long t1 = System.nanoTime();
c = findClass(name);
}
}

return c;
}

双亲委托机制的优点

  • 避免重复加载,如果已经加载过一次Class,就不需要再次加载,而是直接读取已经加载的Class。

  • 更加安全,如果不使用双亲委托模式,就可以自定义一个String 类来替代系统的String类,这显然会造成安全隐患,采用双亲委托模式会使得系统的String类在Java虚拟机启动时就被加载,也就无法自定义String类来替代系统的String类,除非我们修改类加载器搜索类的默认算法。还有一点,只有两个类名一致并且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类,想要骗过Java虚拟机显然不会那么容易。

理解ClassLoader

类ClassLoader是一个抽象类,每个Class对象都有一个方法,可以获取实际加载它的ClassLoader,方法是:

public ClassLoader getClassLoader() {}

ClassLoader有一个方法,可以获取它的父ClassLoader:

public final ClassLoader getParent(){}

如果ClassLoader是Bootstrap ClassLoader,那么返回值是null,比如:

public class ServiceA {

public static void main(String[] args) {
ClassLoader classLoader = ServiceA.class.getClassLoader();
while (classLoader != null) {
System.out.println(classLoader.getClass().getName());
classLoader = classLoader.getParent();
}
System.out.println(String.class.getClassLoader());
}
}

输出为:

sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
null

ClassLoader有一个静态方法,可以获取默认的系统类加载器

public static ClassLoader getSystemClassLoader()

ClassLoader 有一个主要方法,用于加载类:

public Class<?> loadClass(String name) throws ClassNotFoundException

示例如下:

ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
try {
Class<?> cls = systemClassLoader.loadClass("java.util.ArrayList");
ClassLoader classLoader = cls.getClassLoader();
System.out.println(classLoader);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

需要说明的是,由于委派机制,Class的getClassLoader方法返回的不一定是调用loadClass的ClassLoader,比如上面代码中,java.util.ArrayList实际由BootStrap ClassLoader加载,所以返回值就是null。

ClassLoader的loadClass和Class的forName方法都可以加载类,它们有何区别?

【答】基本都是一样的,不过ClassLoader的loadClass不会执行类的初始化代码,而Class的forName,默认initialize为true是执行类的初始化的(如static语句块)

分下源码:

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查类是否已经被加载了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
// 没被加载,先委派父ClassLoader或BootStrap ClassLoader去加载
try {
if (parent != null) {
// 委派父ClassLoader,resolve参数固定为false
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 没找到,捕获异常,以便尝试自己加载
}

if (c == null) {
long t1 = System.nanoTime();
// 自己去加载,findClass才是当前ClassLoader的真正加载方法
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
// 链接,执行static语句块
resolveClass(c);
}
return c;
}
}

参数resolve类似Class.forName中的参数initialize,可以看出,其默认值是false,及时通过自定义ClassLoader重写loadClass,设置resolve为true,它调用父ClassLoader的时候,传递的也是固定的false。

ClassLoader —— Android

ClassLoader类型(系统类 和 自定义类)

  • BootClassLoader:

    Android系统启动时会使用BootClassLoader来预加载常用类,与JDK中的 Bootstrap ClassLoader不同,它并不是C/C++代码实现的,而是由Java实现的。BootClassLoader是ClassLoader的内部类,并继承自ClassLoader。BootClassLoader是一个单例类,需要注意的是BootClassLoader的访问修饰符是默认的,只有在同一个包中才可以访问,因此我们在应用程序中是无法直接调用的。【BootClassLoader是在Zygote进程的Zygote入口方法中被创建的,用于加载preloaded-classes文件中存有的预加载类。】

  • DexClassLoader:

    DexClassLoader可以加载dex文件以及包含dex的压缩文件(apk和jar文件),不管加载哪种文件,最终都要加载dex文件。

    DexClassLoader的构造方法有如下4个参数。

    • dexPath:dex相关文件路径集合,多个路径用文件分隔符分隔,默认文件分隔符为“:”。

    • optimizedDirectory:解压的dex文件存储路径,这个路径必须是一个内部存储路径,在一般情况下,使用当前应用程序的私有路径:/data/data/<Package Name>/…。

    • librarySearchPath:包含C/C++库的路径集合,多个路径用文件分隔符分隔,可以为null。

    • parent:父加载器。

      DexClassLoader 继承自BaseDexClassLoader,方法都在BaseDexClassLoader中实现。

  • PathClassLoader:

    Android系统使用PathClassLoader来加载系统类和应用程序的类。PathClassLoader继承自BaseDexClassLoader,也都在BaseDexClassLoader中实现。在PathClassLoader的构造方法中没有参数 optimizedDirectory,这是因为 PathClassLoader 已经默认了参数 optimizedDirectory 的值为 /data/dalvik-cache,很显然 PathClassLoader无法定义解压的dex文件存储路径,因此 PathClassLoader 通常用来加载已经安装的apk的dex文件(安装的apk的dex文件会存储在 /data/dalvik-cache中)。

打赏
  • 微信
  • 支付宝

评论