图文兼备看懂类加载机制的各个阶段,就差你了!

写在前面:类加载机制却是听得很多了,可是又知不知道它到底有什么用呢?为何要学它呢?由于面试(真实.jpg),其实也不只仅是面试,掌握它能够掌握对类加载的时机,在真正须要使用到类时才加载到内存中,能够减轻服务器的压力,并且,许多框架底层源码都用到了反射这个东西,反射的原理就是基于类加载机制的,因此掌握了这门绝活,学反射就不会一头雾水了,看源码也知其因此然了。java

开篇概述:Java文件在编译时转换为字节码文件,字节码文件就是对一个类的描述,Java虚拟机把Class文件加载到内存,而且通过验证、准备、解析和初始化,最终造成能够被JVM直接使用的Java类型,这就是类加载机制。下面就是对类加载机制各个过程的详细分析,每一个阶段都会尽我所能把最详细清晰的图贴上去带你理解这个阶段究竟是怎样的面试

类的生命周期

类的生命周期

类的生命周期分为7个阶段,在图中我已经标注了每一个阶段对类所作的主要事情,你请拿好,若是这张图能帮助到你,你的点赞是对我最大的鼓励和支持!(跑远了哈哈哈)其中验证、准备、解析三个部分统称为链接,下面我就会对每个部分作出通俗易懂的解释,用最友好的图示来告诉你,这一个阶段JVM到底作了什么~服务器

触发类加载的时机

声明:加载是类加载的其中一个阶段,类加载包含了前五个阶段(加载、验证、准备、解析、初始化),要区分开加载和类加载的区别。数据结构

咱们来看看何时会类加载呢?多线程

第一个阶段是加载,在Java虚拟机规范中没有明确规定一个类在何时会被加载,可是它严格规定了只有如下6种状况必须对类进行初始化操做,在初始化操做以前一定会触发类的加载和链接框架

(1)遇到newgetstaticputstaticinvokestatic这四条字节码指令时函数

  • 使用new关键字实例化对象时;对应new字节码指令布局

  • 读取或设置一个类的静态字段(被final修饰的、在编译期把结果放入常量池的静态变量除外)时;对应getstaticputstatic字节码指令学习

  • 调用一个类的静态方法时;对应invokestatic字节码指令spa

(2)使用java.lang.reflect包的方法第一次对类进行反射调用时会触发类的初始化

(3)初始化类时,若是发现父类尚未初始化,则须要先触发父类的初始化

(4)虚拟机启动时,用户须要指定一个主函数类(main()方法所在的类),虚拟机会先启动这个类

(5)使用JDK7新加入的动态语言支持时,若是一个java.lang.invoke.MethodHanlde实例最后的解析结果为REF_getstaticREF_putstaticREF_invokeStaticREF_newInvokeSpecial四种类型的方法句柄时,都须要先初始化该句柄对应的类

(6)接口中定义了JDK 8新加入的默认方法(default修饰符),实现类在初始化以前须要先初始化其接口

上面几种类型是否是看得懵逼,下面我就会对类的初始化进行举例,让大家经过更直观的场景能够理解上面几种状况。由于只要类被初始化,它就必定得先加载该类到内存中

实战初始化类

定义一个StaticClass,若是类被初始化,那么会自动执行静态代码块,在控制台能够看到信息。

/** * @author Zeng * @date 2020/4/8 23:21 */
public class StaticClass {

    static {
        System.out.println("StaticClass initialized!");
    }

    public static int A = 0;

    public static void staticFunction(){
        System.out.println("staticFunction executed;");
    }

}
复制代码

我以第一种状况给你演示一下类是否真的被加载和初始化了

咱们使用一个Test类调用静态变量A和静态方法staticFunction(),以下图所示

/** * @author Zeng * @date 2020/4/8 23:21 */
public class Test {
    public static void main(String[] args) {
        //new
        StaticClass obj = new StaticClass();
        //getstatic
        int a = StaticClass.A;
        //putstatic
        StaticClass.A = 1;
        //invokestatic
        StaticClass.staticFunction();
    }
}
复制代码

控制台的结果以下,很明显StaticClass是被初始化了

类初始化

咱们可使用JVM启动参数-XX:+TraceClassLoading进行查看StaticClass类有没有被加载

类加载

能够看到JVM确确实实是加载了StaticClass

类加载的过程

