JVM、Dalvik、ART

2021-10-12  本文已影响0人  瑜小贤

作为Android开发人员,一直以来都是在把JVM的特点拿来学习,把JVM、Dalvik、ART割裂的单独来看,现在把他们放到一起,去比较他们的区别,但是了解的还很浅显,这里面的学习内容深究下去还是有很多很多的。

JVM

JVM(Java Virtual Machine)是一种虚拟机,用来将由java文件编译成的class字节码文件再编译成机器语言,供机器识别。有了JVM中间人的存在就不需要直接与操作系统打交道,且不同的操作系统有不同的JVM,于是就屏蔽了操作系统间的差异,从而使java成为跨平台语言。

Java的使用流程

其实.jar和.war是.class文件的压缩包,其中还包含了不同的配置文件,使用时通过类加载器取其内部的.class字节码文件加载到JVM。

Class文件结构

JVM接收的最初数据是class字节码文件,由.java文件编译产生。并不是只有java语言可以编译成class文件,其他语言也是可以(Scala、Groovy)。



class文件是由8位为一组的字节为基本单位,构成的二进制文件,为什么是二进制文件呢?

结构如上图,最上方为起始位,内容包含了.java文件的信息。

类型

class文件中只有两种类型:无符号数和表

无符号数为基本类型,有:u1、u2、u4、u8,数字代表字节数。无符号数可以代表数字、索引引用、数量值,或者按照UTF-8编码构成字符串值

表则是由基本类型和表构成的类型,属于组合类型

magic

文件最初的4个字节,称为魔数,是计算机识别文件类型的依据,不同于感官,.word .png .avi这种通过扩展名识别文件类型的方式,计算机识别多数文件是通过文件头部的魔数。

这种做法的优点在于安全性,文件的扩展名可以人为随意的修改,也许并不会造成文件的不可用(早年间的“图种”一词不知多少人有经历),也可能造成文件不可用,但文件的类型在文件创建之初就被赋予魔数的话,就可以大限度的保证文件的安全性。

version

表示此.class的版本信息,有minor_version和major_version两种类型共占了4字节。

不同版本Java有不同的特性,产生的class结构也会不同,JVM通过识别版本从而确定是否可识别此文件。JVM是向下兼容的,如果.class版本过高则不能运行(Unsupported major.minor version **)。

constant

constant_pool为常量池,用来存放常量,constant_pool_count为池中的计数。

constant_pool的索引从1开始,当指针不想引用此constant_pool时则将指针指向0,此操作简单(赋值永远比删除简单)。

常量池中有两大类常量类型:字面量和符号引用

字面量为java中的基本数据类型
符号引用:以一组符号来描述所引用的目标,引用的目标并不一定已经加载到内存中,在类加载过程的解析阶段JVM将常量池内的符号引用替换为直接引用。类型有:

  1. CONSTANT_Class_info 类和接口的全限定名(该名称在所有类型中唯一标识该类型)
  2. CONSTANT_Fieldref_info 字段的名称和描述符
  3. CONSTANT_Methodref_info 方法的名称和描述符

常量池中每一项常量都是一个表


举例:

class A{
    int i=9;
    int b=new B();
}

class B{
}

编译时会产生A.class和B.class,此时A.class有两个常量9和B。JVM加载A.class时,将由于常量9属于字面量即基本数据类型,直接放入常量池。

到常量B时,由于常量B不属于字面量即基本数据类型,所以此时产生一个符号引用来代表常量B。

等到A.class加载到了解析阶段,需要将符号引用改为直接引用,但找不到符号引用B的直接引用,

在使用阶段,由于A对象主动引用了B类,所以JVM通过类加载器开始加载B.class(同样的加载步骤),并创建了B对象,并将符号引用B改为B对象的直接引用。

access

access_flags即java中的类修饰符


类的身份信息

一个类要有类名,关系要有extends和implement。java中类是单继承,所以除Object外所有类都有一个父类,而接口则可以有多实现。

this_class是这个类的全限定名
super_class是这个类父类的全限定名
interfaces是这个类实现接口的集合
interfaces_count是这个类实现接口的数量

fields

fields_count表示fields表中的数量

fields是表结构用来存放字段,字段即为类中声明的变量。字段包括了类级变量或实例级变量,static修饰符为判断依据。

