这里写目录标题
- 一、类加载的概念
- 类加载器核心信息总结表
- 二、类加载的时机
-
- 1、触发类加载的六大场景
- 2、不会触发类加载的特殊情况
-
- [引用类的常量(final static)](#引用类的常量(final static))
- 通过数组定义引用类
- 类名.class语法
- 3、常见误区
- 4、总结
- 三、类加载的过程
- 四、类加载器
-
- [3 种默认类加载器](#3 种默认类加载器)
-
- [1. **Bootstrap ClassLoader**(启动类加载器)](#1. Bootstrap ClassLoader(启动类加载器))
- [2. **Extension ClassLoader**(扩展类加载器)](#2. Extension ClassLoader(扩展类加载器))
- [3. **Application/System ClassLoader**(应用/系统类加载器)](#3. Application/System ClassLoader(应用/系统类加载器))
- [4. **自定义ClassLoader**](#4. 自定义ClassLoader)
- 双亲委派模型
- 打破双亲委派的场景
一、类加载的概念
类加载 是指JVM将类的字节码文件(.class)从磁盘加载到内存 ,并进行验证、准备、解析和初始化,最终形成可以被JVM直接使用的Java类型的过程。

1、核心特征
类加载的三个核心特性:
- 延迟加载(Lazy Loading)
- 类只在第一次使用时才加载
- 不是程序启动时就加载所有类
- 缓存机制(Caching)
- 已加载的类会缓存,避免重复加载
- 缓存Key: 全限定类名 + ClassLoader实例
3. 唯一性(Uniqueness)
- 同一个类在同一个ClassLoader下只加载一次
- 不同ClassLoader加载的类视为不同类
2、为什么需要类加载
类加载器核心信息总结表
| 类加载器 | 作用范围 | 加载路径 |
|---|---|---|
| 启动类加载器(Bootstrap) | 加载 JVM 核心类(比如java.lang.String) |
JAVA_HOME/jre/lib/rt.jar 等核心 jar |
| 扩展类加载器(Extension) | 加载 JVM 扩展类 | JAVA_HOME/jre/lib/ext 目录 |
| 应用程序类加载器(App) | 加载用户编写的类和第三方 jar | 应用 classpath 路径 |
要不要我帮你补充一份类加载器工作流程示意图,直观展示双亲委派模型的执行顺序?
当前文件内容过长,豆包只阅读了前 33%。
- 平台无关性:
- 同一份字节码在不同平台运行
- .class文件是平台中立的
- 安全性:
- 防止恶意字节码破坏JVM
- 需要验证字节码的合法性
- 动态性:
- 支持动态加载、热替换
- 如OSGi、热部署
- 内存管理:
- 合理使用内存,按需加载
- 不是一次性加载所有类
- 优化:
- 字节码优化、即时编译
- 提高执行效率
二、类加载的时机
JVM采用延迟加载(Lazy Loading) 策略。这意味着类不会在程序启动时就全部加载到内存中,而是在第一次真正需要使用的时候才加载。
java
public class LazyLoadingDemo {
public static void main(String[] args) {
System.out.println("程序开始运行");
// 此时User类还没有被加载
System.out.println("准备使用User类...");
// 只有当第一次使用时,User类才会被加载
User user = new User();
user.sayHello();
System.out.println("程序结束");
}
}
class User {
static {
System.out.println("User类被加载并初始化了!");
}
public void sayHello() {
System.out.println("Hello from User!");
}
}
运行上面的代码,你会看到:
plain
程序开始运行
准备使用User类...
User类被加载并初始化了!
Hello from User!
程序结束
1、触发类加载的六大场景
虽然JVM采用延迟加载策略,但在某些特定情况下,类会被触发加载。以下是六种会触发类加载的场景:
1、创建类的实例
java
// 当使用new关键字时,类会被加载
MyClass obj = new MyClass();
// 反射创建实例也会触发加载
Class<?> clazz = Class.forName("com.example.MyClass");
Object obj = clazz.newInstance();
2、访问类的静态成员
java
class MyClass {
static int staticField = 10; // 非final静态字段
static final int CONSTANT = 20; // final静态常量
static {
System.out.println("MyClass静态代码块执行");
}
}
public class Main {
public static void main(String[] args) {
// 情况A:访问非final静态字段(会触发加载)
System.out.println(MyClass.staticField);
// 输出:MyClass静态代码块执行\n10
// 情况B:访问final静态常量(不会触发加载!)
System.out.println(MyClass.CONSTANT);
// 只输出:20,不执行静态代码块
}
}
重要区别:
- 访问非final静态字段:会触发类加载和初始化
- 访问final静态常量:不会触发类加载(编译器直接内联替换)
3、调用类的静态方法
java
class Utility {
static {
System.out.println("Utility类被加载");
}
public static void helper() {
System.out.println("调用helper方法");
}
}
// 调用静态方法会触发类加载
Utility.helper(); // 输出:Utility类被加载\n调用helper方法
4、反射调用(Class.forName)
java
// 使用Class.forName显式加载类
Class<?> clazz = Class.forName("com.example.MyClass");
// 使用ClassLoader.loadClass也会加载(但不一定初始化)
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> clazz2 = loader.loadClass("com.example.MyClass");
**注意Class.forName()默认会执行初始化,而ClassLoader.loadClass()只加载不初始化。
5、初始化子类(父类先被加载)
java
class Parent {
static {
System.out.println("Parent类被加载");
}
}
class Child extends Parent {
static {
System.out.println("Child类被加载");
}
}
// 初始化子类时,父类会先被加载
Child child = new Child();
// 输出:Parent类被加载\nChild类被加载
VM会保证:父类的初始化一定在子类之前完成。
6、JVM启动时的主类
java
# 当你运行java命令时,指定的主类会被加载
java com.example.MainApp
# MainApp类会在程序启动时立即加载
2、不会触发类加载的特殊情况
了解什么情况会触发类加载很重要,但了解什么情况不会触发同样重要!
引用类的常量(final static)
java
class Constants {
// 编译时常量(基本类型或String的字面量)
public static final int MAX_SIZE = 1024;
public static final String APP_NAME = "MyApp";
// 运行时常量(需要计算)
public static final long CURRENT_TIME = System.currentTimeMillis();
static {
System.out.println("Constants类被加载");
}
}
public class Main {
public static void main(String[] args) {
// 不会触发加载(编译器直接替换为字面量)
System.out.println(Constants.MAX_SIZE); // 直接输出1024
System.out.println(Constants.APP_NAME); // 直接输出"MyApp"
// 会触发加载(需要运行时计算)
System.out.println(Constants.CURRENT_TIME); // 输出:Constants类被加载\n时间戳
}
}
通过数组定义引用类
java
class MyClass {
static {
System.out.println("MyClass被加载");
}
}
// 创建数组不会触发元素类的加载
MyClass[] array = new MyClass[10]; // 不输出任何东西
// 只有当真正创建数组元素时才会加载
array[0] = new MyClass(); // 输出:MyClass被加载
类名.class语法
java
// 获取Class对象不会触发初始化(但会触发加载)
Class<?> clazz = MyClass.class; // 只加载,不初始化
// 如果要验证是否已加载,可以使用-XX:+TraceClassLoading
3、常见误区
误区1:"程序启动时会加载所有需要的类"
真相:JVM按需加载,只有用到的类才会被加载。你可以通过监控验证:
java
# 添加这些JVM参数观察类加载
-XX:+TraceClassLoading
-XX:+PrintGCDetails
误区2:"import语句会触发类加载"
真相:import只是编译时的声明,不会导致运行时加载。
java
import java.util.ArrayList; // 这不会导致ArrayList被加载
public class Main {
public static void main(String[] args) {
// 只有这里使用时才会加载
ArrayList<String> list = new ArrayList<>();
}
}
误区3:"子类被加载时,父类的方法也会被加载"
真相:父类的方法只有在被调用时才会解析和验证,不是加载子类时就全部加载。
4、总结
理解类加载时机对于编写高效、可维护的Java程序至关重要。记住这些关键点:
- 延迟加载是默认策略:类在第一次真正使用时才加载
- 六种触发场景:new、访问静态成员、调用静态方法、反射、子类初始化、主类
- final常量是特例:不会触发类加载(如果是编译时常量)
- 数组和import不触发:它们不会导致类被加载
- 父子类加载顺序:父类先于子类加载和初始化
通过合理利用类加载机制,我们可以:
- ✅ 优化启动性能:减少不必要的类加载
- ✅ 实现热部署:动态重新加载修改的类
- ✅ 构建插件系统:独立加载和卸载功能模块
- ✅ 节省内存:及时卸载不再需要的类
类加载机制是JVM优雅设计的一部分,理解它不仅能帮助你写出更好的代码,还能在遇到相关问题时快速定位和解决。
三、类加载的过程
1、类为什么要"加载"
在理解类加载过程之前,我们需要明白一个核心问题:为什么需要类加载?
java
// 我们写的Java代码
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
// 编译后的字节码(部分)
cafe babe 0000 0034 0010 0a00 0300 0d07
000e 0700 0f01 0006 3c69 6e69 743e 0100
...
字节码不能直接执行,它需要经过JVM的"翻译"和"准备"。类加载就是这个翻译和准备的过程,主要解决四个问题:
- 平台无关性:同一份.class文件在不同平台运行
- 安全性:防止恶意字节码破坏JVM
- 内存管理:按需加载,合理使用内存
- 动态性:支持热部署、动态代理等高级特性
2、类加载的生命周期
类加载的生命周期如下:

前5个阶段(加载、验证、准备、解析、初始化)构成了类加载的核心过程,后2个阶段是类的使用和回收。
1、加载
加载是类加载过程的第一步,主要完成三件事:
1 加载的三大任务
加载的三大任务:
- 任务1:获取类的二进制字节流
- 任务2:将字节流转换为方法区的运行时数据结构
- 任务3:在堆中创建Class对象,作为访问入口
java
public class LoadingPhase {
public Class<?> loadClass(String className) throws ClassNotFoundException {
// 任务1:获取类的二进制字节流
byte[] bytecode = getClassByteStream(className);
// 任务2:将字节流转换为方法区的运行时数据结构
ClassMetadata metadata = convertToRuntimeStructure(bytecode);
// 任务3:在堆中创建Class对象,作为访问入口
Class<?> classObj = createClassObject(metadata);
return classObj;
}
// 字节流可以从多种来源获取
private byte[] getClassByteStream(String className) {
Sources sources = {
1. 本地文件系统(最常见)
2. ZIP/JAR/WAR包
3. 网络下载(Applet)
4. 运行时计算生成(动态代理)
5. 数据库或加密文件
6. 其他文件(JSP生成的类)
};
// 根据来源获取字节流
return fetchBytesFromSource(className, sourceType);
}
}
2 加载阶段的独特之处
加载阶段有一个重要特点:它是唯一一个可以由开发人员干预的阶段。通过自定义类加载器,我们可以控制从哪里获取字节流。
java
// 自定义类加载器示例
public class CustomClassLoader extends ClassLoader {
private String classPath;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 从自定义位置读取字节码
byte[] data = loadClassData(name);
// 调用defineClass完成加载
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
private byte[] loadClassData(String className) throws IOException {
// 自定义加载逻辑...
String path = className.replace('.', '/') + ".class";
FileInputStream fis = new FileInputStream(classPath + "/" + path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
fis.close();
return baos.toByteArray();
}
}
2、验证
验证是安全防线,确保加载的字节码不会危害JVM。它包含四个层次:
文件格式验证
java
class FileFormatVerifier {
void verify(byte[] bytecode) throws ClassFormatError {
// 检查1:魔数(Magic Number)
// .class文件前4个字节必须是0xCAFEBABE
if (bytecode[0] != (byte)0xCA || bytecode[1] != (byte)0xFE ||
bytecode[2] != (byte)0xBA || bytecode[3] != (byte)0xBE) {
throw new ClassFormatError("Invalid magic number");
}
// 检查2:版本号
int minorVersion = ((bytecode[4] & 0xFF) << 8) | (bytecode[5] & 0xFF);
int majorVersion = ((bytecode[6] & 0xFF) << 8) | (bytecode[7] & 0xFF);
if (majorVersion > 61) { // Java 17对应的主版本号
throw new UnsupportedClassVersionError(
"Unsupported version: " + majorVersion + "." + minorVersion);
}
// 检查3:常量池tag验证
verifyConstantPoolTags(bytecode);
// 检查4:索引引用是否指向有效位置
verifyIndexReferences(bytecode);
}
}
元数据验证
java
class MetadataVerifier {
void verify(ClassMetadata metadata) {
// 检查1:类是否有父类(除Object外)
if (!metadata.className.equals("java/lang/Object") &&
metadata.superClassName == null) {
throw new IncompatibleClassChangeError("类必须有父类");
}
// 检查2:是否继承了final类
if (metadata.superClass != null &&
metadata.superClass.isFinal()) {
throw new VerifyError("不能继承final类");
}
// 检查3:是否实现了抽象方法
verifyAbstractMethods(metadata);
// 检查4:字段/方法是否与父类冲突
verifyOverrideRules(metadata);
}
}
字节码验证
字节码验证使用数据流分析技术,确保程序在运行时不会出现以下问题:
java
class BytecodeVerifier {
void verify(byte[] bytecode) {
// 创建控制流图
ControlFlowGraph cfg = buildControlFlowGraph(bytecode);
// 数据流分析
for (BasicBlock block : cfg.getBlocks()) {
// 模拟执行,验证类型正确性
simulateExecution(block);
// 检查操作数栈
verifyOperandStack(block);
// 检查局部变量表
verifyLocalVariables(block);
// 检查跳转目标
verifyBranchTargets(block);
}
// 验证StackMapTable属性(Java 6+)
verifyStackMapTable(bytecode);
}
// 示例:验证操作数栈
void verifyOperandStack(BasicBlock block) {
// 跟踪栈的深度和类型
Stack<Type> operandStack = new Stack<>();
for (Instruction ins : block.getInstructions()) {
switch (ins.getOpcode()) {
case ILOAD: // 加载int到栈
operandStack.push(Type.INT);
break;
case IADD: // int加法
if (operandStack.size() < 2) {
throw new VerifyError("栈下溢");
}
Type type1 = operandStack.pop();
Type type2 = operandStack.pop();
if (type1 != Type.INT || type2 != Type.INT) {
throw new VerifyError("操作数类型错误");
}
operandStack.push(Type.INT);
break;
case IRETURN: // 返回int
if (operandStack.isEmpty()) {
throw new VerifyError("栈为空");
}
Type returnType = operandStack.pop();
if (returnType != Type.INT) {
throw new VerifyError("返回类型错误");
}
break;
// ... 其他指令验证
}
// 检查栈深度是否超限(最大65535)
if (operandStack.size() > 65535) {
throw new VerifyError("操作数栈溢出");
}
}
}
}
符号引用验证
java
class SymbolicReferenceVerifier {
void verify(ClassMetadata metadata, ClassLoader loader) {
// 验证所有符号引用是否可以被解析
// 1. 验证类引用
for (String className : metadata.referencedClasses) {
try {
loader.loadClass(className.replace('/', '.'));
} catch (ClassNotFoundException e) {
throw new NoClassDefFoundError("类不存在: " + className);
}
}
// 2. 验证字段引用
for (FieldRef ref : metadata.fieldReferences) {
verifyFieldExists(ref);
}
// 3. 验证方法引用
for (MethodRef ref : metadata.methodReferences) {
verifyMethodExists(ref);
}
}
}
3、准备
准备阶段为类变量(static变量)分配内存并设置初始值 。这里有个重要概念:初始值 ≠ 代码中的赋值值。
准备阶段的核心逻辑
java
class PreparationPhase {
void prepare(ClassMetadata metadata) {
// 为所有static变量分配内存并设置零值
for (FieldInfo field : metadata.staticFields) {
// 根据类型分配内存
MemoryAllocation allocation = allocateMemory(field);
// 设置零值(Zero Value)
// 注意:此时不执行任何Java代码的赋值操作!
setZeroValue(field, allocation.address);
// 特殊情况处理
handleSpecialCases(field, allocation);
}
}
// 零值规则
void setZeroValue(FieldInfo field, long address) {
switch (field.getType()) {
case "I": // int
case "S": // short
case "B": // byte
case "C": // char
case "Z": // boolean
writeInt(address, 0);
break;
case "J": // long
writeLong(address, 0L);
break;
case "F": // float
writeFloat(address, 0.0f);
break;
case "D": // double
writeDouble(address, 0.0);
break;
case "L": // 引用类型
case "[":
writeReference(address, null);
break;
}
}
// 特殊情况:final static常量
void handleSpecialCases(FieldInfo field, MemoryAllocation allocation) {
if (field.isFinal() && field.isStatic()) {
// 检查是否有ConstantValue属性
ConstantValueAttribute constAttr = field.getAttribute(
ConstantValueAttribute.class);
if (constAttr != null) {
// 对于编译时常量,在准备阶段直接赋实际值
Object constantValue = constAttr.getValue();
setFieldValue(field, allocation.address, constantValue);
// 记录为编译时常量(后续可能被内联优化)
markAsCompileTimeConstant(field);
}
}
}
}
常见误解
java
public class PreparationMisunderstanding {
// 误解:准备阶段会把value设为10
// 真相:准备阶段value=0,初始化阶段才设为10
static int value = 10;
// 特殊情况:final static常量
// 准备阶段MAX就直接是100
static final int MAX = 100;
// 另一个特殊情况:非编译时常量
// 准备阶段TIME=0,初始化阶段才计算
static final long TIME = System.currentTimeMillis();
static {
System.out.println("静态代码块执行");
}
public static void main(String[] args) {
// 这里会输出什么?
System.out.println("value = " + value); // 10
System.out.println("MAX = " + MAX); // 100
System.out.println("TIME = " + TIME); // 时间戳
}
}
运行顺序:
- 准备阶段:value=0, MAX=100,TIME=0
- 初始化阶段:执行静态代码块和赋值 → value=10, TIME=当前时间戳
4、解析
解析阶段将符号引用转换为直接引用。这是连接静态存储和动态运行的关键步骤。
什么是符号引用和直接引用?
java
class ReferenceExample {
// 符号引用:一种描述性的引用
SymbolicReference symRef = new SymbolicReference(
"java/lang/String", // 类名
"length", // 字段名
"()I" // 描述符
);
// 直接引用:指向内存的具体指针
DirectReference dirRef = new DirectReference(
0x7f123456, // 方法区中的类地址
0x1000, // 方法在vtable中的偏移量
null // 可能的内联缓存信息
);
}
解析过程
java
class ResolutionPhase {
void resolveAll(ClassMetadata metadata, ClassLoader loader) {
// 1. 类或接口解析
resolveClasses(metadata.classReferences, loader);
// 2. 字段解析
resolveFields(metadata.fieldReferences);
// 3. 方法解析
resolveMethods(metadata.methodReferences);
// 4. 接口方法解析
resolveInterfaceMethods(metadata.interfaceMethodReferences);
}
// 类解析示例
Class<?> resolveClass(String className, ClassLoader loader,
Class<?> fromClass) {
// 步骤1:加载目标类
Class<?> targetClass;
try {
targetClass = loader.loadClass(className);
} catch (ClassNotFoundException e) {
throw new NoClassDefFoundError("类不存在: " + className);
}
// 步骤2:检查访问权限
checkAccessPermission(fromClass, targetClass);
// 步骤3:验证兼容性
verifyCompatibility(fromClass, targetClass);
// 步骤4:缓存解析结果
cacheResolution(className, targetClass);
return targetClass;
}
// 方法解析(考虑虚方法分派)
Method resolveMethod(MethodRef ref, Class<?> fromClass) {
// 1. 查找声明类
Class<?> declaringClass = resolveClass(
ref.getDeclaringClassName(),
fromClass.getClassLoader(),
fromClass
);
// 2. 查找方法(考虑重载)
Method method = findMethod(declaringClass,
ref.getName(), ref.getDescriptor());
// 3. 检查访问权限
checkMethodAccess(fromClass, method);
// 4. 虚方法处理(分配vtable索引)
if (!method.isStatic() && !method.isPrivate()) {
int vtableIndex = allocateVTableIndex(method);
ref.setVTableIndex(vtableIndex);
}
return method;
}
}
早解析 vs 晚解析
JVM有两种解析策略:
java
class ResolutionTiming {
// 早解析(Eager Resolution)
// - 在类加载的链接阶段完成所有解析
// - 优点:启动时一次性完成,运行时不暂停
// - 缺点:启动慢,可能解析不用的引用
// 晚解析(Lazy Resolution)
// - 只在第一次使用时才解析
// - 优点:启动快,按需解析
// - 缺点:运行时可能暂停进行解析
// 现代JVM通常采用混合策略
void hybridResolution() {
// 1. 启动时解析必要的引用(如父类、接口)
resolveEssentialReferences();
// 2. 运行时按需解析其他引用
// 使用invokedynamic指令支持晚解析
handleInvokeDynamic();
}
}
5、初始化
初始化是类加载的最后一步 ,也是真正开始执行Java代码的阶段。
初始化触发条件
见第2章节
初始化过程详解
java
class InitializationPhase {
// JVM使用<clinit>()方法进行类初始化
// <clinit>()由编译器自动生成
void initialize(Class<?> clazz) {
// 获取初始化锁(每个类唯一)
Object initLock = getClassInitLock(clazz);
synchronized (initLock) {
// 双重检查锁定
if (isInitialized(clazz)) {
return;
}
// 标记为正在初始化(防止递归)
markInitializing(clazz);
try {
// 步骤1:递归初始化父类
Class<?> superClass = clazz.getSuperclass();
if (superClass != null && !isInitialized(superClass)) {
initialize(superClass);
}
// 步骤2:初始化接口(如果定义了默认方法)
initializeInterfacesWithDefaultMethods(clazz);
// 步骤3:执行<clinit>()方法
executeClinitMethod(clazz);
// 步骤4:标记为已初始化
markInitialized(clazz);
} catch (Throwable t) {
// 初始化失败
markInitializationFailed(clazz, t);
throw new ExceptionInInitializerError(t);
}
}
}
// <clinit>()方法的生成规则
byte[] generateClinitMethod(ClassMetadata metadata) {
BytecodeWriter writer = new BytecodeWriter();
// 按源码顺序收集初始化代码
List<InitializationCode> initCodes = collectInitCodes(metadata);
for (InitializationCode code : initCodes) {
if (code instanceof StaticFieldAssignment) {
// 生成静态字段赋值代码
generateFieldAssignment(writer, (StaticFieldAssignment) code);
} else if (code instanceof StaticBlock) {
// 插入静态代码块
writer.write(((StaticBlock) code).getBytecode());
}
}
// 添加return指令
writer.writeReturn();
return writer.toByteArray();
}
}
初始化顺序示例
java
public class InitializationOrder {
// 静态字段(按定义顺序初始化)
static int a = init("a", 1);
static {
System.out.println("第一个静态代码块");
}
static int b = init("b", 2);
static {
System.out.println("第二个静态代码块");
}
static int c = init("c", 3);
private static int init(String name, int value) {
System.out.println("初始化 " + name + " = " + value);
return value;
}
// 父类
static class Parent {
static {
System.out.println("Parent类初始化");
}
}
// 子类
static class Child extends Parent {
static {
System.out.println("Child类初始化");
}
}
public static void main(String[] args) {
System.out.println("=== 开始测试 ===");
// 触发Child类初始化
new Child();
System.out.println("=== 测试结束 ===");
}
}
java
=== 开始测试 ===
初始化 a = 1
第一个静态代码块
初始化 b = 2
第二个静态代码块
初始化 c = 3
Parent类初始化
Child类初始化
=== 测试结束 ===
顺序:静态->父类->子类
重要规则:
- 父类先于子类初始化
- 静态字段和静态代码块按源码顺序执行
- 每个类只初始化一次
四、类加载器
类加载的 "加载" 阶段,由类加载器(ClassLoader)负责。JVM 提供了 3 种默认类加载器,还有用户可自定义的类加载器,它们形成 "双亲委派模型"。
类加载器的核心价值:
- 延迟加载:类在第一次使用时才加载
- 安全性:防止核心API被篡改
- 隔离性:不同模块可以使用不同版本的类
- 动态性:支持热部署和动态扩展
3 种默认类加载器
| 类加载器 | 作用范围 | 加载路径 |
|---|---|---|
| 启动类加载器(Bootstrap) | 加载 JVM 核心类(比如java.lang.String) |
JAVA_HOME/jre/lib/rt.jar 等核心 jar |
| 扩展类加载器(Extension) | 加载 JVM 扩展类 | JAVA_HOME/jre/lib/ext 目录 |
| 应用程序类加载器(App) | 加载用户编写的类和第三方 jar | 应用 classpath 路径 |
1. Bootstrap ClassLoader(启动类加载器)
- 父加载器:无(null)
- 加载范围:JAVA_HOME/jre/lib下的核心库(rt.jar、resources.jar等)
- 实现:原生代码(C/C++)实现,Java中不可见
- 获取方式:String.class.getClassLoader()返回null
2. Extension ClassLoader(扩展类加载器)
- 父加载器:Bootstrap ClassLoader
- **加载范围**JAVA_HOME/jre/lib/ext目录
- **实现**sun.misc.Launcher$ExtClassLoader
- **获取方式*ClassLoader.getSystemClassLoader().getParent()
3. Application/System ClassLoader(应用/系统类加载器)
- 父加载器:Extension ClassLoader
- 加载范围:`CLASSPATH路径下的类
- 实现:sun.misc.Launcher$AppClassLoader
- 获取方式:`ClassLoader.getSystemClassLoader()
4. 自定义ClassLoader
- 父加载器:默认是`AppClassLoader,可指定
- 加载范围:自定义路径
- 实现:继承ClassLoade类

java
public class ClassLoaderHierarchy {
public static void main(String[] args) {
// 演示类加载器层次结构
// 1. Bootstrap ClassLoader(启动类加载器)
// - 由C++实现,Java中为null
// - 加载JAVA_HOME/jre/lib目录下的核心类库
ClassLoader bootstrap = String.class.getClassLoader();
System.out.println("String的类加载器: " + bootstrap); // null
// 2. Platform ClassLoader(平台类加载器,JDK9+)
// - 替代JDK8的Extension ClassLoader
// - 加载JAVA_HOME/jre/lib/ext目录或系统变量指定的目录
ClassLoader platform = ClassLoader.getPlatformClassLoader();
System.out.println("平台类加载器: " + platform);
// 3. Application ClassLoader(应用类加载器,也叫System ClassLoader)
// - 加载classpath或系统属性java.class.path指定的类
ClassLoader app = ClassLoader.getSystemClassLoader();
System.out.println("应用类加载器: " + app);
// 4. 当前类的类加载器
ClassLoader current = ClassLoaderHierarchy.class.getClassLoader();
System.out.println("当前类的类加载器: " + current);
// 打印完整的层次结构
printClassLoaderHierarchy(current);
}
static void printClassLoaderHierarchy(ClassLoader loader) {
System.out.println("\n=== 类加载器层次结构 ===");
while (loader != null) {
System.out.println(loader);
loader = loader.getParent();
}
System.out.println("Bootstrap ClassLoader (null)");
}
}
java
String的类加载器: null
平台类加载器: jdk.internal.loader.ClassLoaders$PlatformClassLoader@1a2b3c4d
应用类加载器: jdk.internal.loader.ClassLoaders$AppClassLoader@5e8c92f4
当前类的类加载器: jdk.internal.loader.ClassLoaders$AppClassLoader@5e8c92f4
=== 类加载器层次结构 ===
jdk.internal.loader.ClassLoaders$AppClassLoader@5e8c92f4
jdk.internal.loader.ClassLoaders$PlatformClassLoader@1a2b3c4d
Bootstrap ClassLoader (null)
双亲委派模型
双亲委派是类加载器的核心设计模式 ,它确保了类的唯一性 和安全性。
**双亲委派模型的工作过程:**如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请 求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载

双亲委派的优势和劣势
优势:
- 避免重复加载: 子类加载器先委托父类加载器尝试加载类,确保同一个类(全限定名相同)只会被一个类加载器加载一次,避免内存中出现多个相同的 Class 对象,保证程序运行一致性。
- **安全性: **JVM 核心类(如
<font style="color:rgb(31, 35, 41);background-color:rgba(0, 0, 0, 0);">java.lang.String</font>)由启动类加载器优先加载,开发者无法通过自定义同名类覆盖核心类,防止核心 API 被篡改,避免程序运行混乱。 - 类的唯一性
劣势:
- **灵活性不足: **严格的层级委托机制限制了特殊加载需求,例如某些框架(如 Tomcat)需要对不同 Web 应用的类进行隔离加载,必须打破双亲委派才能实现。
- 无法适配动态场景:对于需要动态加载类(如热部署、模块化部署)的场景,双亲委派的固定层级会阻碍类的重新加载,需额外开发复杂逻辑绕开机制。
- 子类加载器依赖父类:如果父类加载器加载了某个类,子类加载器无法再加载该类的不同版本,导致无法实现类的版本隔离,不适用于需要多版本类共存的场景(如插件化开发)。
java
public class ParentDelegationAdvantages {
// 优势1:避免重复加载
void avoidDuplicateLoading() {
// 同一个类在多个加载器间共享
// 节省内存,保证类的一致性
}
// 优势2:安全性
void security() {
// 核心类库由Bootstrap加载
// 自定义的java.lang.String不会被加载
// 防止核心API被篡改
try {
// 尝试加载自定义的java.lang.String
CustomClassLoader loader = new CustomClassLoader();
Class<?> customString = loader.loadClass("java.lang.String");
System.out.println("这里永远不会执行");
} catch (ClassNotFoundException e) {
System.out.println("安全保护:无法加载java.lang.String");
}
}
// 优势3:类的唯一性
void classUniqueness() {
// 同一个类在不同加载器下被视为不同类
// 但通过双亲委派,避免了这种情况
// ClassA由AppClassLoader加载
// ClassA在CustomClassLoader中不会被重复加载
// 而是委派给AppClassLoader
}
}
打破双亲委派的场景
在Java的世界里,双亲委派模型被奉为类加载的"黄金法则"。但正如所有规则都有例外,真正的高手不仅知道如何遵守规则,更知道何时应该优雅地打破它。
场景:
- 场景1:SPI(Service Provider Interface)机制
- 场景2:热部署需求
- 场景3:模块化与版本隔离
- 场景4:代码保护与加密
java
public class RealWorldChallenges {
// 场景1:SPI(Service Provider Interface)机制
class SPIChallenge {
/*
问题描述:
JDBC Driver由Bootstrap加载器加载(在rt.jar中)
但具体的数据库驱动(如MySQL Driver)在ClassPath中
Bootstrap加载器看不到ClassPath中的类!
结果:DriverManager无法加载具体的数据库驱动
*/
void demonstrate() {
// 这行代码在纯双亲委派下会失败
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test",
"user", "pass"
);
}
}
// 场景2:热部署需求
class HotDeployChallenge {
/*
问题描述:
企业应用需要7x24小时运行
代码更新不能重启服务器
但双亲委派会缓存已加载的类
结果:修改后的类无法重新加载
*/
void demonstrate() throws Exception {
// 第一次加载
ClassLoader loader1 = new CustomClassLoader();
Class<?> clazz1 = loader1.loadClass("MyService");
// 修改MyService后...
// 双亲委派会直接返回缓存的Class对象
// 无法加载新版本!
Class<?> clazz2 = loader1.loadClass("MyService");
System.out.println(clazz1 == clazz2); // true
}
}
// 场景3:模块化与版本隔离
class ModuleIsolationChallenge {
/*
问题描述:
大型应用有多个模块
不同模块可能依赖同一库的不同版本
双亲委派只加载第一个找到的版本
结果:版本冲突,功能异常
*/
void demonstrate() {
// 模块A需要v1.0的commons-lang
// 模块B需要v2.0的commons-lang
// 双亲委派下,只能加载一个版本
// 另一个模块会出错!
}
}
// 场景4:代码保护与加密
class CodeProtectionChallenge {
/*
问题描述:
商业软件需要保护源代码
类文件需要加密存储
但标准类加载器只能加载明文的.class文件
结果:无法实现代码加密保护
*/
}
}