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

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

作者: 小城哇哇 | 来源:发表于2022-11-24 17:50 被阅读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

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/zvexfdtx.html