public static final transient String str = "Hello World";

一个字段包含的信息有:

一个字段有多种修饰符,每种修饰符只有两种状态:有、没有,所以采用标志位来表示最为合理。字段的其他信息,叫什么名字、被定义为什么数据类型,这些都是无法固定的,所以引用常量池中的常量来描述。

access_flags:修饰符

name_index:简单名称
指变量名,存放在常量池。例如字段str的简单名称“str”。

descriptor_index:描述符
描述字段的类型


举例:
java.lang.String[][] —— [[Ljava/lang/String
int[] —— [I
String s —— Ljava/lang/String

attributes:属性集合,以用于描述某些场景专有的信息。

上面的类型只定义了变量信息,那变量的初始赋值操作呢?

赋值操作是将常量赋值给变量,常量有字面量和符号引用,字面量会在常量池中,符号引用依据情况会在解析或使用阶段改为直接引用。

字段赋值的时机:
a:对于非静态的field字段的赋值将会出现在实例构造方法<init>()中
b:对于静态的field字段,有两个选择:

1、在类构造方法<cinit>()中进行;
2、使用ConstantValue属性进行赋值
编译器对于静态field字段的初始化赋值策略:

如果final和static同时修饰一个字段,并且这个字段是基本类型或者String类型的,
那么编译器在编译这个字段的时候,会在对应的field_info结构体中增加一个ConstantValue类型的结构体,在赋值的时候使用这个ConstantValue进行赋值;

如果该field字段并没有被final修饰,或者不是基本类型或者String类型,那么将在类构造方法中赋值。

对于全局变量的值是被编译在构造器中赋值的

https://www.cnblogs.com/straybirds/p/8331687.html

methods

methods_count表示methods表中的数量

methods是表结构用来存放方法,表结构和字段的表结构一致


由于部分关键字相对于变量和方法是有区别的

举例:

int indexOf(char[] source,int sourceOffset,int sourceCount,char[] targetOffset,int targetCount,int fromIndex) —— ([CII[CII)I

方法内部的代码存到什么地方了?

attributes:属性表

在字段表和方法表中都有属性表


属性表所能识别的属性有

方法中到具体代码就存放在方法表中属性表的Code属性中

参考 https://blog.csdn.net/sinat_37138973/article/details/54378263

JVM内存模型

参考 https://www.cnblogs.com/dingyingsi/p/3760447.html

在知道了class字节码文件结构和JVM内存模型后,需要一个过程将字节码文件加载到内存。

类加载器

负责将字节码文件载入到内存,

BootstrapClassLoader – JRE/lib/rt.jar
ExtensionClassLoader – JRE/lib/ext或者java.ext.dirs指向的目录
ApplicationClassLoader – CLASSPATH环境变量, 由-classpath或-cp选项定义,或者是JAR中的Manifest的classpath属性定义

从上至下依次为父子关系,并不是继承关系。

类加载机制

类加载方式

自定义类加载器

以上三种类加载器,在某些场景下就不适用。由于以上三种类加载器都是加载指定位置的class,当加载异地加密的class时就无法使用,此时需要自定义类加载器,加载指定位置的class到内存并在执行解密后使用。

有了class字节码文件结构、JVM内存模型和类加载器这三个部分的初识,接着就是三个独立部分合作的场景:类加载过程

类加载过程

类加载器将字节码文件载入JVM内存的过程


参考 https://www.cnblogs.com/dooor/p/5289994.html

一个项目、jar包、war包中有数百成千上万的字节码文件,一个字节码文件只能被加载一次(类加载器的委托机制),JVM是一次性加载全部文件的吗?肯定不是,具体的实现由不同的JVM自由发挥,但对于初始化阶段JVM有明确要求(被引用),自然初始化之前的阶段也必须完成。

类的引用方式

主动引用
  1. 当使用new关键字实例化对象时,当读取或者设置一个类的静态字段(被final修饰的除外)时,以及当调用一个类的静态方法时(比如构造方法就是静态方法),如果类未初始化,则需先初始化。
  2. 通过反射机制对类进行调用时,如果类未初始化,则需先初始化。
  3. 当初始化一个类时,如果其父类未初始化,先初始化父类。
  4. 用户指定的执行主类(含main方法的那个类)在虚拟机启动时会先被初始化。
被动引用

除了上面这4种方式,所有引用类的方式都不会触发初始化,称为被动引用。如:

参考 https://blog.csdn.net/zcxwww/article/details/51330327

初始化的对象会被放在什么地方呢?

内存分配策略

两个存储位置:本地线程缓存TLAB

新对象产生时首先检查本地线程是否开启了缓存,是则存储在TLAB,否则去堆中寻找位置。

堆又分了:Eden两个SurvivorTenured共4个区,Eden与Survivor大小比是8:1,Eden和Survivor称为新生代,Tenured称为老年代(JDK8已经没有持久代了)

当新对象产生时,存放在Eden,当Eden放不下时触发Minor GC,将Eden中存活的对象复制到一Survivor中。继续存放对象到Eden,当Eden放不下时触发Minor GC,将Eden和非空闲Survivor中存活的对象复制到空闲Survivor中,往复操作。每经过一次Minor GC,对象的年龄加1,当对象年龄达到阀值(默认15)进入Tenured。如果在Minor GC期间发现存活对象无法放入空闲的Survivor区,则会通过空间分配担保机制使对象提前进入Tenured。如果在Survivor空间中的相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于和等于该年的对象就可以直接进入老年代,无需等到指定的阀值。

空间分配担保机制
在执行Minor GC前, VM会首先检查Tenured是否有足够的空间存放新生代尚存活对象,由于新生代使用复制收集算法,为了提升内存利用率,只使用了其中一个Survivor作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况时,就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代,但前提是老年代需要有足够的空间容纳这些存活对象。但存活对象的大小在实际完成GC前是无法明确知道的,因此Minor GC前,VM会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小,如果条件成立, 则进行Minor GC,否则进行Full GC(让老年代腾出更多空间)。然而取历次晋升的对象的平均大小也是有一定风险的,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure,老年代也无法存放这些对象了),此时就只好在失败后重新发起一次Full GC(让老年代腾出更多空间)。

分代的唯一理由就是优化GC性能,让GC在固定区域工作。

GC

垃圾回收最重要的一点是如何判断对象为垃圾?

可达性分析算法

通过一系列称为GC Roots的对象作为起点,然后向下搜索,搜索所走过的路径称为引用链/Reference Chain,当一个对象到GC Roots没有任何引用链相连时,即该对象不可达,也就说明此对象是不可用的,如图:Object5、6、7虽然互有关联,但它们到GC Roots是不可达的,因此也会被判定为可回收的对象。

回收时的回收算法:

分代

根据对象存活周期的不同将内存划分为几块,如JVM中的新生代、老年代,这样就可以根据各年代特点分别采用最适当的GC算法:

在新生代:每次垃圾收集都能发现大批对象已死,只有少量存活。因此选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

在老年代:因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记—清理”或“标记—整理”算法来进行回收,不必进行内存复制,且直接腾出空闲内存。

新生代-复制算法

该算法的核心是将可用内存按容量划分为大小相等的两块,每次只用其中一块,当这一块的内存用完,就将还存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉。

这使得每次只对其中一块内存进行回收,分配也就不用考虑内存碎片等复杂情况,实现简单且运行高效。

但由于新生代中的98%的对象都是生存周期极短的,因此并不需完全按照1:1的比例划分新生代空间,所以新生代划分为一块较大的Eden区和两块较小的Survivor区。

老年代-标记清除算法

该算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象(可达性分析), 在标记完成后统一清理掉所有被标记的对象。


该算法会有以下两个问题:

  1. 效率问题:标记和清除过程的效率都不高;
  2. 空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。

老年代-标记整理算法

标记清除算法会产生内存碎片问题,而复制算法需要有额外的内存担保空间,于是针对老年代的特点,又有了标记整理算法。标记整理算法的标记过程与标记清除算法相同,但后续步骤不再对可回收对象直接清理,而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。

分区

将整个堆空间划分为连续的不同小区间,每个小区间独立使用,独立回收。这样做的好处是可以控制一次回收多少个小区间。

在相同条件下,堆空间越大,一次GC耗时就越长,从而产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割为多个小块,根据目标停顿时间,每次合理地回收若干个小区间(而不是整个堆),从而减少一次GC所产生的停顿。

参考 http://www.importnew.com/23035.html

对象逃逸(点到为止)

本该销毁的对象,逃到了它处。

public class A {
    public static Object obj;
    public void globalVariableEscape() {  // 给全局变量赋值,发生逃逸
        obj = new Object();//new的对象本该在栈帧出栈时销毁,但被外部static引用导致进入方法区常量池
    }
    public Object methodEscape() {  // 方法返回值,发生逃逸
        return new Object();//new的对象本该在栈帧出栈时销毁,但被外部方法或线程引用,导致对象只能在外部方法栈帧出栈或线程销毁时被清理
    }
    public void instanceEscape() {  // 实例引用发生逃逸
        b = new B(this); //(示意而已,并不准确)新建B对象时引用了A对象,除B使用外,其余无引用A,此时本可以回收A,但B却引用导致无法回收。循环引用就是A在引用B,导致互相引用都不能被回收。
    }
}

public class B {
    public static Object obj;
    public void instance(A a) {  // 引用传入的实例
        obj = a;
    }
}

对象的引用类型(强软弱虚)

JVM中真正将一个对象判死刑至少需要经历两次标记过程:

第一个过程,可达性分析算法
第二个过程,判断这个对象是否需要执行finalize()方法。

第一次GC时,对象在经历了可达性分析算法被标记后,若对象重写了finalize()方法且没被执行过则会被放入F-Queue队列中,否则回收。
第二次GC时,JVM会有一个优先级比较低的线程去执行队列中对象的finalize()方法,执行只是触发finalize()方法并不会确保方法一定完成,防止死循环或异常等情况导致对象不可被回收,这时第二次标记完成对象被回收。

只有当对象存在引用链连接GC Roots时才确保不会被回收,即对象为强引用。那么有些对象,我们希望在内存充足的情况下不要回收,在内存不足的时候再将其回收掉。如果只有强引用,那这个对象永远都不会被回收。于是有了软引用、弱引用、虚引用的概念。

参考 https://blog.csdn.net/huachao1001/article/details/51547290

类卸载

卸载需要满足三个条件:
1、该类所有的实例已经被回收
2、加载该类的ClassLoder已经被回收
3、该类对应的java.lang.Class对象没有被引用

JVM自带的根类加载器、扩展类加载器和系统类加载器,JVM本身会始终引用这些类加载器,因此条件2不会形成。

而这些类加载器则会始终引用它们所加载的类对象,因此条件3也不会形成。

唯一会被卸载的类只有自定义的类加载器加载的类。

Dalvik【DVM】

  1. Google自己设计的,用于Android平台的虚拟机;

  2. 支持已转化为dex格式的java应用程序运行;而dex是专为Dalvik设计的一种压缩格式

  3. 允许在有限的内存中同时运行多个虚拟机实例,并未每一个Dalvik应用作为一和独立的Linux进程运行;

  4. Android 5.0以后,Google直接删除Dalvik,取而代之的是ART

Dalvik与JVM的区别

1. 基于的架构不同

1.1 JVM基于的栈结构
举例:计算20+7在栈中的计算过程:
1、POP 20
2、POP 7
3、ADD 20, 7, result
4、PUSH result

JVM基于栈则意味着需要去栈中读写数据,所需的指令会更多,这样会导致速度慢,对于性能有限的移动设备,显然不是很适合。

1.2 DVM基于的寄存器结构

基于寄存器的虚拟机,它们的操作数是存放在CPU的寄存器的。没有入栈和出栈的操作和概念。但是执行的指令就需要包含操作数的地址了,也就是说,指令必须明确的包含操作数的地址


//需要明确的制定操作数R1、R2、R3(这些都是寄存器)的地址
ADD R1, R2, R3 ;就一条指令搞定了。

DVM是基于寄存器的,它没有基于栈的虚拟机在拷贝数据而使用的大量的出入栈指令,同时指令更紧凑更简洁。但是由于显示指定了操作数,所以基于寄存器的指令会比基于栈的指令要大,但是由于指令数量的减少,总的代码数不会增加多少。

总结
基于寄存器的虚拟机对于更大的程序来说,在它们编译的时候,花费的时间更短。 JVM字节码中,局部变量会被放入局部变量表中,继而被压入堆栈供操作码进行运算,当然JVM也可以只使用堆栈而不显式地将局部变量存入变量表中。Dalvik字节码中,局部变量会被赋给65536个可用的寄存器中的任何一个,Dalvik指令直接操作这些寄存器,而不是访问堆栈中的元素。

2. 可执行程序的字节码不同

在Java SE程序中,Java类会被编译成一个或多个.class文件,打包成jar文件,而后JVM会通过相应的.class文件和jar文件获取相应的字节码。
执行顺序为: .java文件 -> .class文件 -> .jar文件

而DVM会用dx工具将所有的.class文件转换为一个.dex文件,然后DVM会从该.dex文件读取指令和数据。
执行顺序为:.java文件 –>.class文件-> .dex文件

如上图所示,.jar文件里面包含多个.class文件,每个.class文件里面包含了该类的常量池、类信息、属性等等。当JVM加载该.jar文件的时候,会加载里面的所有的.class文件,JVM的这种加载方式很慢,对于内存有限的移动设备并不合适。

而在.apk文件中只包含了一个.dex文件,这个.dex文件里面将所有的.class里面所包含的信息全部整合在一起了,这样再加载就提高了速度。.class文件存在很多的冗余信息,dex工具会去除冗余信息,并把所有的.class文件整合到.dex文件中,减少了I/O操作,提高了类的查找速度。

dex文件是虚拟机直接执行的文件。但是系统做了一些优化,不直接从apk中提取dex运行启动apk,效率太慢,所以就有了后面的odex、oat文件,我们后续再说

3. DVM允许在有限的内存中同时运行多个进程

DVM经过优化,允许在有限的内存中同时运行多个进程。在Android中的每一个应用都运行在一个DVM实例中,每一个DVM实例都运行在一个独立的进程空间。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。

4. DVM由Zygote创建和初始化

Zygote是虚拟机实例的孵化器,它是一个DVM进程,同时它也用来创建和初始化DVM实例,完成库的加载,预制类库和初始化等操作。每当系统需要创建一个应用程序时,Zygote就会fork自身,快速的创建和初始化一个DVM实例,用于应用程序的运行。对于一些只读的系统库,所有虚拟机实例都和Zygote共享一块内存区域。

5. Dalvik虚拟机有自己的 bytecode, 并非使用 Java bytecode.

DVM架构

DVM的源码位于dalvik/目录下,其中dalvik/vm目录下的内容是DVM的具体实现部分,它会被编译成libdvm.so;dalvik/libdex会被编译成libdex.a静态库,作为dex工具使用;dalvik/dexdump是.dex文件的反编译工具;DVM的可执行程序位于dalvik/dalvikvm中,将会被编译成dalvikvm可执行程序。DVM架构如下图所示。



从上图可以看出,首先Java编译器编译的.class文件经过DX工具转换为.dex文件,.dex文件由类加载器处理,接着解释器根据指令集对Dalvik字节码进行解释、执行,最后交与Linux处理。

Optimized DEX,即优化过的DEX

DVM的运行时堆

DVM的运行时堆主要由两个Space以及多个辅助数据结构组成,两个Space分别是Zygote Space(Zygote Heap)和Allocation Space(Active Heap)。

  1. Zygote Space用来管理Zygote进程在启动过程中预加载和创建的各种对象,Zygote Space中不会触发GC,所有进程都共享该区域,比如系统资源。
  2. Allocation Space是在Zygote进程fork第一个子进程之前创建的,它是一种私有进程,Zygote进程和fock的子进程在Allocation Space上进行对象分配和释放。

除了这两个Space,还包含以下数据结构:

Dalvik虚拟机你需要知道的15个问题

  1. 大部分jvm是基于栈的,而Dalvik是基于寄存器的。
    基于栈的机器必须使用指令来载入栈上数据,或是用指令来操纵数据,因此指令集更为庞大。但是对于寄存器指令而言,又必须指定源地址和目的地址,因此,基于寄存器的jvm单个指令更大。

  2. Dalvik一些特点:
    a)常量池32位索引
    b)默认栈12kb,3个页,每页4kb
    c)默认启动堆2MB,最大值16MB,最小1MB
    d)堆最大支持1024MB
    e)堆和栈的参数可以通过-Xms和-Xmx更改

  3. 所有的android线程都对应一个linux线程。每个Android Dalvik应用程序都运行在自己的沙盒里,不同的应用在不同的进程空间里运行。

  4. Dalvik相当于java的JVM,.NET的CLI,Python、Perl、Ruby的Interpreter。Dalvik定义自己的字节码为VM的指令。

  5. 目前Dalvik支持的功能:
    a).dex文件
    b)Dalvik指令集
    c)J2ME CLDC API
    d)多线程

  6. Dalvik支持的平台有:
    a)基于Unix的系统
    b)Linux
    c)BSD
    d)Mac OSX

  7. Dalvik 虚拟机实现位于 dalvik/目录下,dalvik/vm是虚拟机的实现部分,被编译为libdvm.so,而dalvik/libdex被编译成libdex.a静态库作为dex工具库;dalvik/dexdump是.dex文件的反编译工具。虚拟机的可执行程序位于dalvik/dalvikvm中,将被编译为dalvikvm可执行程序。

  8. Dalvik需要的其他库:
    a)OpenSSl 加密技术
    b)Zlib 免费的一般目的数据压缩库
    c)ICU 字符编码技术
    d)java包 包括java.nio,java.lang,java.util
    e)Apache Harmony classlibApache HttpClient

  9. Dalvik虚拟机的运行库大部分是用可移植的C写的,除了JNI call bridge。

  10. Dalvik不遵循java SE和java ME的API规范,所以不支持AWT或者Swing。

  11. dalvik/vm/Dvm.mk 中会根据dvm_arch来选择编译的目标集体系结构。

  12. dx工具:位于dalvik/dx目录,用于将字节码转换成.dex。
    例:dx --dex --output=helloworld.dex helloworld.class

  13. dexdump工具:位于dalvik/dexdump目录,用于反编译dex文件。

  14. dex数据类型:
    byte 8bit
    ubyte 8bit
    short 16bitlittle-endian
    ushort 16bit little-endian
    int 32bitlittle-endian
    uint 32bitlittle-endian
    long 64bitlittle-endian
    ulong 64bitlittle-endian
    sleb128 LEB128 variable-lengtha
    uleb128 LEB128 variable-lengtha
    uleb128p1 LEB128 variable-lengtha
    LEB128类型:1~5个字节组成。所有字节组合在一起代表一个32位值。除最后一个字节最高标志位为0外,其他都为1,剩下的7位为有效负荷。有符号的LEB128的符号由最后一个字节的有效负荷最高位决定。具体算法在:dalvik/libdex/LEB128.h。

  15. dex文件被映射到DexMapList,结构体定义在dalvik/libdex/DexFile.h© 中。

