05_Java 类加载过程

前言

Java 类加载是 JVM 核心底层机制之一,本质是将外部.class字节码文件加载至 JVM 内存,完成字节码→类元数据→可执行 Class 对象的转化过程,是 Java 程序运行的前置基础。

类加载的完整生命周期分为 加载(Loading)→ 链接(Linking)→ 初始化(Initialization)→ 使用(Using)→ 卸载(Unloading) 五个阶段,其中加载、链接、初始化是核心执行阶段(链接又细分为验证、准备、解析三个子阶段),也是面试与底层学习的核心考点。

一、加载(Loading):字节码→Class 对象

1. 核心作用

将类的字节码数据从外部存储介质读取到 JVM 内存,在方法区(JDK8 + 为元空间) 生成该类的元数据信息,同时在堆区 创建唯一的Class对象 ------ 该对象是 JVM 中访问类元数据的唯一入口。

2. 字节码的常见加载来源

加载来源 典型应用场景
本地文件系统 项目编译后classpath下的.class文件
压缩包(JAR/WAR/EAR) Maven/Gradle 引入的第三方依赖(如 mysql-connector.jar)
网络传输 早期 Applet 程序、远程 RPC 框架(如 Dubbo)的类传输
动态生成 反射、JDK 动态代理、ASM/CGLIB 字节码框架动态构建
数据库 / 磁盘加密文件 自定义类加载器加载加密字节码,实现类的安全防护

3. 类加载器的层次结构(双亲委派模型)

JVM 内置三类核心类加载器,严格遵循双亲委派模型(父加载器优先加载),该模型是保证 JVM 内类的唯一性、保护核心类库安全的关键。

注:JDK9 + 引入模块系统后,类加载器命名略有调整,但核心逻辑与双亲委派模型不变。

类加载器层次结构与核心属性表

类加载器类型 实现语言 核心加载路径 父加载器 关键特点
启动类加载器(Bootstrap ClassLoader) C++ $JAVA_HOME/jre/lib(如 rt.jar、core.jar 等核心类库) 无(根加载器) 无对应的 Java 类,getClassLoader() 返回 null
扩展类加载器(Extension ClassLoader) Java $JAVA_HOME/jre/lib/extjava.ext.dirs 系统属性指定路径 启动类加载器 加载 JVM 扩展功能的类库,提供核心类库外的扩展能力
应用类加载器(Application ClassLoader) Java 应用 classpath 路径(-cp/classpath 命令行参数指定) 扩展类加载器 自定义类加载器的默认父加载器,负责加载项目业务类与第三方依赖类

4. 双亲委派机制(核心规则)

执行逻辑

类加载器接收到类加载请求时,不会立即自行加载 ,而是先将请求向上委派给父加载器 ;父加载器在自身搜索范围内尝试加载,若加载失败(未找到类),则将请求向下回传,由当前加载器自行加载。

核心优势
  • 保证类的唯一性:全限定名相同的类,由同一父加载器加载,避免 JVM 内存中出现多个相同的类元数据;
  • 保护核心类库安全 :避免自定义java.lang.Stringjava.lang.Object等核心类被加载,防止恶意篡改核心类逻辑。

5. 自定义类加载器

适用场景
  • 加载非标准来源的字节码(如加密的.class、数据库 / 网络读取的字节码);
  • 实现类的热部署 / 类隔离(如 Tomcat 的 WebAppClassLoader,实现不同 Web 应用的类隔离);
  • 突破classpath限制,加载指定磁盘路径的类。
实现原则

推荐重写findClass()方法,而非loadClass() ------ 直接重写loadClass()会破坏双亲委派模型,findClass()是 ClassLoader 为自定义加载预留的方法,仅在父加载器加载失败后执行,完美遵循双亲委派。

