钓大鱼
123.36M · 2026-04-01
类的加载阶段总体上分为五个阶段:
.class 文件<clinit> 方法,初始化静态变量和静态代码块在加载阶段,JVM 需要完成以下三件事:
java.lang.Class 对象,作为对方法区中这些数据的访问入口类字节流的来源有以下途径:
在验证阶段,JVM 需要完成的是:确保 .class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段会做 4 个方面的内容:
.class 文件格式的规范,比如是否以 0xCAFEBABE 开头等final 修饰的类在准备阶段,JVM 会为类的静态变量分配内存并初始化为默认值 (默认的零值,如 0、0L、null、false 等)
用一个例子具体说明准备阶段的初始化默认值操作:
public class PreparationDemo {
// 准备阶段:value = 0(默认值)
// 初始化阶段:value = 123(实际值)
public static int value = 123;
// 准备阶段:CONSTANT = 123(编译期常量,直接赋值)
// 同时被 static 和 final 修饰的常量会被直接赋值
public static final int CONSTANT = 123;
}
static 和 final 修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过final 修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值;总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值在解析阶段,JVM 会把常量池内的符号引用替换为直接引用
.class 文件编译后,常量池存储的是方法的名称、类的名称
JVM 还不知道这些方法和类在内存中对应的位置
解析阶段过去后,常量池中存储的会是某个方法在方法表中的偏移量 (0x0012),某个类在方法区中的地址 (0x00012345)
JVM 知道了这些位置后,在调用时就可以直接跳转过去执行
解析的范围包括:
在初始化阶段,JVM 会执行类的初始化方法 <clinit>() 完成初始化
初始化步骤:
public class ClinitDemo {
static {
System.out.println("静态代码块 1");
}
public static int value = 123;
static {
System.out.println("静态代码块 2");
}
// 编译后生成的 <clinit>() 方法按顺序执行:
// 1. System.out.println("静态代码块 1");
// 2. value = 123; 在准备阶段,value 会被赋值为 0,在初始化阶段才按值初始化
// 3. System.out.println("静态代码块 2");
}
初始化时机:
只有当对类主动使用的时候才会导致类的初始化
// 1. 通过 new 的方式创建类实例
new MyClass();
// 2. 访问或修改类的静态变量
int value = MyClass.value;
MyClass.value = 100;
// 3. 调用类的静态方法
MyClass.staticMethod();
// 4. 使用 java.lang.reflect 包的方法对类进行反射调用
Class.forName("com.example.MyClass");
// 5. 初始化子类时,父类还没有初始化
class Parent { static {} }
class Child extends Parent { static {} }
new Child(); // 先初始化 Parent,再初始化 Child
// 6. 虚拟机启动时,用户指定的主类(包含 main 方法、单元测试方法)
public static void main(String[] args) { }
被动引用不会触发初始化
// 1. 通过子类引用父类的静态字段,只会初始化父类
class Parent {
public static int value = 123;
static { System.out.println("Parent init"); }
}
class Child extends Parent {
static { System.out.println("Child init"); }
}
int v = Child.value; // 只输出 "Parent init",Child 不会初始化
// 2. 通过数组定义引用类
Parent[] arr = new Parent[10]; // Parent 不会初始化
// 3. 引用编译期常量
class Constants {
public static final int VALUE = 123;
static { System.out.println("Constants init"); }
}
int v = Constants.VALUE; // Constants 不会初始化
graph TB
Bootstrap[启动类加载器<br/>Bootstrap ClassLoader<br/>JDK 核心类库]
Platform[扩展/平台类加载器<br/>Extension/Platform ClassLoader<br/>JDK 扩展类库]
App[应用程序类加载器<br/>App ClassLoader<br/>用户类路径 ClassPath]
Custom[自定义类加载器<br/>Custom ClassLoader]
Bootstrap --> Platform --> App --> Custom
Bootstrap ClassLoader 负责加载 $JAVA_HOME/jre/lib/ 路径下的核心类库,比如 rt.jar 等Bootstrap ClassLoader 负责加载 $JAVA_HOME/lib/modules 文件(JIMAGE 格式,包含所有 JDK 模块)-Xbootclasspath VM 参数来自定义Extension ClassLoader 只存在于 JDK 9 之前,Extension ClassLoader 负责加载 $JAVA_HOME/jre/lib/ext/ 路径下的扩展类库,比如包名 javax.* 开头的类
JDK 9 之后,Platform ClassLoader 代替了 Extension ClassLoader,负责加载 JDK 内部模块,比如包名 java.sql 开头的类
App ClassLoader 负责加载 classpath 指定(当前工作目录下)的类。如果没有自定义类加载器,那么这个就是默认的类加载器
ClassLoader classLoader = this.getClass().getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
System.out.println(classLoader.getParent().getParent());
//******** 输出 *********
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@78308db1
null
// 获取 Platform ClassLoader
ClassLoader platformClassLoader = ClassLoader.getPlatformClassLoader();
// JDK 9 之后用 Platform ClassLoader 替换了 Extension ClassLoader
System.out.println("Platform ClassLoader: " + platformClassLoader);
// 获取 Bootstrap ClassLoader
System.out.println("Platform ClassLoader's parent => Bootstrap ClassLoader: " + platformClassLoader.getParent());
// 获取 App ClassLoader
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
// JDK 9 之前的实现:sun.misc.Launcher$AppClassLoader
// JDK 9 之后的实现:jdk.internal.loader.ClassLoaders$AppClassLoader
System.out.println("App ClassLoader: " + appClassLoader);
// ******** 输出 *********
Platform ClassLoader: jdk.internal.loader.ClassLoaders$PlatformClassLoader@433c675d
Platform ClassLoader's parent => Bootstrap ClassLoader: null
App ClassLoader: jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b
可以证明我们可以获取 App ClassLoader、Extension ClassLoader / Platform ClassLoader,但是无法获取 Bootstrap ClassLoader,因为 Bootstrap ClassLoader 是使用 C 语言实现的,找不到一个确定的返回方式,所以返回 null
类加载有三种方式
Class.forName() 方法动态加载ClassLoader.loadClass() 方法动态加载Class.forName() 和 ClassLoader.loadClass() 的区别
ClassLoader.loadClass():仅将 class 文件加载到 JVM 中,只有在调用 newInstance() 方法才会去执行 static 块Class.forName():除了将类的 class 文件加载到 JVM 中之外,还会对类进行初始化,执行类中的 static 块Class.forName(name, initialize, loader):带参方法可以控制是否加载 static 块。如果 initialize 传入 false ,只有调用了 newInstance() 方法才用去执行 static 块public class Person {
static {
System.out.println("静态初始化块执行了!");
}
}
@Test
public void testLoadClass() throws ClassNotFoundException {
ClassLoader loader = this.getClass().getClassLoader();
// 使用 ClassLoader.loadClass() 来加载类,不会执行初始化块
loader.loadClass("com.example.demo.entity.Person");
}
// 没有输出,因为不会执行初始化块
@Test
public void testForName() throws ClassNotFoundException {
ClassLoader loader = this.getClass().getClassLoader();
// 使用 Class.forName() 来加载类,默认会执行初始化块
Class.forName("com.example.demo.entity.Person");
}
// 静态初始化块执行了!
@Test
public void testForNameNotInit() throws ClassNotFoundException {
ClassLoader loader = this.getClass().getClassLoader();
// 使用 Class.forName() 来加载类,并指定 initialize,初始化时不执行静态块
Class.forName("com.example.demo.entity.Person", false, loader);
}
// 没有输出,因为不会执行初始化块
sequenceDiagram
participant App as AppClassLoader
participant Platform as PlatformClassLoader
participant Bootstrap as BootstrapClassLoader
App->>App: 收到加载 MyClass 请求
App->>Platform: 委托给父加载器
Platform->>Bootstrap: 委托给父加载器
Bootstrap->>Bootstrap: 尝试加载
alt 在 Bootstrap 范围内
Bootstrap-->>Platform: 加载成功
Platform-->>App: 返回 Class 对象
else 不在 Bootstrap 范围内
Bootstrap-->>Platform: 无法加载
Platform->>Platform: 尝试加载
alt 在 Platform 范围内
Platform-->>App: 返回 Class 对象
else 不在 Platform 范围内
Platform-->>App: 无法加载
App->>App: 自己尝试加载
end
end
类加载机制特点:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 确保同一个类不会被并发加载
synchronized (getClassLoadingLock(name)) {
// 检查这个类是否被加载过,如果已加载直接返回,避免重复加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 双亲委派核心
if (parent != null) {
// 如果有父类加载器,委托给父类加载器来加载(这个过程是递归的)
c = parent.loadClass(name, false);
} else {
// 如果没有父类加载器(说明已经是 Bootstrap ClassLoader),那么尝试从核心类库路径加载这个类
// 最终依赖 native 方法 findBootstrapClass 来实现
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器无法加载这个类的时候,捕获异常但不进一步抛出
// 意味着:父类加载器无法加载,留给自己加载
}
// c 仍然是 null,说明父类无法加载,要自己加载
if (c == null) {
long t1 = System.nanoTime();
// findClass 是模板方法,加载器需要实现的核心逻辑,定义具体的查找逻辑
c = findClass(name);
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
// resolve = ture,那么执行类的连接工作(验证、准备、解析)
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派机制最大的优势是避免重复加载相同的类,并且保护核心类库
看以下例子:
// 恶意代码:尝试覆盖 String 类
package java.lang;
public class String {
public String() {
// 恶意代码
System.exit(0);
}
}
用户自定义了一个 java.lang.String 类,意图覆盖 JVM 中的正统 String 类,并且存在一些恶意代码。此时如果没有双亲委派模型,这份恶意代码就会对系统造成伤害;
有了双亲委派机制,这个类永远不会被 Bootstrap ClassLoader 加载,系统中永远只有 JDK 中的 String 类,保护了核心类库不会被覆盖
DriverManager 是 JDK 核心类,由 Bootstrap ClassLoader 负责加载。但是 MySQL JDBC 驱动 com.mysql.cj.jdbc.Driver 是第三方类库,需要由 App ClassLoader 加载,按照双亲委派模型,父类加载器无法访问加载子类加载器加载的类
解决方案:JDK 通过线程上下文的类加载器来加载 JDBC 驱动,打破双亲委派模型,保证了驱动被正确加载
// DriverManager 的静态代码块
static {
loadInitialDrivers();
}
// loadInitialDrivers 方法内部
private static void loadInitialDrivers() {
// 关键:使用线程上下文类加载器,而不是 DriverManager 自己的类加载器
// 这个类加载器通常是 App ClassLoader
AccessController.doPrivileged(() -> {
ServiceLoader<Driver> sl = ServiceLoader.load(Driver.class, classLoader);
// ...
});
}
所以 Tomcat 的做法是:每个 Web 应用启动时,给他们创建一个独立的 WebAppClassLoader,并且在加载类的时候,优先自己加载,自己找不到再寻求父类加载器
Tomcat 类加载器结构:
─────────────────────────────────────────────
BootstrapClassLoader
↓
ExtensionClassLoader
↓
ApplicationClassLoader
↓
CommonClassLoader(加载 Tomcat 公共类)
↓
┌───────────┴───────────┐
↓ ↓
CatalinaClassLoader SharedClassLoader
(加载 Tomcat 自身类) (加载共享类)
↓
WebAppClassLoader(每个 WebApp 一个)
/ |
WebApp1 WebApp2 WebApp3
Tomcat 打破双亲委派的方式:
─────────────────────────────────────────────
1. WebAppClassLoader 先自己尝试加载
2. 找不到再委托给父加载器
.class 文件,并创建对应的 java.lang.Class 对象.class 文件合法性,包括文件格式、语法语义<clinit>()public static int value = 123;
// 准备阶段:value = 0(默认值)
// 初始化阶段:value = 123(实际值)
public static final int CONSTANT = 123;
// 准备阶段:CONSTANT = 123(编译期常量,直接赋值)
// 初始化阶段:无操作
Class.forName() 方法时,传入的类会被初始化双亲委派模型是:当类加载器收到需要加载类的请求时,优先寻找父类加载器,而不是自己加载;如果父类加载器无法加载,才尝试自己加载
好处:防止代码覆盖并且可以保护核心类库。比如防止用户自定义一个 java.lang.String 去覆盖 JDK 的 String 类
loadClass() 方法