JVM类加载机制

类加载的定义:Java代码经过编译,会生成JVM能够识别的二进制字节流文件(*.class)。在Java程序的运行时,JVM把描述类的数据从Class文件加载到内存,并对数据进行校检、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制

Class文件中的类,从加载到JVM内存,到卸载出内存,共有七个生命周期阶段,分别是加载验证准备解析初始化使用卸载类加载机制包含了前五个阶段

初始化解析发生的先后顺序并不确定,解析可能发生在初始化之前,也可以发生在初始化之后(为了支持Java的动态绑定机制,由于Java多态,暂时不知道到底哪段代码会被调用,所以解析阶段可能会延后)。

类加载的时机:类加载无需等到程序中“首次被使用”时才开始,JVM预先加载某些类也是允许的。

类加载的五个阶段可以总结为三步加载链接初始化

1.加载

主要完成三件事:

  • 通过一个类的全限定名(包名+类名)来获取定义此类的二进制字节流(Class文件)。
  • 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构。(方法区就是用来存放已被加载的类信息、常量、静态变量的运行时内存区域)
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

总的来说,加载过程就是将一个类的二进制字节流Class文件转化成对应的java.lang.Class对象的过程。

2.链接

类加载后生成了java.lang.Class对象,接着会进入链接阶段,将类的二进制数据合并入JRE(Java运行时环境)中,链接包含三个阶段:

  • 验证:验证被加载的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机的安全。
  • 准备:为类的静态变量(static)方法区分配内存,并赋默认初值(0或者null)。注意如果是静态常量(static final),早在编译期就已经存放在常量池中了,而现在再次在方法区里分配内存,并直接赋设定初值,而非默认初值。如果是普通成员成员变量,则要等到实例化的时候才会在堆中分配内存。
  • 解析:Class文件常量池(不是运行时常量池)内的符号引用替换为直接引用。(静态绑定和动态绑定就在这时进行)直接引用可以是直接指向目标的指针、或是一个能直接定位到目标的句柄。如果有了直接引用,那么引用的目标必定已经在内存中存在

3.初始化

类的初始化是类加载机制的最后一步,到了初始化阶段才真正执行Java代码。主要工作是为静态变量(static,但不包括static final)设定初值

JVM规范中有且仅有五种情况必须对类进行初始化类加载机制的前几个阶段自然而然也要在初始化之前进行):

  • 使用new字节码指令创建类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(在编译期就已经放入常量池中的static final常量除外),或者调用一个静态方法的时候,对应类必须进行过初始化。

  • 通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。

  • 当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化

  • 虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类。

  • 使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。

上面这五种情况被称为主动引用,除了这五种情况,其他所有的类引用方式都不会触发类的初始化,被称为被动引用(三种)

  • 通过子类引用父类的静态字段,对于父类属于“主动引用”的第三种情况,父类会被初始化;而对于子类属于“被动引用”,故子类不会初始化。
//父类
public class SuperClass {
//静态变量value
public static int value = 666;
//静态块,父类初始化时会调用
static{
System.out.println("父类初始化!");
}
}

//子类
public class SubClass extends SuperClass{
//静态块,子类初始化时会调用
static{
System.out.println("子类初始化!");
}
}

//主类、测试类
public class NotInit {
public static void main(String[] args){
System.out.println(SubClass.value);
}
}
  • 通过数组来引用类,因为是new数组而不是new类的实例,因此属于被动引用。
//父类
public class SuperClass {
//静态变量value
public static int value = 666;
//静态块,父类初始化时会调用
static{
System.out.println("父类初始化!");
}
}

//主类、测试类
public class NotInit {
public static void main(String[] args){
SuperClass[] test = new SuperClass[10];
}
}
  • 读取类的静态常量(static final),因为已经在编译时就存在常量池了,可以直接从常量池中读,因此不会触发类的初始化,属于被动引用。
//常量类
public class ConstClass {
static{
System.out.println("常量类初始化!");
}

public static final String HELLOWORLD = "hello world!";
}

//主类、测试类
public class NotInit {
public static void main(String[] args){
System.out.println(ConstClass.HELLOWORLD);
}
}
文章作者: Moon Lou
文章链接: https://loumoon.github.io/2021/03/09/JVM类加载机制/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Moon's Blog