完整可运行代码
自定义类加载器核心类
java 复制代码
package com.dwl.ex01_类加载子系统;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * @Description 实现从指定磁盘路径加载 class 字节码文件的自定义类加载器
 * 自定义类加载器
 * 继承 JDK 的 ClassLoader,重写 findClass 方法实现自定义路径的类加载
 * 核心是 classLoader 的 defineClass 方法将字节数组转为 JVM 可识别的 Class 对象
 * 注意:推荐重写findClass而非loadClass,避免破坏双亲委派模型
 * @Version 1.0.0
 * @Date 2026/2/6 1:38
 * @Author Dwl
 */
public class CustomClassLoader extends ClassLoader {

    /**
     * 重写ClassLoader的findClass方法
     * 当双亲委派模型中父类加载器均无法加载类时,会调用此方法进行自定义加载
     *
     * @param name 要加载的类的全限定类名
     * @return 加载成功的Class对象,由defineClass方法将字节码转换而来
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 定义自定义class文件的磁盘路径
            String classFilePath = "F:\\idea-workspase\\JVM-Explorer\\JDK17\\CustomClass.class";
            // 读取class文件的字节数组(核心:将磁盘上的字节码文件读入内存)
            byte[] classBytes = Files.readAllBytes(Paths.get(classFilePath));
            /*
             * 调用ClassLoader的核心方法defineClass,将字节数组转为Class对象
             * 此方法为native方法,由JVM实现,负责将字节码解析为JVM可识别的Class元数据
             * 参数说明:
             * @param name 类的全限定名,可为null(本示例直接传入参数name)
             * @param b    类的字节码数组
             * @param off  字节数组的起始偏移量(从0开始)
             * @param len  字节数组的有效长度(字节码的实际长度)
             */
            return defineClass(name, classBytes, 0, classBytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException("自定义类加载器加载类失败,类名:" + name, e);
        }
    }

}
待加载的测试类(无包名)
java 复制代码
/**
 * @Description javac -encoding UTF-8 F:\idea-workspase\JVM-Explorer\JDK17\CustomClass.java
 * @Version 1.0.0
 * @Date 2026/2/6 1:48
 * @Author Dwl
 */
public class CustomClass {

    private String msg = "自定义类加载器加载成功!";

    public CustomClass() {
        System.out.println("CustomClass 无参构造器执行");
    }

    public void showMsg() {
        System.out.println("测试方法执行:" + msg);
    }

    public String sayHello(String name) {
        return "Hello " + name + ",我是被自定义类加载器加载的类!";
    }

}
测试类(验证自定义加载逻辑)
java 复制代码
package com.dwl.ex01_类加载子系统;


import java.lang.reflect.Method;

/**
 * @Description 自定义类加载器的测试类
 * @Version 1.0.0
 * @Date 2026/2/6 1:49
 * @Author Dwl
 */
public class TestCustomClassLoader {

    public static void main(String[] args) {
        try {
            // 创建自定义类加载器的实例
            CustomClassLoader customClassLoader = new CustomClassLoader();

            // 调用loadClass方法加载类(而非直接调用findClass)
            // 原因:loadClass是ClassLoader的入口方法,会遵循双亲委派模型,先让父类加载器尝试加载
            // 父类加载器(扩展类加载器、启动类加载器)无法加载D盘的类,最终会调用我们重写的findClass
            Class<?> clazz = customClassLoader.loadClass("CustomClass");

            // 打印类的信息和加载该类的类加载器(验证是否为自定义类加载器)
            System.out.println("类的全限定名:" + clazz.getName());
            System.out.println("加载该类的类加载器:" + clazz.getClassLoader());
            // 系统类加载器(AppClassLoader)
            System.out.println("自定义类加载器的父类加载器:" + customClassLoader.getParent());
            System.out.println("------------------------");

            // 通过反射创建类的实例(调用无参构造器)
            Object instance = clazz.getDeclaredConstructor().newInstance();

            // 通过反射调用无参方法showMsg()
            Method showMsgMethod = clazz.getMethod("showMsg");
            showMsgMethod.invoke(instance);

            // 通过反射调用有参方法sayHello(String)
            Method sayHelloMethod = clazz.getMethod("sayHello", String.class);
            Object result = sayHelloMethod.invoke(instance, "Java学习者");
            System.out.println("有参方法返回结果:" + result);

        } catch (Exception e) {
            System.out.println("异常信息:" + e);
        }
    }

}
运行结果
tex 复制代码
类的全限定名:CustomClass
加载该类的类加载器:com.dwl.ex01_类加载子系统.CustomClassLoader@58372a00
自定义类加载器的父类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b
------------------------
CustomClass 无参构造器执行
测试方法执行:自定义类加载器加载成功!
有参方法返回结果:Hello Java学习者,我是被自定义类加载器加载的类!

