写在前面的话:本文是在观看尚硅谷JVM教程后,整理的学习笔记。其观看地址如下:尚硅谷2020最新版宋红康JVM教程

一、什么是类加载过程

(1)、概述
我们编写的类(.java文件)会被编译器(如javac编译器)编译成Class文件。Java虚拟机把Class文件加载到内存中的过程就称为类加载过程。
(2)、类的生命周期

  • 一个类从被加载到虚拟机内存中,到卸载出内存,共经历七个过程,即这个类的生命周期会经历加载、验证、准备、解析、初始化、使用、卸载七个阶段。其中,验证、准备、解析三个阶段又统称为连接。图示如下:
    在这里插入图片描述
    下面我们将逐个介绍类生命周期每个阶段的执行过程。

二、加载阶段

加载阶段是整个类加载过程的第一个阶段。
在本阶段,Java虚拟机主要完成以下三件事:
(1)、 通过一个类的全限定名称获取定义此类的二进制字节流。
(2)、 将该字节流所代表的静态存储结构转化为方法区中数据结构。
(3)、 在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
Java虚拟机运行时数据区示意图如下:
在这里插入图片描述
总结概括就是,先将Class文件以字节流的形式加载到内存,再把这个字节流放到虚拟机运行时数据区的方法区中,以及在内存中生成一个代表该类的Class对象,作为方法区中该类的访问入口。

三、验证阶段

(1)、 验证阶段目的是确保Class文件中的字节流中包含的信息符合虚拟机规范为约束要求,保证被加载类的正确性不会威胁到虚拟机自身的安全。
(2)、 验证的内容有,
文件格式验证:字节流是否符合Class文件格式。
元数据验证:对字节码描述的信息进行语义分析。
字节码验证:确定程序语义是否合法、符合逻辑。
符号引用验证:该类是否缺少或禁止访问它依赖的外部资源,如其他类、方法等。

四、准备阶段

准备阶段是正式为类中定义的静态变量(static修饰的变量)分配内存并设置零值。

零值:虚拟机为基本数据类型设置的初始值,比如int类型的零值为0,boolean类型的零值为false。

比如,private static int a = 123,在准备阶段过后,a的初始值为0,而不是123。而 a = 123这个赋值动作是在初始化阶段进行的。
通常情况下,准备阶段的静态变量的初始值是零值。但是,如果静态变量被 final 修饰,则可能不会是零值。如 private final static int b = 123 ,则 b 的初始值是123,而不是零值。因为 final static修饰的 b 为常量,在编译时就已经被赋值了,即在被编译成字节码的时候就已经是 b = 123;

五、解析阶段

解析阶段是将常量池中的符号引用替换为直接引用的过程。

符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用:是可以直接指向目标的指针、相对偏移量、或者句柄。

解析阶段主要涉及到符号引用和直接引用的转换,但不影响了解整个类加载过程,故先略过,以后再补充。

六、初始化阶段

初始化时类加载过程的最后一个阶段,在前面的几个类加载动作中,除了加载阶段用户编写的代码(应用程序)可以通过自定义的类加载器参与类加载外,其余阶段的类加载动作都是虚拟机完成的。直到初始化阶段,虚拟机才真正执行类中用户编写的Java代码。

类加载器:前面写到,在加载阶段虚拟机需要完成“通过一个类的全限定名称获取定义此类的二进制字节流”这个动作,虚拟机将这个动作交给应用程序,让其自行去决定怎么获取所需的类。而实现这个动作的代码就被称为类加载器。

初始化阶段就是执行类构造器 < clinit >()方法的过程。
< clinit >()方法不是我们直接编写的Java代码,而是javac编译器的自动生成物。
关于< clinit >()方法,需要知道一下几点:
(1)、 此方法由javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并而成(即static修饰的变量和static{}),按语句的顺序执行。
如下代码所示:

public class HelloClinit {

    //num = 1 和 static{} 的过程就是 <clinit>()的过程
    private static int num = 1;

    static{
        num = 2;
        a = 2;  //可以为之后定义的变量赋值,但不能访问
        //System.out.println(a);  //报错
    }

