JVM、DVM、ART的区别
Android底层学习必须要了解一下java的虚拟机JVM,以及Android的虚拟机DVM(Dalvik VM) 以及ART。
实际上Android的DVM以及ART都不是JVM的一种,因为它们没有遵从JVM的规范。
废话不多说,先分析一下JVM
JVM
JVM是整个java平台的基石,是java语言编译代码的运行平台。
我们先来看看JVM的结构(面试必问:JVM内存模型)
可以看出JVM结构包含运行时数据区,执行引擎,本地方法库,本地方法接口组成。其中类加载子系统并不属于JVM虚拟机的内部结构。
1.类的生命周期
Java文件被编译后生成Class文件,这种二进制格式文件不依赖于特定硬件和操作系统。每一个Class文件都对应着唯一的类或者接口定义信息。无论任何语言只要能编译成Class文件,都能被Java虚拟机识别并执行。
一个Java文件被加载到Java虚拟机内存中到从内存中卸载的过程被称为类的生命周期。类的生命周期包括:加载、链接、初始化、使用、卸载,其中链接包括了三个阶段:验证,准备,解析。因此类的生命周期包括了7个阶段。
接下来大致介绍一下各个阶段的工作:
1)加载:查找并加载Class文件。
2)链接:包括验证、准备、和解析。
验证:确保被导入类型的正确性
准备:为类的静态字段分配字段,并使用默认值初始化这个字段
解析:虚拟机将常量池内的符号引用替换为直接引用
3)初始化:将类变量初始化为正确的初始值
其他的就是使用和卸载,不必多说。
2.运行时数据区域
也就是面试常问的JVM内存模型,主要分为程序计数器,栈,堆,方法区和本地方法区。
1)程序计数器:
为保证程序能够连续的执行下去,处理器必须具有某些手段来确定下一条指令的地址。程序计数器就起到了这个作用。程序计数器(Program Counter Register)也叫作PC寄存器,是一块较小的内存空间。在虚拟机概念模型中,字节码解释器的工作就是通过改变程序计数器来选取下一条需要执行的字节码指令。Java虚拟机的多线程是通过轮流切换并分配处理器执行时间的方式来实现的,在一个确定的时刻只有一个处理器执行一条线程中的指令,为了线程在切换后能回复到正确的执行为止,每个线程都会有一个独立的程序计数器。因此,程序计数器是线程私有的。
程序计数器是Java虚拟机规范中唯一没有规定OutOfMemoryError情况的数据区域。
2)Java虚拟机栈:
每一条Java虚拟机线程都有一个线程私有的Java虚拟机栈(Java Virtual Machine Stacks)。它的生命周期与线程相同。Java虚拟机栈储存线程中的Java方法调用的状态,包括局部变量、参数、返回值以及运算的中间结果。一个Java虚拟机栈包括多个栈帧,一个栈帧用来存储局部变量表、操作数栈、动态链接、方法出口等信息。当线程调用一个Java方法的时候,虚拟机压入一个新的栈帧到该线程的Java虚拟机栈中,当该方法执行完成后,这个栈帧就从java虚拟机栈中弹出。
Java虚拟机栈规定了两种异常情况
1:如果线程请求分配的栈容量超过Java虚拟机所允许的最大容量,会抛出StackOverflowError。
2:如果Java虚拟机栈可以动态扩展,但是无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的Java虚拟机栈,则会抛出OutOfMemoryError。
3)本地方法栈
Java虚拟机实现可能需要C Stacks来支持Native语言,这个C Stacks就是本地方法栈(Native Method Stack)。与Java虚拟机栈累死,只不过本地方法栈是用来支持Native方法的。
4)Java堆
Java堆(Java Heap)是被所有线程共享的运行时内存区域。Java堆用来存放对象实例,几乎所有的对象实例都会在这里分配内存。Java堆存储的对象被垃圾收集器管理,这些受管理的对象无法显式的销毁。
5)方法区
方法区(Method Area)是被所有线程共享的运行时内存区域。用来存储已经被Java虚拟机加载的类的结构信息,包括运行时常量池、字段、方法信息、静态变量等数据。方法区是Java堆的逻辑组成部分,它一样在物理上不需要连续,并且可以选择在方法区中不实现垃圾收集。
Dalvik虚拟机
Dalvik虚拟机(Dalvik Virtual Machine),简称Dalvik VM或者DVM。
为什么开篇说DVM不属于JVM的范畴,下面给出答案。
DVM与JVM的区别
1)基于的架构不同
JVM基于栈,意味着需要去栈中读写数据,所需要的指令会更多,这样会导致速度变慢,对于性能有限的移动设备显然不合适。DVM是基于寄存器的,它没有基于栈的虚拟机在复制数据时而使用大量的出入栈指令,同事指令更紧凑、简洁。但是由于显式的制定了操作数,所以基于寄存器的指令会比基于栈的指令要大。
2)执行字节码不同
JVM中,java类被编译成一个或多个.class文件,并打包成.jar文件,而后JVM会通过相应的.class文件和.jar文件获取相应的字节码。
而DVM会用dx工具把所有的class文件打包成一个.dex文件,然后DVM会从该.dex文件中读取指令和数据。这个.dex文件将所有的.class文件里面所包含的信息全部整合到了一起,这样加载就加快了速度。
3)DVM允许在有限的内存中同时运行多个进程
DVM经过优化,允许在有限的内存中同时运行多个进程。在Android中每一个应用都运行在一个DVM实例中,每一个DVM实例都运行在一个独立的进程空间,独立的进程可以防止虚拟机崩溃时所有程序都被关闭。
4)DVM由Zygote创建并初始化
在《Android系统启动流程》中提到过,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),这样在下次执行到相同的逻辑时,直接使用编译好的机器码即可。需要注意的是,应用程序每次重新运行的时候,都需要重做这个编译工作。
ART虚拟机
ART(Android Runtime)虚拟机是Android 4.4发布的,用来替换Dalvik虚拟机。Android 4.4默认还是DVM,但是可以在开发者选项中切换成ART。在Android 5.0之后默认采用了ART,从此DVM退出了历史舞台。
ART与DVM的区别
1)前文了解到,DVM中的应用每次运行时,字节码都需要通过JIT编译器编译为机器码,这样会使应用程序的运行效率降低。而在ART中,系统安装应用程序时会进行一次AOT(ahead of time compilation),将字节码预编译成机器码并存储在本地,这样应用程序每次运行时就不需要执行编译了,会大大增加效率。但是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不同。
下面主要讲一下ART对垃圾回收机制的改进:
ART垃圾回收
DVM的垃圾回收算法采用的是标记-清除算法(Mark-Sweep),ART改进了该算法,并且使用了多种垃圾收集器。
1.Concurrent Mark Sweep(CMS):CMS收集器是一种获取最短收集暂停时间为目标的收集器,采用了标记-清除算法实现。它是完整的堆垃圾收集器。
2.Concurrent Partial Mark Sweep:部分完整的堆垃圾收集器
3.Concurrent Sticky Mark Sweep:粘性收集器,基于分代垃圾收集思想,它只能释放上次GC以来分配的对象。这个垃圾收集器比一个完整或者部分完整的垃圾收集器扫描的更加频繁,因为它更快而且暂停时间更短。
4.Marksweep + Semispace:肺病发的GC,复制GC用于堆转换以及齐性空间压缩(碎片整理)。
总结
DVM和ART知识体系完全可以写一本书,如果想要更多的了解它请阅读专业的书籍和博客,比如老罗的博客。