JVM 类加载机制
JVM类加载机制是Java运行时环境的核心部分,它负责将类的.class文件加载到JVM中,并将其转换为可以被JVM执行的数据结构。
类加载的整体流程
类加载的整体流程可以分为五个阶段:加载(Loading)、链接(Linking)、初始化(Initialization)、使用和卸载(Unloading)。其中,链接阶段又可以细分为验证(Verification)、准备(Preparation)和解析(Resolution)三个阶段。
加载阶段
- 系统提供的类加载器或自定义类加载器:首先,通过类加载器(可以是系统提供的,也可以是用户自定义的)根据类的全名(包括包名)找到对应的.class文件。
- 读取.class文件:类加载器从文件系统或网络等位置读取.class文件的二进制数据。
- 创建Class对象:将读取到的二进制数据转换为方法区的运行时数据结构,并在堆区创建一个java.lang.Class对象,这个Class对象作为该类在JVM中的元数据表示。
链接阶段
- 验证阶段:
- 文件格式验证:验证.class文件是否符合JVM规范,是否是一个有效的字节码文件。
- 元数据验证:验证字节码中的元数据是否符合Java语言规范。
- 字节码验证:验证字节码的执行是否符合Java虚拟机规范。
- 符号引用验证:验证类中的符号引用是否有效,能否被正确解析。
- 准备阶段:为类的静态变量分配内存空间,并设置初始值(注意,这里的初始值不是代码中显式赋予的值,而是根据变量的数据类型赋予的默认值,如int为0,引用类型为null)。
- 解析阶段:将常量池中的符号引用转换为直接引用。符号引用是一个抽象的概念,如字段名、方法名等,而直接引用则是指向内存中的具体地址。
初始化阶段
- 初始化阶段是执行类构造器()方法的过程。类构造器()方法是由编译器自动收集类中的所有变量的赋值动作和静态代码块(static代码块)中的语句合并产生的。
- 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先初始初始化其父类。
- JVM会保证一个类的()方法在多线程环境中被正确的加锁和同步。因此JVM中一个类的class是线程安全的。
- 只有当类或接口的静态变量被首次主动使用时,JVM才会初始化这个类或接口。
类的主动引用(一定会发生类的初始化)
- new一个类的对象
- 调用类的静态成员(除了final常量)和静态方法
- 使用java.lang.reflect包的方法对类进行反射调用
- 当JVM启动,java Hello,则一定会初始化Hello类(即先启动main方法说在的类)
- 当初始化一个类,如果其父类没有被初始化,则会先初始化它的父类
注意:
一个类被初始化后,不会重复进行被初始化
类的被动引用(不会发生类的初始化)
- 当访问一个静态属性是,只有真正声明这个域的类才会被初始化
- 通过子类引用父类的静态变量,不会导致子类初始化
- 通过数组定义引用,不会触发此类的初始化
- 引用常量不会触发此类的初始化(因为常量在编译节点就存入调用类的常量池中了)
使用和卸载阶段
- 类被初始化后,就可以被JVM使用,创建实例对象、调用方法等。
- 当类不再被使用时,JVM会将其从内存中卸载,释放其占用的资源。
测试类的加载机制
创建一个A对象和一个B对象,使B对象继承A,测试时初始化A,查看JVM类的加载过程
A.java
package demo;
public class A {
/** 定义一个静态属性 */
public static String NAME = "A";
/** 定义一个final 静态常量 */
public static final int AGE = 20;
static {
System.out.println("A static 代码块被调用了");
NAME = "static 修改了 NAME";
}
public A(){
System.out.println("A 默认构造方法被调用了");
}
}
B.java
package demo;
public class B extends A {
/** 定义一个静态属性 */
private static String TYPE = "A";
/** 定义一个final 静态常量 */
private static final int WIDTH = 20;
static {
System.out.println("B static 代码块被调用了");
TYPE = "static 修改了 TYPE";
}
public B(){
System.out.println("B 默认构造方法被调用了");
}
}
ClassLoadDemo.java
package demo;
/**
* 测试JVM类的加载机制
*
* @author Anna.
* @date 2024/4/5 14:14
*/
public class ClassLoadDemo {
static {
System.out.println("mian方法所在类的 static 代码块被调用了");
}
public static void main(String[] args) {
new B();
System.out.println("初始化完成后-第二次调用不会重复进行初始化");
new B();
}
}
执行结果:
结论:
一个类初始化时,如果其父类没有被初始化,则先初始化其父类。
一个类被初始化后,再次被引用时,则不会重复初始化。
new 对象会默认调用类的无参构造方法
测试类的主动引用,一定会发生类的初始化
类定义使用上述A.java及B.java
- 通过new一个对象
public class ClassLoadDemo1 {
public static void main(String[] args) {
System.out.println("========1 通过new========");
new A();
}
}
执行结果:
- 调用类的静态成员(除了final常量)和静态方法
package demo;
public class ClassLoadDemo1 {
public static void main(String[] args) {
System.out.println("========2 调用类的静态成员(除了final常量)和静态方法========");
System.out.println("调用final常量:");
System.out.println("A.AGE:" + A.AGE);
System.out.println("调用非final静态成员常量:");
System.out.println("A.NAME:" + A.NAME);
}
}