类加载器是 JVM 的"搬运工",把字节码搬进内存,把符号变成地址,把类从文件变成可执行的生命体。
类加载器在 JVM 架构中的定位
JVM 的"运行期数据区"里,方法区(JDK 8 以后叫元空间)存放着类的元数据;堆中则躺着 java.lang.Class 对象。连接两者的桥梁正是类加载器(ClassLoader)。没有它,再漂亮的字节码也只是一堆 0 和 1。
一、类加载器定义:三个层次的理解
1. 规范视角
《Java 虚拟机规范》第 5.3 节:
类加载器是一个用于加载类文件的对象,它读取二进制字节流,将其转换为 JVM 内部的数据结构,并生成对应的
java.lang.Class实例。
2. 实现视角
在 JDK 源码里,抽象类 java.lang.ClassLoader 定义了核心契约:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 2. 委派给父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法完成加载,由自身尝试
}
if (c == null) {
// 3. 自己加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}3. 运行时视角
通过 jcmd <pid> VM.classloader_stats 可以查看当前 JVM 中所有类加载器实例及其加载的类数量、占用元空间大小。调试内存泄漏时,这一步往往先于堆 dump。
二、官方分类:三层模型
| 名称 | 加载路径 | 实现语言 | 父加载器 |
|---|---|---|---|
| Bootstrap | %JAVA_HOME%/lib, -Xbootclasspath | C++(JVM 内部) | null |
| Platform | %JAVA_HOME%/lib/ext, java.ext.dirs | Java | Bootstrap |
| Application | -classpath, -cp, MANIFEST.MF 中 Class-Path | Java | Platform |
注:JDK 9 引入模块系统后,ExtClassLoader 被 PlatformClassLoader 取代,但层级思想不变。
三、核心机制:双亲委派模型(Parents Delegation Model)
1. 工作流程
2. 代码验证
打开 sun.misc.Launcher,可以看到启动时即构造好这条链:
public Launcher() {
// 创建扩展类加载器
ExtClassLoader ext = ExtClassLoader.getExtClassLoader();
// 创建应用类加载器,以扩展类加载器为父
loader = AppClassLoader.getAppClassLoader(ext);
// 设置到线程上下文,供 SPI 使用
Thread.currentThread().setContextClassLoader(loader);
}3. 打破委派:SPI 与线程上下文类加载器
JDBC 4.0 之前,Driver 实现由 rt.jar 里的 java.sql.DriverManager 加载,而具体实现类(如 com.mysql.cj.jdbc.Driver)却在应用 classpath。Bootstrap 无法反向委派给 AppClassLoader,于是引入 线程上下文类加载器(TCCL):
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);ServiceLoader 内部通过 Thread.currentThread().getContextClassLoader() 拿到 TCCL,从而成功加载第三方驱动。
四、自定义类加载器:隔离与热替换
1. 隔离场景
微服务 sidecar、Flink JobManager 与用户代码、Tomcat WebApp 之间都需要"类隔离"——各自加载同名不同版本的类,互不干扰。
2. 实现模板
public class HotSwapClassLoader extends ClassLoader {
private final Path classesRoot;
public HotSwapClassLoader(Path classesRoot, ClassLoader parent) {
super(parent); // 关键:指定父加载器
this.classesRoot = classesRoot;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = readClassBytes(name);
return defineClass(name, bytes, 0, bytes.length);
}
private byte[] readClassBytes(String name) throws ClassNotFoundException {
String fileName = name.replace('.', '/') + ".class";
Path classFile = classesRoot.resolve(fileName);
try {
return Files.readAllBytes(classFile);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}3. 热替换陷阱
- 类标识:JVM 判断类相同需"全限定名 + 定义类加载器"两者一致。换加载器再加载同名类会得到全新
Class对象,导致ClassCastException。 - 静态块:只会执行一次,由首次加载的加载器触发;再次加载新加载器实例不会重新初始化。
- 元空间泄漏:若反复创建自定义加载器而未卸载,类元数据会占用本地内存,直到触发
OutOfMemoryError: Metaspace。
五、调试与监控:从日志到字节码
1. 打开类加载日志
JDK 8:-XX:+TraceClassLoading 或 -verbose:class
JDK 11+:-Xlog:class+load=debug
输出示例:
[0.345s][info][class,load] java.lang.Object source: jrt:/java.base
[0.346s][info][class,load] java.io.Serializable source: jrt:/java.base
[0.347s][info][class,load] com.example.Demo source: file:/tmp/demo.jar