ART虚拟机

  1. Android 2.2 使用DVM虚拟机
  2. Android 4.4 推出ART虚拟机
  3. Android 5.0 全面替换ART
  4. Android7.0 仍是ART环境,但是编译策咯改为AOT 和 JIT 混合编译
    4.1 首次安装 不再统一执行dex2oat
    4.2 改由程序运行时根据实际情况来决定哪些部分被编译成本地代码,会采用JIT混合编译
    4.3 然后在满足两个条件的时候在执行AOT优化。
    4.4 集合了AOP和JIT的特点,使得安装速度,运行速度,储存空间和耗电等指标都得到了优化

ART(Android Runtime)是Android 4.4发布的,用来替换Dalvik虚拟,Android 4.4默认采用的还是DVM,系统会提供一个选项来开启ART。在Android 5.0时,默认采用ART,DVM从此退出历史舞台。

ART与DVM的区别

DVM

ART

ART的运行时堆

与DVM的GC不同的是,ART的GC类型有多种,主要分为Mark-Sweep GC和Compacting GC。ART的运行时堆的空间根据不同的GC类型也有着不同的划分,如果采用的是Mark-Sweep GC,运行时堆主要是由四个Space和多个辅助数据结构组成,四个Space分别是Zygote Space、Allocation Space、Image Space和Large Object Space。Zygote Space、Allocation Space和DVM中的作用是一样的。Image Space用来存放一些预加载类,Large Object Space用来分配一些大对象(默认大小为12k)。其中Zygote Space和Image Space是进程间共享的。

