类加载机制是如何处理字节码文件?

2022-11-24  本文已影响0人  小城哇哇
对于Android开发人员来说,熟悉JVM的规范之后,重点需要了解Dalvik、ART的内部规范。JVM加载的是.class文件,而AVM(全称Android虚拟机,包括Dalvik和ART)加载的是dex文件,如果看过apk文件的伙伴们应该知道

dex文件其实就是一系列.class文件的集合。

1 JIT、解释、AOT

前面我们在介绍AVM堆区内存结构的时候,关于Image Space区域存储内容时,介绍了Android N之后的混合编译,就是通过JIT + 解释 + AOT完成,那么我们这边详细介绍一下这个编译方式。

1.1 Android N编译流程

首先我们先看下一个流程图。

(1)我们在本地编写的Java或者Kotlin代码,首先会通过前端编译器将其编译成汇编指令;
(2)在后端编译器中,存在执行引擎,执行引擎中有解释器、JIT。
其中解释器的作用就是方法执行时,将字节码逐行编译,配合PC寄存器执行方法运算;
JIT则是会查找热点代码记录在Profile文件中,等到设备空闲时,通过AOT将Profile中所有字节码文件翻译成本地机器码缓存在base.art文件中,并存储在Image Space中。

1.2 dex2oat与dexopt

dex2oat的目的也是dex优化,因为每次调用方法时,执行引擎都需要拿到字节码文件,通过解释器来编译成本地机器码执行,这个过程是耗时的,尤其是单核或者双核的CPU设备,因此系统会找一个时机将dex文件提前编译优化成本地机器码,加快执行的速度。
具体文件路径在/system/bin/dex2oat



dex2oat是针对于ART虚拟机做的dex优化,而远古时期的Dalvik虚拟机则是使用的是dexopt做dex文件优化,将dex文件优化为odex文件,如果熟悉Android类加载的伙伴们应该了解,在Android 8.0之前,DexClassLoader需要传入一个参数为optFileDirectory,就是用来存储odex文件的文件夹路径。

2 class文件与dex文件数据结构

通过前端编译器编译成字节码文件之后,像class文件或者dex文件,他们内部结构是什么样的呢?我们需要知道,扔给后端编译器的文件是什么样的。

2.1 class文件结构

在Class文件中,我们之前已经知道一个大概

看下上面的图就是整个class文件,其中包含的信息包括:
(1)主次版本号:标注JDK的版本,当前版本为jdk 1.8版本;
(2)常量池:这部分我们之前用到过,在方法执行时,可以拿到符号引用,通过符号引用去常量池中查找方法的直接引用;
(3)访问标志:标记当前类文件的访问标识,private、public、static等;
(4)当前类的方法、接口、属性等信息
其中,每个方法都对应一份字节码,对于JVM来说,执行引擎通过执行这些字节码指令完成方法的执行,可以说.class文件就是由字节码组成的。



而dex文件则是由一组.class文件组成的,拆出来每个.class文件的结构都是如此。

2.2 class文件的生命周期

class文件的生命周期主要分为3个阶段:加载、链接、初始化,首先我们先看下加载阶段。

2.2.1 加载

加载的含义就是我们理解的类加载,就是将.class文件(物理)加载到方法区的这个过程。在这个过程当中,首先会将.class文件中静态数据、常量池放到方法区中,然后生成一个对应该类的java.lang.Class对象,作为访问该类的入口。
如果熟悉反射的伙伴们应该知道,如果想要获取这个类中的某个方法或者属性,就需要首先获取这个类的Class对象

    Class<Person> personClass = Person.class;
    Method declaredMethod = personClass.getDeclaredMethod("");

当class文件加载到方法区后,方法区存储的是这个类的模板,同时会在堆区生成一个Class对象封装类在方法区的数据结构,这整个过程是在类加载的过程中完成的。
这里面有个特殊情况,就是数组类的加载,首先数组不是类,因此不会遵循类加载机制,而是在执行字节码指令的时候,如果发现当前引用类型为数组,则是由JVM直接完成创建,数组的元素类型则是会继续由类加载完成。

int[] a = new int[5];
Person[] per = new Person[3];

例如,int数组不会发生类加载,Person数组也不会发生类加载,但是Person类元素则是需要进行类加载的。

2.2.2 链接

链接其实分为3部分,分别为:验证、准备、解析;
验证:目的是为了检查字节码是否符合规范,例如是否包含基本的信息(魔数、版本号等)、方法符号引用是否存在直接引用等等。
准备阶段:为类的静态变量分配内存,并且赋值初始值,例如int类型静态变量默认初始值为0,但是不会为引用类型(堆内存中)赋值初始值

解析:将类、接口、方法中的符号引用转换为直接引用,看下面的字节码

0 new #5 <com/lay/mvi/jvm/Person>
3 dup
4 invokespecial #6 <com/lay/mvi/jvm/Person.<init> : ()V>
7 astore_0
8 return

这段字节码指令是创建了一个Person对象,其中我们看到的#5、#6就是符号引用,这些符号引用就是存储在方法区中Class常量池中的。
因为在前面验证阶段已经验证,当前符号引用是否存在直接引用,因此这个过程中,就是将#5、#6替换成直接引用

2.2.3 初始化

在这个阶段主要做两件事:
(1)为相关变量赋予初始值;例如为name赋值

public class Person {
    private String name = "Faker";
    public Person(String name){
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
(2)执行 init 方法,这个方法不是类的构造函数,可以认为是类构造函数的父亲

这个方法是由编译器生成的,由JVM去调用,不能人为去调用。

后续就是调用阶段,使用这个类中的方法或者属性值。所以整个class文件的生命周期可以使用下面这个流程做统一的概括

3 类加载器

前面我们介绍了class文件从加载到使用的过程,现在我们单拎出来类加载的这个过程,详细介绍下类加载的过程,并熟悉Android中常用的类加载器。
首先我们需要知道什么是类加载,就是读取指定目录下的字节码文件,解析字节码文件。

3.1 Android中的类加载器

我们可以看一下,在Android中主要是分为以下几类加载器:

(1)BaseDexClassLoader:其中有两个子类PathClassLoader和DexClassLoader
(2)BootClassLoader:是PathClassLoader和DexClassLoader的父类加载器,注意并不是Java意义上的继承父类。

3.2 Android中的类加载方法

在类加载中,主要有3个方法,分别是:
(1)loadClass:这个方法的主要作用就是双亲委派,查找这个类是不是被某个父类加载器加载过了,如果已经加载过了,那么就直接拿到这个加载过的类使用;
(2)findClass(String name):如果父类加载器都没加载过这个类,则由子类加载器根据某个具体的字节码文件路径加载,就是本小节开头说的,读取指定目录下的字节码文件。
(3)defineClass:读取完字节码文件之后完成检验,生成对应的Class对象
defineClass完成,类加载也就完成了。

来自:https://juejin.cn/post/7155015446511484964

上一篇下一篇

猜你喜欢

热点阅读