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 管实例;

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

相关推荐
PPPPPaPeR.3 小时前
光学算法实战:深度解析镜片厚度对前后表面折射/反射的影响(纯Python实现)
开发语言·python·数码相机·算法
echoVic3 小时前
多模型支持的架构设计:如何集成 10+ AI 模型
java·javascript
橙露3 小时前
Java并发编程进阶:线程池原理、参数配置与死锁避免实战
java·开发语言
froginwe113 小时前
C 标准库 - `<float.h>`
开发语言
echoVic3 小时前
AI Agent 安全权限设计:blade-code 的 5 种权限模式与三级控制
java·javascript
PPPPickup3 小时前
easymall---图片上传以及图片展示
java
历程里程碑3 小时前
Linux 库
java·linux·运维·服务器·数据结构·c++·算法
Wpa.wk3 小时前
接口自动化 - 接口鉴权处理常用方法
java·运维·测试工具·自动化·接口自动化
Pluchon3 小时前
硅基计划4.0 简单模拟实现AVL树&红黑树
java·数据结构·算法