采用Mark-Sweep GC的运行时堆空间划分如下图所示。


除了这四个Space,ART的Java堆中还包括两个Mod Union Table,一个Card Table,两个Heap Bitmap,两个Object Map,以及三个Object Stack。如果想要跟多的了解它们,请参考ART运行时Java堆创建过程分析 – 罗升阳这篇文章

OAT & ART

5.0全面替换ART虚拟机,使用AOT编译模式,dex文件经过dex2oat编译,会生成.art、.oat两个文件

vdex

google在android8.0新增加了vdex文件,通过package直接转化的可执行二进制文件。

smali文件

虚拟机有自己的一套指令集,汇编语言,反编译dex文件就可以得到smali文件, smali文件就是寄存器语言

ART的运行原理:

  1. 在Android系统启动过程中创建的Zygote进程利用ART运行时导出的Java虚拟机接口创建ART虚拟机。
  2. APK在安装的时候,打包在里面的classes.dex文件会被工具dex2oat翻译成本地机器指令,最终得到一个ELF格式的oat文件。
  3. APK运行时,上述生成的oat文件会被加载到内存中,并且ART虚拟机可以通过里面的oatdata和oatexec段找到任意一个类的方法对应的本地机器指令来执行。
    3.1 oat文件中的oatdata包含用来生成本地机器指令的dex文件内容
    3.2 oat文件中的oatexec包含有生成的本地机器指令。

