1. 引言
Java 之所以能"一次编写,到处运行",很大程度上归功于 JVM 的类加载机制。一个 .java 文件被编译成 .class 字节码后,并不会立刻进入内存,而是由类加载器在首次使用时按需加载、链接、初始化。
理解类加载过程,是排查 ClassNotFoundException、NoClassDefFoundError、ExceptionInInitializerError、静态变量初始化顺序等问题的基本功,也是掌握 SPI、热部署、字节码增强、类隔离(如 Tomcat、Dubbo)等技术的前提。
本文从 JVM 规范出发,把类加载的完整生命周期拆开讲清楚,并配合代码示例说明每个阶段到底发生了什么。
2. 类加载的整体生命周期
按照《Java 虚拟机规范》,一个类从被加载到虚拟机内存开始,到卸载出内存为止,完整生命周期包括 7 个阶段:
#mermaid-svg-rMappsiu0DR4cEk4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rMappsiu0DR4cEk4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rMappsiu0DR4cEk4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rMappsiu0DR4cEk4 .error-icon{fill:#552222;}#mermaid-svg-rMappsiu0DR4cEk4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rMappsiu0DR4cEk4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rMappsiu0DR4cEk4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rMappsiu0DR4cEk4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rMappsiu0DR4cEk4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rMappsiu0DR4cEk4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rMappsiu0DR4cEk4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rMappsiu0DR4cEk4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rMappsiu0DR4cEk4 .marker.cross{stroke:#333333;}#mermaid-svg-rMappsiu0DR4cEk4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rMappsiu0DR4cEk4 p{margin:0;}#mermaid-svg-rMappsiu0DR4cEk4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rMappsiu0DR4cEk4 .cluster-label text{fill:#333;}#mermaid-svg-rMappsiu0DR4cEk4 .cluster-label span{color:#333;}#mermaid-svg-rMappsiu0DR4cEk4 .cluster-label span p{background-color:transparent;}#mermaid-svg-rMappsiu0DR4cEk4 .label text,#mermaid-svg-rMappsiu0DR4cEk4 span{fill:#333;color:#333;}#mermaid-svg-rMappsiu0DR4cEk4 .node rect,#mermaid-svg-rMappsiu0DR4cEk4 .node circle,#mermaid-svg-rMappsiu0DR4cEk4 .node ellipse,#mermaid-svg-rMappsiu0DR4cEk4 .node polygon,#mermaid-svg-rMappsiu0DR4cEk4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rMappsiu0DR4cEk4 .rough-node .label text,#mermaid-svg-rMappsiu0DR4cEk4 .node .label text,#mermaid-svg-rMappsiu0DR4cEk4 .image-shape .label,#mermaid-svg-rMappsiu0DR4cEk4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-rMappsiu0DR4cEk4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rMappsiu0DR4cEk4 .rough-node .label,#mermaid-svg-rMappsiu0DR4cEk4 .node .label,#mermaid-svg-rMappsiu0DR4cEk4 .image-shape .label,#mermaid-svg-rMappsiu0DR4cEk4 .icon-shape .label{text-align:center;}#mermaid-svg-rMappsiu0DR4cEk4 .node.clickable{cursor:pointer;}#mermaid-svg-rMappsiu0DR4cEk4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rMappsiu0DR4cEk4 .arrowheadPath{fill:#333333;}#mermaid-svg-rMappsiu0DR4cEk4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rMappsiu0DR4cEk4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rMappsiu0DR4cEk4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rMappsiu0DR4cEk4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rMappsiu0DR4cEk4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rMappsiu0DR4cEk4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rMappsiu0DR4cEk4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rMappsiu0DR4cEk4 .cluster text{fill:#333;}#mermaid-svg-rMappsiu0DR4cEk4 .cluster span{color:#333;}#mermaid-svg-rMappsiu0DR4cEk4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rMappsiu0DR4cEk4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rMappsiu0DR4cEk4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-rMappsiu0DR4cEk4 .icon-shape,#mermaid-svg-rMappsiu0DR4cEk4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rMappsiu0DR4cEk4 .icon-shape p,#mermaid-svg-rMappsiu0DR4cEk4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rMappsiu0DR4cEk4 .icon-shape .label rect,#mermaid-svg-rMappsiu0DR4cEk4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rMappsiu0DR4cEk4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rMappsiu0DR4cEk4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rMappsiu0DR4cEk4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 加载 Loading
验证 Verification
准备 Preparation
解析 Resolution
初始化 Initialization
使用 Using
卸载 Unloading
其中验证、准备、解析 三个阶段统称为链接(Linking)。
需要强调几点:
- 加载、验证、准备、初始化、卸载这 5 个阶段的顺序是确定的。
- 解析阶段不一定严格在初始化之前,它在某些情况下可以推迟到初始化之后进行(为了支持 Java 的动态绑定)。
- 各阶段通常是互相交叉地混合进行的,而不是一个阶段完全结束后再进入下一个阶段。比如加载阶段还没结束,验证阶段可能就已经开始了。
3. 加载(Loading)
加载是类加载过程的第一个阶段。在这个阶段,JVM 需要完成三件事:
- 通过一个类的全限定名 获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构 转化为方法区的运行时数据结构。
- 在内存中生成一个代表该类的
java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
3.1 字节流的来源
注意规范里说的是"通过全限定名获取二进制字节流",并没有限定必须从 Class 文件读取。这就为各种灵活的技术留下了空间:
- 从 ZIP 包读取(Jar、War,最常见)
- 从网络获取(Applet、Web 服务)
- 运行时动态生成(动态代理、CGLIB、ASM)
- 由其他文件生成(JSP 编译成 Class)
- 从数据库读取(中间件常见方案)
3.2 数组类的特殊性
数组类本身不通过类加载器创建,而是由 JVM 直接在内存中动态构造。但数组的元素类型(去掉所有维度后的类型)最终还是要靠类加载器去加载。
3.3 加载完成后
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机设定的格式存储在方法区之中,然后在堆内存中实例化一个 java.lang.Class 类的对象,这个对象将作为程序访问方法区中这些类型数据的外部接口。
4. 验证(Verification)
验证是链接阶段的第一步,目的是确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
因为 Class 文件并不一定由 Java 编译器生成,完全可能被恶意篡改或手工构造,所以验证是必要的防护。
验证阶段大致会完成下面 4 个动作:
4.1 文件格式验证
验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。例如:
- 是否以魔数
0xCAFEBABE开头 - 主次版本号是否在当前虚拟机处理范围之内
- 常量池中的常量是否有不被支持的常量类型
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
只有通过了文件格式验证,字节流才会进入内存的方法区进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构进行的。
4.2 元数据验证
对类的元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。例如:
- 这个类是否有父类(除了
java.lang.Object之外,所有类都应当有父类) - 这个类的父类是否继承了不允许被继承的类(被
final修饰的类) - 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
4.3 字节码验证
这是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如:
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
- 保证跳转指令不会跳转到方法体以外的字节码指令上
- 保证方法体中的类型转换是有效的
4.4 符号引用验证
这个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,例如:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的访问性是否可被当前类访问
5. 准备(Preparation)
准备阶段是正式为类变量(被 static 修饰的变量)分配内存并设置类变量初始值的阶段。
这里有两个容易踩坑的细节:
5.1 只分配类变量,不分配实例变量
准备阶段进行内存分配的仅包括类变量 ,而实例变量会在对象实例化时随着对象一起分配在 Java 堆中。
5.2 "初始值"通常是零值
这里的"初始值"在通常情况下是数据类型的零值,而不是代码里显式赋的值。
java
public static int value = 123;
变量 value 在准备阶段过后的初始值是 0,而不是 123。把 value 赋值为 123 的动作要到初始化阶段才会执行。
5.3 特殊情况:ConstantValue
如果类字段的字段属性表中存在 ConstantValue 属性,那在准备阶段变量值就会被初始化为 ConstantValue 属性所指定的初始值。
java
public static final int VALUE = 123;
对于 static final 修饰的基本类型或 String 常量,编译期就会把结果放入常量池,并在字段属性表中标记 ConstantValue,因此准备阶段 VALUE 就已经是 123。
注意:
static final修饰的对象引用(如static final List<String> LIST = new ArrayList<>())不属于这种情况,它仍然要到初始化阶段才赋值。
6. 解析(Resolution)
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用(Symbolic Reference):以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。它与虚拟机实现的内存布局无关。
- 直接引用(Direct Reference):可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这 7 类符号引用进行。
6.1 解析的时机
虚拟机规范并未规定解析阶段发生的具体时间,只要求在执行 anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield 和 putstatic 这 17 个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。
所以虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。
6.2 缓存机制
对同一个符号引用进行多次解析请求是很常见的事情,除 invokedynamic 指令以外,虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态),从而避免解析动作重复进行。
7. 初始化(Initialization)
初始化阶段是类加载过程的最后一个步骤,也是真正开始执行类中定义的 Java 程序代码的阶段。
从代码角度看,初始化阶段是执行类构造器 <clinit>() 方法的过程。
7.1 <clinit>() 方法的特点
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作 和静态语句块(static {}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的。- 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
<clinit>()方法与类的构造函数(<init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。- 由于父类的
<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。 <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。- 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成
<clinit>()方法,但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。 - 虚拟机会保证一个类的
<clinit>()方法在多线程环境中被正确地加锁、同步。
7.2 代码示例:静态块的执行顺序
java
public class Parent {
static {
System.out.println("Parent 静态块");
}
public static int parentValue = 1;
}
public class Child extends Parent {
static {
System.out.println("Child 静态块");
}
public static int childValue = 2;
}
public class Main {
public static void main(String[] args) {
System.out.println("开始");
System.out.println(Child.childValue);
}
}
输出结果:
text
开始
Parent 静态块
Child 静态块
2
可以看到,访问 Child.childValue 触发了 Child 的初始化,而 Child 初始化前必须先完成 Parent 的初始化。
7.3 代码示例:静态块的顺序陷阱
java
public class StaticOrderDemo {
static {
// 编译通过,但输出 0,因为此时 i 还未被赋值
System.out.println("静态块中 i = " + i);
}
static int i = 10;
public static void main(String[] args) {
System.out.println("main 中 i = " + i);
}
}
输出结果:
text
静态块中 i = 0
main 中 i = 10
原因在于:准备阶段 i 被赋为零值 0,初始化阶段按源码顺序执行,先执行静态块(此时 i 仍是 0),再执行 i = 10 的赋值。
8. 类初始化的触发时机
JVM 规范严格规定了有且只有以下 6 种情况必须立即对类进行初始化(而加载、验证、准备自然需要在此之前开始):
- 遇到
new、getstatic、putstatic或invokestatic这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应到 Java 代码场景是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。 - 使用
java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。 - 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含
main()方法的那个类),虚拟机会先初始化这个主类。 - 当使用 JDK 7 的动态语言支持时,如果一个
java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。 - 当一个接口中定义了 JDK 8 新加入的默认方法(被
default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
8.1 被动引用:不会触发初始化
以下三种情况属于被动引用,不会触发类的初始化:
情况一:通过子类引用父类的静态字段,不会导致子类初始化
java
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
输出结果:
text
SuperClass init!
123
只有 SuperClass 被初始化,SubClass 没有。
情况二:通过数组定义来引用类,不会触发此类的初始化
java
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}
这段代码运行后不会输出 SuperClass init!,因为它只触发了 [LSuperClass 这个数组类的初始化,而不是 SuperClass 本身。
情况三:常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类
java
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
运行后不会输出 ConstClass init!,因为常量 HELLOWORLD 在编译阶段已经通过常量传播优化,存入了 NotInitialization 的常量池中。
9. 类加载器与双亲委派模型
类加载过程需要由**类加载器(ClassLoader)**来完成。JVM 提供了三层类加载器:
#mermaid-svg-uhOAjPYKePsl9ImB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uhOAjPYKePsl9ImB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uhOAjPYKePsl9ImB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uhOAjPYKePsl9ImB .error-icon{fill:#552222;}#mermaid-svg-uhOAjPYKePsl9ImB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uhOAjPYKePsl9ImB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uhOAjPYKePsl9ImB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uhOAjPYKePsl9ImB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uhOAjPYKePsl9ImB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uhOAjPYKePsl9ImB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uhOAjPYKePsl9ImB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uhOAjPYKePsl9ImB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uhOAjPYKePsl9ImB .marker.cross{stroke:#333333;}#mermaid-svg-uhOAjPYKePsl9ImB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uhOAjPYKePsl9ImB p{margin:0;}#mermaid-svg-uhOAjPYKePsl9ImB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-uhOAjPYKePsl9ImB .cluster-label text{fill:#333;}#mermaid-svg-uhOAjPYKePsl9ImB .cluster-label span{color:#333;}#mermaid-svg-uhOAjPYKePsl9ImB .cluster-label span p{background-color:transparent;}#mermaid-svg-uhOAjPYKePsl9ImB .label text,#mermaid-svg-uhOAjPYKePsl9ImB span{fill:#333;color:#333;}#mermaid-svg-uhOAjPYKePsl9ImB .node rect,#mermaid-svg-uhOAjPYKePsl9ImB .node circle,#mermaid-svg-uhOAjPYKePsl9ImB .node ellipse,#mermaid-svg-uhOAjPYKePsl9ImB .node polygon,#mermaid-svg-uhOAjPYKePsl9ImB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uhOAjPYKePsl9ImB .rough-node .label text,#mermaid-svg-uhOAjPYKePsl9ImB .node .label text,#mermaid-svg-uhOAjPYKePsl9ImB .image-shape .label,#mermaid-svg-uhOAjPYKePsl9ImB .icon-shape .label{text-anchor:middle;}#mermaid-svg-uhOAjPYKePsl9ImB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uhOAjPYKePsl9ImB .rough-node .label,#mermaid-svg-uhOAjPYKePsl9ImB .node .label,#mermaid-svg-uhOAjPYKePsl9ImB .image-shape .label,#mermaid-svg-uhOAjPYKePsl9ImB .icon-shape .label{text-align:center;}#mermaid-svg-uhOAjPYKePsl9ImB .node.clickable{cursor:pointer;}#mermaid-svg-uhOAjPYKePsl9ImB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uhOAjPYKePsl9ImB .arrowheadPath{fill:#333333;}#mermaid-svg-uhOAjPYKePsl9ImB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uhOAjPYKePsl9ImB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uhOAjPYKePsl9ImB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uhOAjPYKePsl9ImB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uhOAjPYKePsl9ImB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uhOAjPYKePsl9ImB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uhOAjPYKePsl9ImB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uhOAjPYKePsl9ImB .cluster text{fill:#333;}#mermaid-svg-uhOAjPYKePsl9ImB .cluster span{color:#333;}#mermaid-svg-uhOAjPYKePsl9ImB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uhOAjPYKePsl9ImB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uhOAjPYKePsl9ImB rect.text{fill:none;stroke-width:0;}#mermaid-svg-uhOAjPYKePsl9ImB .icon-shape,#mermaid-svg-uhOAjPYKePsl9ImB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uhOAjPYKePsl9ImB .icon-shape p,#mermaid-svg-uhOAjPYKePsl9ImB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uhOAjPYKePsl9ImB .icon-shape .label rect,#mermaid-svg-uhOAjPYKePsl9ImB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uhOAjPYKePsl9ImB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uhOAjPYKePsl9ImB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uhOAjPYKePsl9ImB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Bootstrap ClassLoader 启动类加载器
Extension ClassLoader 扩展类加载器
Application ClassLoader 应用程序类加载器
自定义 ClassLoader
- 启动类加载器(Bootstrap ClassLoader) :由 C++ 实现,是虚拟机自身的一部分,负责加载
<JAVA_HOME>/lib目录下的核心类库。 - 扩展类加载器(Extension ClassLoader) :由
sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>/lib/ext目录下的类库。 - 应用程序类加载器(Application ClassLoader) :由
sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径(ClassPath)上所有的类库。
9.1 双亲委派模型的工作过程
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。工作过程是:
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
- 每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。
- 只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
9.2 双亲委派的好处
- 避免类重复加载:Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。
- 保证核心类库安全 :例如
java.lang.Object存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
9.3 自定义类加载器示例
java
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
public class CustomClassLoader extends ClassLoader {
private final String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadClassData(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
throw new ClassNotFoundException(name, e);
}
}
private byte[] loadClassData(String name) throws Exception {
String fileName = classPath + name.replace('.', '/') + ".class";
try (InputStream in = new FileInputStream(fileName);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
}
}
public static void main(String[] args) throws Exception {
CustomClassLoader loader = new CustomClassLoader("/tmp/classes/");
Class<?> clazz = loader.loadClass("com.example.Demo");
Object instance = clazz.getDeclaredConstructor().newInstance();
System.out.println(instance.getClass().getClassLoader());
}
}
关键点在于重写 findClass 而不是 loadClass,这样仍然保留双亲委派的逻辑;如果重写 loadClass,就可以打破双亲委派(Tomcat 的 WebappClassLoader 就是这么做的)。
10. 常见问题与排查思路
10.1 ClassNotFoundException 与 NoClassDefFoundError 的区别
| 异常类型 | 触发时机 | 典型原因 |
|---|---|---|
ClassNotFoundException |
显式加载类时(Class.forName、ClassLoader.loadClass) |
类路径缺失、类名拼写错误、依赖未引入 |
NoClassDefFoundError |
隐式加载类时(new、静态方法调用) |
类在编译时存在但运行时缺失,或该类初始化失败 |
10.2 ExceptionInInitializerError
当 <clinit>() 方法执行过程中抛出异常时,JVM 会抛出 ExceptionInInitializerError。之后再次访问该类,会直接抛出 NoClassDefFoundError,因为类已经处于错误状态,不会再次尝试初始化。
排查思路:查看 ExceptionInInitializerError 的 getCause(),定位静态块或静态变量初始化中的具体异常。
10.3 静态变量初始化顺序问题
记住两条规则:
- 准备阶段赋零值,初始化阶段按源码顺序赋值。
- 父类静态块优先于子类静态块,父类静态变量优先于子类静态变量。
11. 总结
把类加载过程串起来看:
| 阶段 | 核心动作 | 关键点 |
|---|---|---|
| 加载 | 获取字节流、生成 Class 对象 | 字节流来源多样,数组类特殊处理 |
| 验证 | 文件格式、元数据、字节码、符号引用验证 | 保障虚拟机安全 |
| 准备 | 为类变量分配内存并设零值 | static final 常量走 ConstantValue |
| 解析 | 符号引用转直接引用 | 时机灵活,可缓存 |
| 初始化 | 执行 <clinit>() |
父类优先、按源码顺序、线程安全 |
理解这条链路,不仅能帮你读懂 JVM 规范,更能在遇到类加载相关异常时快速定位问题。下一步可以继续深入类加载器隔离 、打破双亲委派的实践场景 (如 SPI、OSGi、Tomcat)以及字节码增强技术。