二、链接(Linking):验证 → 准备 → 解析

链接是将加载阶段生成的类元数据 与 JVM 运行时环境绑定的过程,核心目的是确保类的结构合法、可执行,为后续初始化和使用做准备。该阶段完全由 JVM 自动执行,无程序员干预,分为三个有序子阶段。

1. 验证(Verification):安全校验

核心目的

校验字节码文件是否符合JVM 规范,防止恶意字节码(如篡改魔数、非法指令、破坏语法规则)攻击 JVM,是 JVM 的安全屏障。

四层校验流程(由浅入深)
校验层级 核心校验内容 关键校验点
文件格式验证 校验字节码文件的基础格式合法性 魔数必须为0xCAFEBABE、版本号与当前 JVM 兼容、字节码长度合法
元数据验证 校验类的语义结构合法性 不能继承final类、不能实现私有接口、父类不能为自身、方法重写需要符合规则
字节码验证 校验指令执行逻辑的合法性 操作数栈与局部变量表匹配、跳转指令指向合法位置、避免空指针 / 数组越界等运行时异常
符号引用验证 校验常量池中符号引用的有效性 引用的类 / 方法 / 字段存在、权限符合访问规则(如不能访问私有成员)

注:若校验失败,会抛出VerifyError或其子异常(如ClassFormatError),类加载过程立即终止。

2. 准备(Preparation):静态变量内存分配与默认赋值

核心目的

为类的静态变量(类变量,static修饰)方法区(元空间) 分配内存,并为其设置JVM 默认初始值(非程序员在代码中指定的赋值)。

关键规则
  1. 仅处理静态变量,实例变量的内存分配在对象创建时(实例初始化阶段)完成,与本阶段无关;
  2. 默认初始值为 JVM 为每种数据类型定义的零值,与编程语言的默认值一致;
  3. 编译期 final 常量static final且值为编译期可确定的字面量)是特例 ------ 本阶段直接赋值为程序员指定的常量值,而非零值。
典型示例(清晰对比默认值与最终值)
代码中静态变量定义 准备阶段赋值 初始化阶段赋值 赋值说明
public static int num = 10; 0(int 零值) 10 非 final 静态变量,遵循 "准备赋零值,初始化赋指定值"
public static final int NUM = 20; 20 20 编译期 final 常量,准备阶段直接赋最终值,初始化阶段无需重复赋值
public static Object obj; null(引用类型零值) 程序员指定的对象 /null 引用类型静态变量,零值固定为null
public static final int RAND_NUM = new Random().nextInt(100); 0 运行时随机数 运行期 final 常量(值编译期无法确定),按普通静态变量处理

3. 解析(Resolution):符号引用 → 直接引用

核心概念
  • 符号引用 :常量池中以字符串形式 描述的类 / 方法 / 字段引用(如java.lang.Object.toString()),仅描述目标名称,不关联实际内存地址;
  • 直接引用 :指向目标的实际内存地址 / 偏移量 / 句柄,是 JVM 运行时可直接使用的内存引用。
核心目的

将类的常量池中所有符号引用 替换为直接引用 ,建立类与依赖类、方法、字段之间的实际内存关联

解析时机

JVM 采用懒加载策略,解析时机分为两种:

  1. 静态链接 :类初始化完成解析(如静态方法、静态变量的引用);
  2. 动态链接 :类运行时(如调用多态方法)才完成解析(延迟解析),减少类加载时的性能开销。