注意

这里将DEX文件中的类和方法称之为DEX类和DEX方法,将OTA中的类和方法称之为OTA类和OTA方法,ART运行时将类和方法称之为Class和ArtMethod。

ART中一个已经加载的Class对象包含了一系列的ArtField对象和ArtMethod对象,其中,ArtField对象用来描述成员变量信息,而ArtMethod用来描述成员函数信息。对于每一个ArtMethod对象,它都有一个解释器入口点和一个本地机器指令入口点。
ART找到一个类和方法的流程:

  1. 在DEX文件中找到目标DEX类的编号,并且以这个编号为索引,在OAT文件中找到对应的OAT类。
  2. 在DEX文件中找到目标DEX方法的编号,并且以这个编号为索引,在上一步找到的OAT类中找到对应的OAT方法。
  3. 使用上一步找到的OAT方法的成员变量begin_和code_offset_,计算出该方法对应的本地机器指令。
ART堆结构图

可以分配内存的Space有三个:Zygote Space、Allocation Space和Large Object Space。不过,Zygote Space在还没有划分出Allocation Space之前,就在Zygote Space上分配,而当Zygote Space划分出Allocation Space之后,就只能在Allocation Space上分配。因此实际上应用运行的时候能够分配内存也就Allocation 和 Large Object Space两个。