知道了触发类加载的6种作法之后,咱们就深刻类加载的过程,探秘每个过程发生的事情

加载阶段

加载阶段,Java虚拟机须要完成三件事情

  1. 经过一个类的全限定名来获取定义此类的二进制字节流。

  2. 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构

  3. Java堆内存中生成一个表明这个类的java.lang.Class的对象,做为方法区中这个类的各类数据的访问入口

怎么理解上面的第二、3点呢?

静态存储结构指的是Class文件结构,它是一组以8位字节为单位的二进制流,此时是静态的,虚拟机会把这个文件的相关类信息加载到方法区当中,并在Java堆上建立java.lang.Class的对象,该对象就是图中的SubClass类,注意不是SubClass的对象实例,而是java.lang.Class的对象实例

到这里咱们会产生一个疑问,加载阶段不是应该在链接阶段以前执行吗?为何还没进行验证、准备和解析就能够把类信息放入方法区?

注意:加载阶段和链接的部分动做(如一部分字节码的文件格式验证动做)是交叉进行的,也就是说加载阶段还没完成,链接阶段可能已经开始,但这些夹在加载阶段之中进行的动做(验证文件格式、字节码验证······)都属于链接操做。

链接阶段

链接阶段包括验证、准备和解析,下面咱们每个阶段来细看,咱们不用记住每个阶段内部具体校验的东西,从总体上归纳该阶段干了一些什么。

验证阶段

验证阶段主要包括四个检验动做:

文件格式检验:验证上面的Class文件字节流是否符合Class文件格式的规范,而且能被当前版本的虚拟机处理。例如文件前四个字节是否为CA FE BA BE表明这是一个Class文件。

元数据验证:对字节码描述的信息进行语义分析,保证其描述的信息符合要求,例如数据类型是否正确,是否正确继承类·····

字节码验证最复杂的一个阶段,经过数据流分析和控制流分析,肯定程序语义是合法的、符合逻辑的,例如是否在return后面还有语句,这些语句是不可达的······

符号引用验证:对类自身之外的各种信息进行匹配性校验,通俗地说,该类是否缺乏或禁止访问它依赖的某些外部类、方法、字段等资源

下面用上面的字节码文件做例子来给你说明这四个检验动做:

文件格式检验:假如在获取到Class文件以后,我偷偷地将开头修改成CA FE DE AD,虚拟机还能正常加载该文件吗?

咱们此时使用java SubClass命令尝试加载该文件,看看发生什么结果!

上图中的错误信息已经很是明显了,告诉咱们魔数3405700782是一个非法值,咱们再来看看这个值是什么~

这不就是咱们刚刚修改的地方嘛,所以,文件格式验证会对文件的相关格式作检验,固然个人例子只是冰山一角,实际上JVM作的校验多了去了。我只是将最容易理解的方式写给大家,让大家对整个过程有个最直观的理解,尽可能不要死记硬背

元数据验证、字节码验证和符号引用验证因为篇幅问题就再也不作例子了,咱们只要知道验证阶段是校验字节码文件里的一切东西都要符合《Java虚拟机规范》

准备阶段

准备阶段是正式为类中定义的变量(即静态变量、被static修饰的变量)分配内存并设置类变量初始值的阶段,咱们须要知道两个要点

  1. 类变量被分配在哪一个存储区域
  2. 类变量的初始值是什么

首先咱们先来讲第1点,咱们都知道方法区是存储类相关信息的区域,在JDK7及之前,类变量是存储在方法区当中的,而在JDK8及以后,类变量已经随着Class对象一块儿存放在Java堆当中了,这时候类变量存放在方法区这句话已经只是停留在逻辑上的概念表述层面了。

第2点是类变量的初始值,假设有一个类变量public static int value = 666,在准备阶段事后的初始值是0而不是666,在初始化阶段时才会被赋值为123

注意若是有一个静态类变量为public static final int value = 666,那么它在准备阶段JVM就已经会给它赋值为666,不会赋零值。

解析阶段(重要!)

将常量池内的符号引用替换为直接引用的过程。

符号引用:符号引用是以一组符号来描述所引用的目标,符号能够是任何的字面形式的字面量,只要不会出现冲突可以定位到就行。符号引用于JVM内存布局无关。

