大家好,我是程序员二叉。
简介
本文梳理后端面试必考的类加载五大步骤、双亲委派机制原理、机制优缺点、打破方案、自定义类加载器完整实现逻辑,附带可运行代码示例。欢迎点赞关注收藏。
一、JVM类加载的五个步骤
类从磁盘.class文件到内存实例化对象,完整分为加载、验证、准备、解析、初始化五个阶段。
1. 加载 Loading
2. 验证 Verification
校验字节码合法性、安全性,防止恶意代码破坏虚拟机,分为四层校验:
- 文件格式验证:校验字节流是否符合Class文件规范;
- 元数据验证:校验类的语义、继承关系等基础语法;
- 字节码验证:校验方法体内指令逻辑安全;
- 符号引用验证:校验引用的外部类/字段/方法是否可访问存在。
3. 准备 Preparation
示例:static int num = 100,准备阶段num=0,初始化阶段才赋值100
常量static final编译期直接赋值,准备阶段就赋予确定值。
4. 解析 Resolution
把常量池里的符号引用替换成内存中真实的直接引用;
解析对象包含:类/接口、字段、普通方法、接口方法、方法句柄、调用点限定符等。
5. 初始化 Initialization
类加载最后一步,真正执行Java代码逻辑:
只有主动引用类时才会触发初始化,被动引用不会执行初始化。
二、什么是双亲委派机制
1. 三层原生类加载器层级
2. 委派执行流程
当一个类加载器收到加载请求:
3. 双亲委派核心优势
三、双亲委派机制的缺点
启动类加载器、扩展加载器不能识别应用加载器加载的业务类,上下单向隔离;
如JDBC、日志框架SPI,核心接口由启动类加载器加载,但实现类在项目classpath,默认委派模式拿不到实现类;
OSGi、Tomcat多web应用隔离、代码热更新等场景,需要独立隔离类加载环境,原生委派无法实现;
多个模块依赖不同版本Jar包时,双亲委派只会加载第一个找到的Jar,版本冲突无法隔离。
四、如何打破双亲委派机制
1. 底层原理
ClassLoader加载入口是loadClass(String name),原生方法内置双亲委派逻辑;重写loadClass()方法,删除向上委托逻辑即可打破。
2. 三代打破方案
完全重写加载逻辑,跳过父加载器委托,早期Tomcat、OSGi使用;
SPI标准解决方案,核心接口由启动加载器加载,通过线程上下文切换成应用加载器去加载实现类;
自定义平行类加载器,不遵循父层级,用于插件、多版本Jar隔离。
典型打破场景
- JDBC驱动加载、Dubbo SPI、Spring SPI;
- Tomcat多个Web工程独立类隔离;
- OSGi模块化框架、开发工具热部署;
- 中间件插件化架构。
五、如何实现自定义类加载器
规范实现规则
示例1:标准遵循双亲委派的自定义加载器
import java.io.File;
import java.io.FileInputStream;
public class CustomClassLoader extends ClassLoader {
// 自定义class文件存放路径
private final String classDir;
public CustomClassLoader(String classDir) {
this.classDir = classDir;
}
// 只重写findClass,保留原生双亲委派逻辑
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
try {
// 1. 读取class文件字节数组
byte[] classBytes = readClassFile(className);
if (classBytes == null) {
throw new ClassNotFoundException();
}
// 2. 字节码转为Class对象
return defineClass(className, classBytes, 0, classBytes.length);
} catch (Exception e) {
throw new ClassNotFoundException(className, e);
}
}
// 读取磁盘.class文件
private byte[] readClassFile(String className) throws Exception {
String path = classDir + File.separator + className.replace(".", File.separator) + ".class";
try (FileInputStream fis = new FileInputStream(path)) {
return fis.readAllBytes();
}
}
// 测试调用
public static void main(String[] args) throws Exception {
CustomClassLoader loader = new CustomClassLoader("D:/classpath");
Class<?> testCls = loader.loadClass("com.demo.Test");
Object obj = testCls.newInstance();
System.out.println(obj);
}
}
示例 2:打破双亲委派(重写 loadClass)
public class BreakDelegateClassLoader extends ClassLoader {
private final String classDir;
public BreakDelegateClassLoader(String classDir) {
this.classDir = classDir;
}
// 重写加载入口,跳过父加载器委托
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 1. 先查询缓存是否已加载
Class<?> cacheClass = findLoadedClass(name);
if (cacheClass != null) {
return cacheClass;
}
// 不向上委托父加载器,直接自己加载
try {
byte[] bytes = readBytes(name);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
// 自己加载失败,再交给父类兜底
return super.loadClass(name);
}
}
private byte[] readBytes(String className) throws Exception {
// 读取class文件逻辑省略,同上示例
return new byte[0];
}
}