而分配的对象究竟是存入上面的哪个Space呢?满足如下三个条件的内存,存入Large Object Space:1)Zygote Space已经划分除了Allocation Space,2)分配对象是原子类型数组,如int[] byte[] boolean[], 3)分配的内存大小大于一定的门限值。

对于分配对象时内存不足的问题,是通过垃圾回收和在允许范围内增长堆大小解决的。由于垃圾回收会影响程序,因此ART运行时采用力度从小到大的进垃圾回收策略。一旦力度小的垃圾回收执行过后能满足分配要求,那就不需要进行力度大的垃圾回收了。这跟dalvik虚拟机的对象分配策略也是类似的。

Mod Union Table对象
一个用来记录在GC并行阶段在Image Space上分配的对象对在Zygote Space和Allocation Space上分配的对象的引用。
另一个用来记录在GC并行阶段在Zygote Space上分配的对象对在Allocation Space上分配的对象的引用。

Allocation Stack:用来记录上一次GC后分配的对象,用来实现类型为Sticky的Mark Sweep Collector。

Live Stack:配合allocation_stack_一起使用,用来实现类型为Sticky的Mark Sweep Collector。

Mark Stack:用来在GC过程中实现递归对象标记

部分摘自
https://www.jianshu.com/p/5e5cb9c6ed37
https://blog.csdn.net/fei20121106/article/details/44494009
https://blog.csdn.net/evan_man/article/details/52414390

上一篇下一篇

猜你喜欢

热点阅读