ART 和 Dalvik
什么是Dalvik: Dalvik是谷歌公司自己设计用于Android平台的Java虚拟机。支持已转换为.dex(Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik应用设计的一种压缩格式,适合内存和处理器速度有限的系统。 **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。
更加安全,如果不使用双亲委托模式,就可以自定义一个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 { |
输出为:
sun.misc.Launcher$AppClassLoader |
ClassLoader有一个静态方法,可以获取默认的系统类加载器
public static ClassLoader getSystemClassLoader() |
ClassLoader 有一个主要方法,用于加载类:
public Class<?> loadClass(String name) throws ClassNotFoundException |
示例如下:
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); |
需要说明的是,由于委派机制,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) |
参数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
中)。