注:解析阶段仅处理类 / 接口 / 方法 / 字段的引用,不处理实例对象的引用。

三、初始化(Initialization):执行静态代码逻辑

初始化是类加载过程中唯一真正执行程序员编写的 Java 代码 的阶段,核心作用是完成类的静态逻辑初始化,仅在类被主动引用 时触发,且仅执行一次

1. 核心作用

执行 JVM 自动生成的类构造器<clinit>()方法,完成两件核心内容:

  1. 为静态变量赋程序员在代码中指定的显式值(覆盖准备阶段的默认值);
  2. 按源码顺序执行静态代码块(static{} 中的逻辑。
关于<clinit>()方法的关键特性
  • 由 JVM自动生成 ,无需程序员手动定义,方法名是固定的<clinit>
  • 若类无静态变量显式赋值、无静态代码块,JVM不会生成 <clinit>()方法;
  • JVM 保证<clinit>()方法的线程安全 :多个线程同时初始化一个类时,仅一个线程执行<clinit>(),其余线程阻塞等待,避免静态逻辑重复执行。

2. 触发初始化的条件:主动引用(7 种)

JVM 规范明确规定:只有对类进行「主动引用」时,才会触发类的初始化;反之,「被动引用」不会触发初始化,这是面试高频考点。

7 种主动引用场景(必记)
  1. 通过new关键字创建类的实例(如new Parent());
  2. 访问 / 修改类的非 final 静态变量 (如Parent.num = 20;System.out.println(Parent.num););
  3. 调用类的静态方法 (如Parent.testMethod(););
  4. 通过Class.forName("全限定类名")显式加载类(默认参数initialize=true,触发初始化);
  5. 通过反射 API 操作类(如Class.getDeclaredField()Constructor.newInstance());
  6. 初始化子类时,若父类未初始化,则先触发父类的初始化;
  7. 执行程序入口类 (包含main()方法的类),JVM 会首先初始化该类;
  8. JDK7 + 新增:通过java.lang.invoke.MethodHandle调用类的静态方法 / 访问静态变量。

3. 不触发初始化的条件:被动引用(3 个经典场景)

被动引用指仅通过类名访问类的相关资源,但未真正使用类的自身逻辑,JVM 判定为 "无需初始化",以下是 3 个最经典的被动引用场景,附字节码验证(基于 jclasslib)。

完整测试代码
java 复制代码
package com.dwl.ex01_类加载子系统;


/**
 * @Description 测试JVM类初始化的3个经典「不触发/仅部分触发」场景
 * 遵循JVM规范:类仅在「主动使用」时触发初始化,「被动使用」不触发
 * @Version 1.0.0
 * @Date 2026/2/6 2:11
 * @Author Dwl
 */
public class JvmClassInitTriggerRuleTest {
    public static void main(String[] args) {
        // 场景1:子类访问父类的普通静态变量 → 仅父类主动使用(初始化),子类被动使用(不初始化)
        System.out.println("===== 场景1:子类访问父类普通静态变量 =====");
        System.out.println("子类访问父类静态变量的值:" + ClassInitChild.parentNormalStaticVar);
        System.out.println();

        // 场景2:创建类的数组引用 → 仅生成数组类,原类(父类)被动使用,完全不触发初始化
        System.out.println("===== 场景2:创建父类的数组引用 =====");
        ClassInitParent[] parentClassArr = new ClassInitParent[10];
        System.out.println("数组创建成功,数组的实际类型:" + parentClassArr.getClass().getName());
        System.out.println();

        // 场景3:访问类的编译期final常量 → 编译器内联常量值,原类(父类)被动使用,不触发初始化
        System.out.println("===== 场景3:访问父类编译期final常量 =====");
        System.out.println("访问父类编译期final常量的值:" + ClassInitParent.COMPILE_TIME_FINAL_CONST);
    }
}


/**
 * 父类 - 用于测试JVM类初始化触发规则
 * 包含静态变量、编译期final常量、静态代码块(初始化标识)
 */
class ClassInitParent {
    // 父类普通静态变量(非final,用于测试子类访问父类静态成员的场景)
    public static int parentNormalStaticVar = 10;
    // 父类编译期final常量(编译期可确定值,用于测试访问常量不触发初始化的场景)
    public static final int COMPILE_TIME_FINAL_CONST = 20;

    // 静态代码块:类初始化的核心标识(执行则说明类完成初始化)
    static {
        System.out.println("【ClassInitParent】父类执行初始化 <clinit> 方法");
    }
}

/**
 * 子类 - 继承父类,用于测试子类被动使用不触发自身初始化的场景
 */
class ClassInitChild extends ClassInitParent {
    // 静态代码块:子类初始化标识(若未执行,说明子类未初始化)
    static {
        System.out.println("【ClassInitChild】子类执行初始化 <clinit> 方法");
    }
}
运行结果
tex 复制代码
===== 场景1:子类访问父类普通静态变量 =====
【ClassInitParent】父类执行初始化 <clinit> 方法
子类访问父类静态变量的值:10

===== 场景2:创建父类的数组引用 =====
数组创建成功,数组的实际类型:[Lcom.dwl.ex01_类加载子系统.ClassInitParent;

===== 场景3:访问父类编译期final常量 =====
访问父类编译期final常量的值:20
场景 1:子类访问父类普通静态变量 → 仅父类初始化,子类不初始化

核心结论ClassInitChild.parentNormalStaticVar本质是通过子类访问父类静态变量 ,子类仅为 "访问入口",属于被动引用 ,父类为主动引用

字节码验证 :测试类的核心指令为getstatic #xx <ClassInitParent.parentNormalStaticVar : I>,指令直接指向父类 ,无任何子类相关的静态访问指令,因此仅触发父类<clinit>()执行,子类<clinit>()始终不执行。

场景 2:创建类的数组引用 → 原类完全不初始化

核心结论new ClassInitParent[10]并非创建ClassInitParent的实例,而是让 JVM动态生成一个数组类 (类名格式为[LClassInitParent;),原类仅作为 "数组元素类型标识",属于被动引用

字节码验证 :核心指令为anewarray #xx <ClassInitParent>,该指令仅用于创建引用类型数组,无任何访问原类静态成员、调用原类<clinit>()的操作,因此原类完全不初始化。

场景 3:访问编译期 final 常量 → 原类不初始化

核心结论 :编译期 final 常量的值在准备阶段已确定 ,编译器会将常量值直接内联 到测试类的字节码中,测试类运行时无需访问原类,原类属于被动引用

字节码验证 :核心指令为ldc #xx <20>,该指令直接从测试类自身的常量池 加载常量值20,无getstatic(访问静态变量)指令,因此原类完全不初始化。

关键区分:运行期 final 常量 (如public static final int RAND_NUM = new Random().nextInt(100))编译期无法确定值,访问时会触发原类初始化,字节码指令为getstatic

4. 继承体系下:类 + 实例初始化完整执行顺序

实际开发中,类的初始化常伴随实例化(new创建对象) ,且多存在继承关系,此时会同时触发类初始化(<clinit>()实例初始化(<init>() ,核心遵循 「静态先行、父类优先、源码顺序」 三大规则。

完整测试代码
java 复制代码
package com.dwl.ex01_类加载子系统;

/**
 * 测试继承体系下类初始化+实例初始化的完整执行顺序
 * 核心:静态初始化(类级)→ 实例初始化(对象级),每级均遵循父类优先
 * @Version 1.0.0
 * @Date 2026/2/6
 * @Author Dwl
 */
public class ClassInitInstanceInitOrderTest {
    public static void main(String[] args) {
        // 第一次new:触发类初始化+实例初始化
        System.out.println("===== 第一次new子类实例 =====");
        new InitOrderChild();

        // 第二次new:仅触发实例初始化(静态内容仅执行一次)
        System.out.println("\n===== 第二次new子类实例 =====");
        new InitOrderChild();
    }
}

/**
 * 父类:含静态变量、静态代码块、实例变量、实例代码块、构造方法
 */
class InitOrderParent {
    // 父类静态变量(类级)
    public static int parentStaticVar = 10;
    // 父类静态代码块(类级,<clinit>()一部分)
    static {
        System.out.println("1. 父类静态代码块");
    }

    // 父类实例变量(对象级)
    public int parentInstanceVar = 20;
    // 父类实例代码块(对象级,<init>()一部分)
    {
        System.out.println("3. 父类实例代码块");
    }

    // 父类无参构造方法(对象级,<init>()核心部分)
    public InitOrderParent() {
        System.out.println("4. 父类构造方法");
    }
}

/**
 * 子类:继承父类,结构与父类一致,验证执行顺序
 */
class InitOrderChild extends InitOrderParent {
    // 子类静态变量(类级)
    public static int childStaticVar = 30;
    // 子类静态代码块(类级,<clinit>()一部分)
    static {
        System.out.println("2. 子类静态代码块");
    }

    // 子类实例变量(对象级)
    public int childInstanceVar = 40;
    // 子类实例代码块(对象级,<init>()一部分)
    {
        System.out.println("5. 子类实例代码块");
    }

    // 子类无参构造方法(对象级,<init>()核心部分)
    public InitOrderChild() {
        System.out.println("6. 子类构造方法");
    }
}
运行结果
tex 复制代码
===== 第一次new子类实例 =====
1. 父类静态代码块
2. 子类静态代码块
3. 父类实例代码块
4. 父类构造方法
5. 子类实例代码块
6. 子类构造方法

===== 第二次new子类实例 =====
3. 父类实例代码块
4. 父类构造方法
5. 子类实例代码块
6. 子类构造方法

5. 底层原理拆解:两大初始化阶段(JVM 层面)

执行new InitOrderChild()的完整流程,本质分为互不干扰、顺序执行 的两大阶段,所有执行顺序均由 JVM 强制规定,类初始化阶段一定先于实例初始化阶段执行

阶段 1:类初始化阶段(类级,仅执行 1 次)

触发条件 :第一次对类进行主动引用(此处new InitOrderChild()是对子类的主动引用)。

核心执行 :JVM 自动生成的类构造器<clinit>()方法

三大核心规则

  1. 父类优先于子类:JVM 强制要求 "子类初始化前,必须先完成父类初始化",避免子类静态逻辑依赖父类静态变量但父类未初始化的问题;
  2. 按源码顺序执行 :JVM 将静态变量显式赋值语句静态代码块源码书写顺序 合并到<clinit>()中,从上到下执行;
  3. 仅执行 1 次 :类加载完成后,<clinit>()方法被 JVM 缓存,后续无论new多少次实例,均不再执行。

本案例执行细节

父类静态变量赋值(parentStaticVar=10)父类静态代码块(打印1)子类静态变量赋值(childStaticVar=30)子类静态代码块(打印2)

阶段 2:实例初始化阶段(对象级,每次 new 都执行)

触发条件 :类初始化完成后,执行new指令创建实例。

核心执行 :JVM 根据手动编写的构造方法自动生成的实例构造器<init>()方法 (每个构造方法对应一个<init>())。

三大核心规则

  1. 父类优先于子类 :JVM 会在子类<init>()方法的最开头 自动添加super()指令(调用父类无参构造器),因此必须先完成父类实例初始化,再执行子类;
  2. 按源码顺序执行 :JVM 将实例变量显式赋值语句实例代码块源码书写顺序 合并到每个<init>()方法的开头,在构造方法体执行前执行;
  3. 每次 new 都执行<init>()方法与实例对象绑定,每次创建新对象都会重新执行,无缓存。

本案例执行细节

tex 复制代码
// 父类实例初始化(由super()触发)
父类实例变量赋值(parentInstanceVar=20)→ 父类实例代码块(打印3)→ 父类构造方法体(打印4)
// 子类实例初始化(父类执行完成后)
子类实例变量赋值(childInstanceVar=40)→ 子类实例代码块(打印5)→ 子类构造方法体(打印6)

6. jclasslib 字节码验证(关键指令)

通过 IDEA 的 jclasslib 插件可直观看到 JVM 生成的<clinit>()<init>()方法,以及核心字节码指令,验证上述规则。

  1. 子类<clinit>() :无直接调用父类<clinit>()的指令,但 JVM 在执行子类<clinit>()前,会强制触发父类<clinit>()(JVM 层面规则,无需字节码);
  2. 子类<init>() :第一行指令必为invokespecial #xx <InitOrderParent.<init> : ()V>,强制调用父类无参构造器;
  3. 父类<init>() :指令顺序为bipush 20(实例变量赋值)→ 打印实例代码块 → 打印构造方法体,验证 "实例变量 + 代码块先于构造方法体执行"。



7. 拓展:带参构造与显式super()

若手动在子类构造方法中写super(xxx)(调用父类有参构造),仅需遵守一个规则,其余逻辑不变:

  • super(xxx)必须是子类构造方法的第一行代码 ,手动书写会覆盖 JVM 默认的super()(无参);
  • 实例初始化仍遵循 "父类优先",只是父类执行的是有参构造对应的<init>()方法

四、使用(Using)& 卸载(Unloading)

1. 使用(Using)

类完成初始化后,进入实际业务执行阶段 ,也是类的生命周期中持续时间最长的阶段。

核心操作:

  1. 通过new关键字创建类的实例对象;

  2. 调用类的静态方法 / 实例方法、访问静态变量 / 实例变量;

  3. 通过反射、动态代理等方式操作类或实例。

  4. 当类不再被使用时,会进入卸载阶段。

2. 卸载(Unloading)

类卸载是指 JVM 将类的元数据(方法区 / 元空间中)堆中的 Class 对象 全部回收的过程,是 JVM 的垃圾回收(GC)行为之一。类卸载的条件极其严格,缺一不可,否则会导致类元数据无法回收,引发内存泄漏。

类卸载的三个必要条件
  1. 该类的所有实例对象均已被 GC 回收,堆中无该类的任何实例;
  2. 加载该类的类加载器已被 GC 回收,无任何引用;
  3. 该类的Class 对象无任何引用(如反射代码中已释放对 Class 对象的引用,无静态变量引用该 Class 对象)。
关键注意点
  1. JVM核心类库 (如java.lang.Stringjava.lang.Object)由启动类加载器(Bootstrap) 加载,启动类加载器是 JVM 的一部分,永远不会被回收,因此核心类库的类永远不会被卸载
  2. 自定义类加载器若使用不当(如静态持有类加载器实例、反射持有 Class 对象),极易导致类卸载失败,引发元空间内存泄漏;
  3. Tomcat 等容器通过自定义类加载器实现类的热部署:卸载旧的类加载器→加载新的类加载器,从而实现不重启容器更新类逻辑。

五、类加载核心生命周期流程图

类加载完整生命周期
加载(Loading)
读取外部字节码文件
方法区生成类元数据
堆区创建Class对象(唯一访问入口)
链接(Linking)
验证:四层安全校验,保证字节码合法
准备:静态变量分配内存+赋默认值
解析:符号引用转为直接内存引用
初始化(Initialization)
触发条件:类被主动引用
执行方法
静态变量显式赋值+静态代码块执行
使用(Using)
创建实例/调用方法/访问字段
卸载(Unloading)
满足三大条件,类元数据+Class对象被GC回收

六、核心总结

  1. 类加载的核心三阶段:加载(生成 Class 对象)→ 链接(验证 / 准备 / 解析)→ 初始化(执行静态逻辑),三个阶段按顺序执行,仅初始化阶段执行程序员编写的代码;
  2. 双亲委派模型的核心是 「父加载器优先」 ,目的是保证 JVM 内类的唯一性和核心类库的安全,自定义类加载器推荐重写findClass()而非loadClass()
  3. 准备阶段为静态变量赋JVM 默认零值 ,初始化阶段赋程序员指定的显式值编译期 final 常量在准备阶段直接赋最终值,是特例;
  4. 类初始化仅由主动引用触发 ,被动引用(子类访问父类静态变量、创建类数组、访问编译期 final 常量)不触发,<clinit>()方法线程安全且仅执行一次;
  5. 继承体系下的初始化规则:静态先行、父类优先、源码顺序 ,类初始化(<clinit>())仅执行一次,实例初始化(<init>())每次 new 都执行;
  6. 类卸载的关键是类加载器、Class 对象、类实例均无任何引用,启动类加载器加载的核心类库永远不会被卸载,自定义类加载器使用不当易引发元空间内存泄漏。

Java 类加载过程 核心记忆口诀

一、类加载五阶段(核心流程)

口诀:加验准解初,各有核心务;解析可延后,初始化执码

解释

  • 加:加载(获取字节码,生成 Class 对象,堆中存引用、元空间存类元信息);
  • 验:验证(字节码 "安检",校验合法性 / 安全性,防止恶意篡改);
  • 准:准备(类变量分配内存,仅赋 JVM 默认值,无代码执行);
  • 解:解析(符号引用转直接引用,默认在初始化前,动态场景可延后);
  • 初:初始化(唯一执行代码的阶段,执行<clinit>(),赋程序员指定值)。

二、准备阶段 vs 初始化阶段(赋值核心区别)

口诀:准备赋默认,初始化赋指定;无码 vs 执码,阶段分前后

解释

  • 准备赋默认:仅给 static 类变量赋 JVM 默认值(int=0、boolean=false、引用 = null),仅分配内存,无任何代码执行;
  • 初始化赋指定:执行<clinit>()方法,给类变量赋代码中写的指定值,执行静态代码块;
  • 举例:static int num = 20; → 准备阶段 num=0,初始化阶段 num=20。

三、<clinit>() vs <init>()(构造器区别)

口诀:clinit 管类静,init 管实例;类初触发 clinit,new 出触发 init

解释

  • clinit:类构造器,处理静态变量 + 静态代码块,类初始化阶段触发,父类<clinit>()先于子类执行;
  • init:实例构造器,处理实例变量 + 构造方法,new 创建对象时触发,先执行父类<init>()
  • 补充:无静态逻辑则不生成<clinit>(),即使无自定义构造方法,JVM 也会生成默认<init>()

四、主动使用 vs 被动使用(初始化触发)

口诀:主动用才初,被动只加载;常引父静数组列,皆为被动用

解释

  • 主动用:触发类初始化(new 对象、调静态 / 反射、子类初始化、主类执行、枚举使用);
  • 被动用:仅加载类(不初始化),包括引用静态常量、子类用父类静态变量、创建类数组、loadClass()加载类;
  • 核心:被动使用仅走 "加载 - 验证 - 准备"(部分到解析),不执行<clinit>()

五、Class.forName () vs ClassLoader.loadClass ()

口诀:forName 会初始化,loadClass 只加载;传 false 可控制,JDBC 用 forName

解释

  • Class.forName("xxx"):默认触发初始化(执行<clinit>()),重载方法Class.forName("xxx", false, loader)可关闭初始化;
  • ClassLoader.loadClass("xxx"):仅执行加载阶段,不触发初始化;
  • 场景:JDBC 加载驱动用forName(),因需执行 Driver 类静态代码块完成驱动注册。

六、综合串讲口诀(整体记忆)

类加载五步走,加验准解初;

准备赋默认,初始化执码;

主动用才初,被动只加载;

clinit 管类静,init 管实例;

双亲父先求,加载只一次。

相关推荐
Scott9999HH5 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔5 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社6 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海6 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖7 小时前
JDK 26 新特性详解
java·开发语言
马优晨8 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区10 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大10 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai10 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说11 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json