符号引用的做用是在编译的过程当中,JVM并不知道引用的具体地址,因此用符号引用进行代替,而在解析阶段将会将这个符号引用转换为真正的内存地址。

直接引用:能够是指向目标的指针,偏移量或者可以直接定位的句柄。该引用是和内存中的布局有关的,而且必定加载进来的。有了直接引用,那么引用的目标一定已经在虚拟机内存中。

直接引用能够理解为:指向类对象、变量、方法的指针、指向实例的指针和一个间接定位到对象的对象句柄。

举个例子让你去理解它们两个的区别

public class Test{
   public static void main(String[] args) {
     String s="adc";
     System.out.println("s="+s);
   }
}
复制代码

上面这段代码的变量s在编译时会被解析成为符号引用,符号引用的标志是astore_<n>,对应下图的astore_1

咱们在方法里定义了一个局部变量s,把它指向adc存放的地址,可是在编译时s并不知道adc的地址,JVM将变量sastore_1对应起来,astore_1的含义是将操做数栈顶的adc保存回索引为1的局部变量表中,此时访问变量s就会读取局部变量表索引值为1中的数据。因此局部变量s就是一个符号引用。

下面这段代码的字符串被解析为直接引用

public class Test{
    public static void main(String[] args) {
        System.out.println("s="+"adc");
    }
}
复制代码

咱们能够看到字节码指令ldc直接将s=abc这一字符串从常量池中推送到栈,而后下一条字节码指令invokevirtual表明调用实例方法,并无将字符串存入局部变量表中,因此这里的s=abc就是一个直接引用。

总结一下:符号引用是指在编译时没法肯定对象的内存地址,因此必须使用一个符号引用去对应局部变量表中的一个特定位置,而后在解析阶段将该变量的值或引用地址保存回局部变量表中,此后访问该变量值都会从局部变量表对应的位置查找该值;而直接引用是在编译时就能够肯定。

初始化

类的初始化是类加载的最后一个阶段了,在准备阶段时,JVM已经为类变量赋了零值,在初始化阶段,会根据代码去真正地初始化类变量值和其它资源

咱们先来看看StaticClass被初始化时是如何执行静态代码块的?

咱们在IDEA中查看StaticClass的字节码文件,看到熟悉的一个输出语句,那么咱们能够推测静态代码块被翻译成下面这个<clinit>函数(先别走T.T,这个函数挺重要的,咱们要掌握的,坚持看下去,学会了很香的)

StaticClass字节码的类构造器

静态代码块其实就是一个类构造函数,当一个类被初始化时,就会被调用这个<clinit>方法对类进行初始化操做,注意这个方法只会执行一次,由于JVM加载某个类到内存中后,直到卸载以前,这个类一直都在内存当中,因此这也解释了为何静态代码块只会执行一次

<clinit>方法

这个方法是由编译器自动收集类中全部类变量的赋值操做和静态代码块中的语句合并产生的,收集的顺序是由语句在源文件中出现的顺序决定的,静态代码块只能访问到定义在它以前的类变量,可是能够为定义在它以后的类变量赋值

public class Test{
    static {
        i = 0; //编译经过
        System.out.print(i); //编译失败
    }
    static int i = 1;
}
复制代码

在初始化一个类时,必须先初始化其父类,所以第一个执行<clinit>方法的必定是Object类。

<clinit>方法不是必须存在的,若是一个类中没有类变量的赋值操做,也没有静态代码块,那么这个类将没有<clinit>方法。

若是多个线程同时但愿初始化一个类,<clinit>方法会在多线程环境下保证正确地加锁同步,只有其中一个线程去执行这个类的clinit<>()方法。

总结

完结撒花!!!看到这里的你已经掌握了类加载机制的绝大多数内容了,主要须要掌握类加载机制的七个阶段,类加载过程当中每一个阶段所作的事情,什么状况下会触发类的初始化解析阶段的直接引用和符号引用在面试过程当中若是能解释清楚是很是加分的,它表明你对虚拟机栈的结构很是清晰,也清楚类加载的每一阶段主要作了什么,首先很感谢你愿意花时间来阅读个人文章,若是这篇文章对你有一点点小的帮助,**你的点赞是对我最大的鼓励和支持!**因为做者能力有限,如文章有严重错误,请务必评论指出,乐意与你们交流和学习!

巨人的肩膀:

blog.csdn.net/qq_34402394…

相关文章
相关标签/搜索