“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第17篇文章,点击查看活动详情”

作者石臻臻, CSDN博客之星Top5Kafka Contributornacos Contributor华为云 MVP ,腾讯云TVP, 滴滴Kafka技能专家 KnowStreaming


KnowStreaming 是滴滴开源的Kafka运维管控渠道, 有兴趣一同参加参加开发的同学,可是怕自己才能不够的同学,能够联系我,当你导师带你参加开源!

类加载时机

虚拟机把描绘类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成能够被虚拟机直接运用的 Java 类型,这就是虚拟机的类加载机制。

在Java言语里面,类型的加载、衔接和初始化进程都是在程序运行期间完结的

类加载的进程

类的生命周期

【Jvm】Jvm类加载机制
PS: NoClassDeFoundError: 发生在类生命周期中解析阶段找不到相应的类 ClassNotFoundException 发生在类生命周期的加载阶段,找不到相应的类。

为支撑运行时绑定,解析进程在某些状况下可在初始化之后再开端,除解析进程外的其他加载进程有必要依照如图次序开端。

加载

  1. 经过全限制类名来获取界说此类的二进制字节省。
  2. 将这个字节省所代表的静态存储结构转化为办法区的运行时数据结构
  3. 在内存中生成一个代表这个类的 java.lang.Class 目标,作为办法区这个类的各种数据的拜访进口。

验证

验证是衔接阶段的第一步,这一阶段的意图是为了确保 Class 文件的字节省中包括的信息契合当时虚拟机的要求,而且不会损害虚拟机本身的安全

  1. 文件格局验证:如是否以魔数 0xCAFEBABE 最初、主、次版本号是否在当时虚拟机处理范围之内、常量合理性验证等。 此阶段确保输入的字节省能正确地解析并存储于办法区之内,格局上契合描绘一个 Java类型信息的要求。
  2. 元数据验证:是否存在父类,父类的承继链是否正确,抽象类是否完成了其父类或接口之中要求完成的一切办法,字段、办法是否与父类发生矛盾等。 第二阶段,确保不存在不契合 Java 言语规范的元数据信息。
  3. 字节码验证:经过数据流和控制流剖析,确定程序语义是合法的、契合逻辑的。例如确保跳转指令不会跳转到办法体以外的字节码指令上。
  4. 符号引证验证:在解析阶段中发生,确保能够将符号引证转化为直接引证。

能够考虑运用 -Xverify:none 参数来封闭大部分的类验证措施,以缩短虚拟机类加载的时间。

预备

变量分配内存并设置类变量初始值,这些变量所运用的内存都将在办法区中进行分配。

public static int value = 123 在预备阶段往后的初始值是0 而不是123,因为这个时分没有正在的履行过java代码;而把value赋值为123的putstatic指令是程序被编译后,存放在类结构器clinit()办法中,将会在初始化的时分履行

特殊状况:final润饰 public final static int value = 123 被final润饰之后就是常量特点,在预备阶段虚拟机就会直接赋值,不需求等到初始化阶段;

解析

虚拟机将常量池内的符号引证替换为直接引证的进程。 解析动作主要针对类或接口、字段、类办法、接口办法、办法类型、办法句柄和调用点限制符 7 类符号引证进行。 为支撑运行时绑定,解析进程在某些状况下可在初始化之后再开端,除解析进程外的其他加载进程有必要依照如图次序开端

NoClassDeFoundError: 发生在类生命周期中解析阶段找不到相应的类

初始化

到初始化阶段,才真实开端履行类中界说的 Java 程序代码,此阶段是履行 clinit() 办法的进程。 clinit() 办法是由编译器按句子在源文件中呈现的次序顺次主动搜集类中的一切类变量的赋值动作和静态代码块中的句子兼并发生的。(不包括结构器中的句子。结构器是初始化目标的,类加载完结后,创建目标时分将调用的 init() 办法来初始化目标)

静态句子块中只能拜访到界说在静态句子块之前的变量,界说在它之后的变量,在前面的静态句子块能够赋值,可是不能拜访 如下:

【Jvm】Jvm类加载机制
这样的写法是正常的; 可是假如把声明换个方位
【Jvm】Jvm类加载机制
这样就会提示 不合法前向引证 ; 进而说明 clinit() 办法是由编译器按句子在源文件中呈现的次序 顺次主动搜集类中的一切类变量的赋值动作和静态代码块中的句子兼并发生的 总结一下:

  1. 初始化接单是履行 clinit() 办法的进程
  2. clinit() 办法是由编译器按句子在源文件中呈现的次序,顺次主动搜集类中的一切类变量的赋值动作和静态代码块中的句子兼并发生的
  3. clinit() 不需求显式调用父类,的初始化办法 clinit(),虚拟时机确保在子类的 clinit() 办法履行之前,父类的 clinit() 办法现已履行结束,也就意味着父类中界说的静态句子块要优先于子类的变量赋值操作。
  4. clinit() 办法对于类或接口来说并不是必需的,假如一个类中没有静态句子块,也没有对变量的赋值操作,那么编译器能够不为这个类生成 clinit() 办法
  5. 虚拟时机确保一个类的 clinit() 办法在多线程环境中被正确地加锁、同步,假如多个线程同时去初始化一个类,那么只会有一个线程去履行这个类的 clinit() 办法,其他线程都需求堵塞等待,直到活动线程履行 clinit() 办法结束。

对于初始化阶段,虚拟机规范规定了有且只要 5 种状况有必要立即对类进行“初始化”(而加载、验证、预备自然需求在此之前开端)

  1. 遇到new、getstatic 和 putstatic 或 invokestatic 这4条字节码指令时,假如类没有进行过初始化,则需求先触发其初始化。对应场景是:运用 new 实例化目标、读取或设置一个类的静态字段(被 final 润饰、已在编译期把成果放入常量池的静态字段除外)、以及调用一个类的静态办法。
  2. 对类进行反射调用的时分,假如类没有进行过初始化,则需求先触发其初始化。
  3. 当初始化类的父类还没有进行过初始化,则需求先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完结了初始化)
  4. 虚拟机启动时,用户需求指定一个要履行的主类(包括 main() 办法的那个类), 虚拟时机先初始化这个主类。
  5. 当运用 JDK 1.7 的动态言语支撑时,假如一个 java.lang.invoke.MethodHandle 实例最终的解析成果 REF_getStatic、REF_putStatic、REF_invokeStatic 的办法句柄,而且这个办法句柄所对应的类没有进行过初始化,则需求先触发其初始化。

类加载器

即便两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。

双亲派遣模型

双亲派遣模型的加载类逻辑可参阅如下代码:

【Jvm】Jvm类加载机制

    // 代码摘自《深化了解Java虚拟机》
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 首先,查看恳求的类是否现已被加载过了
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            // 假如父类加载器抛出ClassNotFoundException
            // 说明父类加载器无法完结加载恳求
            }
            if (c == null) {
                // 在父类加载器无法加载的时分
                // 再调用本身的findClass办法来进行类加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }

自界说类加载器

  1. 承继 java.lang.ClassLoader
  2. 重写父类的 findClass() 办法,JDK 的 loadCalss() 办法在一切父类加载器无法加载的时分,会调用本身的 findClass() 办法来进行类加载,因而咱们只需重写 findClass() 办法找到类的二进制数据即可。

参阅文章

  1. 深化了解虚拟机