    private static int a;

    public static void main(String[] args) {
        System.out.println(num+","+a);
    }
}

输出结果为:2,2

(2)、 < clinit >()方法不需要我们自己去调用父类的< clinit >()方法。因为在子类的< clinit >()执行前,虚拟机会保证父类的< clinit >()已执行完毕
代码如下:

/**
 * Java虚拟机会保证在子类的<clinit>()执行前,父类的<clinit>()先执行,
 * 这也意味着父类中定义的静态代码块要优于子类中的变量赋值操作
 */
public class HelloClinit {
    static class father{
        //父类的静态变量负责语句a = 1和静态代码块static{}就是父类的<clinit>()
        public static int a = 1;
        static {
            a = 2;
        }
    }

    static class son extends father{
        public static int b = 0;
        static {
            b = a;    //故子类在调用的父类的变量a时,实际上a = 2
        }
    }

    public static void main(String[] args) {
        System.out.println(son.b);  //2
    }
}

输出结果为:2

(2)、 Java虚拟机会保证一个类的clinit()方法在多线程环境中被正确的同步加锁,即如果多个线程同时去初始化一个类,那么只有其中一个线程去执行这个类的<clinit>()方法,其他线程都要阻塞的等待,直到活动线程执行完<clinit>()方法,即一个类的clinit()只会被加载一次。</clinit></clinit>

下面的代码模拟一条线程在死循环状态下操作,而另一条线程无限阻塞等待的过程。

public class ClinitThread {
    public static void main(String[] args) {
        /**
         * 下面的代码创建两个线程,让他们都去创建DeadThread类的对象,
         * 但实际上只有线程1会执行<clinit>()方法,即执行static{}语句块,并陷入死循环
         * 而线程2则会阻塞等待
         */
        Runnable r = () ->{
            System.out.println(Thread.currentThread().getName()+"开始");
            DeadThread deadThread = new DeadThread();
            System.out.println(Thread.currentThread().getName()+"结束");
        };

        Thread t1 = new Thread(r,"线程1");
        Thread t2 = new Thread(r,"线程2");

        t1.start();
        t2.start();
    }
}

class DeadThread{
    static {
        if (true){
            System.out.println(Thread.currentThread().getName()+"初始化当前类");
            while(true);    //陷入死循环
        }
    }
}

以上几个阶段就是类加载的大致过程

七、拓展补充

类加载的时机

  • (1)、在生命周期图中,加载、验证、准备、初始化、卸载。这五个阶段的顺序是确定的,即只有前一个阶段开始,后一个阶段才能开始。但是解析阶段是不确定的,因为它在一些情况下可以在初始化过程之后再开始。注意这个说的是阶段开始,而并不是阶段完成,因为这些阶段通常都是交叉混合进行的。比如在加载阶段已开始但尚未完成时,验证阶段可能已经开始了。
  • (2)、关于什么时候开始类加载的第一阶段加载,具体由虚拟机自身来把握实现,没有明确的规定。但是,对于类加载中的初始化阶段,有且只有以下六种情况必须立即对类进行初始化操作。这也意味着,在初始化操作前,加载,验证、准备也已经开始了。
    具体的六种情况如下:
    ① 遇到new、getstatic、putstatic或invokestatic这四条字节码指令的时候,如果类没有进行初始化,则需要先触发其初始化.
    生成这四条指令的最常见的java代码场景是:

    1.使用new关键字实例化对象的时候

2.读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
3.调用一个类的静态方法的时候

② 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化
③ 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
④ 当虚拟机启动的时候,用户需要制定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类
⑤ 当使用jdk7新加入的动态语言支持的时候,如果一个java,lang.invoke.MethodHandler实例的最后解析结果是REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial四种类的方法句柄,并且这个方法句柄对应的类没有进行过初始化,那么需要先触发其初始化.
⑥ (新)当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那么该接口要在其之前初始化

以上几种使用类的情况称为类的主动使用,除了以上6种外,其他使用类的方式都称为类的被动使用,即不会导致类的初始化。