JVM类加载机制
类加载过程Java类加载机制
从class字节码到内存中的Java类,必须经历加载、链接、初始化三大步骤,其中链接又包括验证、准备和解析三阶段,这就是Java完整类加载流程。
JVM语言类型Java语言类型
Java语言类型包括基本类型与引用类型两大类。基本类型是JVM预定义的数据类型,包括String、boolean、char、int、float、double、long和byte八大基本数据类型。
Java引用类型包括数组、泛型、类和接口四大类型,其中泛型在编译阶段会被Java编译器进行泛型擦除,所以Java虚拟机实际上只包含数组、类和接口三种类型;数组由虚拟机直接生成,只有类和接口是由类加载器从字节流加载至虚拟机内存。
既使数组由虚拟机直接生成不需要加载,但是仍然需要链接和初始化才能被使用。
双亲委派模型加载
JVM实现类加载(类和接口的引用类型)均由类加载器完成,类加载器寻找类和接口的字节流完成各自职责的加载工作。
启动类加载器是JVM中所有类加载器的祖师爷,是由C++编写的,不能出现在内存中(内存中用null替代),启动类加载器只完成核心类的加载任务,比如将ClassLoader类加载器及其子类加载器从字节流加载至内存。JVM中除启动类加载器外,其他类加载器均是ClassLoader类的子类,需要被启动类加载器加载至内存中才能执行自己的类加载工作。
扩展类加载器负责加载jdk扩展包下的接口和类字节码,主要负责扩展包lib/ext下的类加载(以及由系统变量 java.ext.dirs 指定的类)。
应用类加载器负责加载应用接口和类字节码,通常指位于src/main/java/包下的应用字节码,这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量java.class.path 或环境变量 CLASSPATH所指的路径。
JVM中类加载器在寻找到字节流之后首先必须交由父类加载器审核加载,一直上交至启动类加载器,在启动类加载器不执行加载任务时才能由子类加载器来完成加载,这种类加载机制也称为双亲委派模型。
在Java9之后,引入模块系统,修改了类加载器;扩展类加载器被命名为平台类加载器,Java SE中除少数关键模块之外(java.base包、sun.misc),其他均有扩展类加载器负责加载。
JVM中除了虚拟机预定义类加载器外,我们可以继承应用类加载器来自定义类加载器,实现特殊加载方式,比如加载加密的class字节码文件。
JVM中类加载器与类全名代表同一个类,即不同类加载加载同一全名类表示两个不同的类,可以利用此特性在内存中加载同一个类的不同版本。
链接过程链接
验证:确保接口和类字节码符合虚拟机的约束条件,正常情况下class字节码文件由Java编译器编译必然是符合虚拟机约束的,但是字节码文件可以被修改。
准备:为加载类的静态字段分配内存,Java静态字段的具体初始化发生在初始化阶段;还需要构造其他与当前类相关的数据结构,比如用来实现虚方法的动态绑定的方法表。
解析:在字节流没有被加载至内存之前,类不知道其他类、字段、方法的具体地址,都是通过符号引用来表示,解析正是将符号引用解析成具体地址。
初始化
Java中初始化一个静态字段可以直接在声明时赋值,也可以在静态代码块中赋值;如果一个静态字段被final修饰符修饰,并且是Java基本数据类型字段时,会被Java标记为常量值,由虚拟机直接完成初始化,除此之外的其他赋值操作以及静态代码块中的代码均被Java编译器置于<clinit>方法中;最后在类加载最后一步即初始化阶段,为标记为常量的字段赋值以及执行<clinit>方法,并且Java虚拟机通过加锁确保<clinit>方法只会执行一次。
初始化完成之后内存中的类处于可执行状态。
会触发类初始化的情况:
- 当虚拟机启动时,初始化用户指定的主类;
- 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令目标类;
- 当遇到调用静态方法的时候,初始化静态方法所在的类;
- 当遇到调用静态字段的时候,初始化静态字段所在的类;
- 子类初始化会触发父类的初始化;
- 如果一个接口实现了default方法,那么直接或者间接实现该接口的类的初始化,会触发该接口的初始化;
- 使用反射API对某个类进行反射调用的时候,初始化该类;
- 当初次调用MethodHandle方法时,初始化该MethodHandle方法指向的方法所在的类(利用此条性质实现懒加载单例模式)。
总结
类加载分加载、链接、初始化三大步骤完成。
加载指寻找字节流并创建接口和类的过程;加载需要借助类加载器实现,Java虚拟机中类加载器使用双亲委派模型,启动类加载器由C++实现,在内存中使用null指代。
链接分验证、准备和解析三步,将创建的接口与类合并至Java虚拟机,并解析具体地址,使得接口和类处于能够执行的状态(解析阶段为必须阶段)。
初始化是为被Java虚拟机标记为常量的字段复制以及执行<clinit>方法的过程,初始化过程只会被虚拟机执行一次。