类加载器
类加载器负责把类的字节码加载到 JVM 中。对 JVM 来说,一个类不仅由全限定类名决定,还由加载它的类加载器共同决定。
# 1. 类加载器的作用
类加载器主要负责:
- 定位 class 字节码。
- 读取 class 字节流。
- 定义
Class对象。 - 参与类隔离。
同一个类文件,如果由两个不同类加载器加载,在 JVM 中会被认为是两个不同的类。
# 2. 常见类加载器
Java 8 中常见类加载器:
- Bootstrap ClassLoader:启动类加载器。
- Extension ClassLoader:扩展类加载器。
- Application ClassLoader:应用类加载器。
Java 9 引入模块系统后,扩展类加载器被平台类加载器替代:
- Bootstrap ClassLoader。
- Platform ClassLoader。
- Application ClassLoader。
# 3. Bootstrap ClassLoader
启动类加载器负责加载核心类库,例如 java.lang.String、java.lang.Object 等。
它通常由 C/C++ 实现,在 Java 层面获取父加载器时可能显示为 null。
System.out.println(String.class.getClassLoader()); // null
# 4. Platform ClassLoader
平台类加载器负责加载平台模块中的类。
Java 9 之后,模块系统改变了类库组织方式,Platform ClassLoader 承担了原扩展类加载器的一部分角色。
# 5. Application ClassLoader
应用类加载器负责加载应用 classpath 或 module path 中的类。
System.out.println(MyClass.class.getClassLoader());
普通业务类通常由应用类加载器加载。
# 6. 自定义类加载器
可以继承 ClassLoader 实现自定义加载逻辑。
常见场景:
- 插件系统。
- 热部署。
- 类隔离。
- 加密 class 解密加载。
- 字节码动态生成。
一般重写 findClass,而不是直接重写 loadClass,以避免破坏双亲委派。
# 7. 类加载器隔离
类的唯一性由类加载器和类全限定名共同决定。
Class identity = ClassLoader + package.ClassName
这解释了为什么有时明明类名一样,却会出现:
ClassCastException: A cannot be cast to A
因为两个 A 可能由不同类加载器加载。
# 8. 类加载器层次图
Bootstrap ClassLoader
▲
│
Platform ClassLoader
▲
│
Application ClassLoader
▲
│
Custom ClassLoader
这里的父子关系不是 Java 继承关系,而是加载委派关系。一个类加载器通常保存父加载器引用,加载类时先委派给父加载器。
# 9. 类加载器命名空间
JVM 判断两个类是否相同,需要同时看:
类全限定名 + 定义类加载器
例如:
loaderA 加载 com.example.User
loaderB 加载 com.example.User
这两个 User 在 JVM 中不是同一个类,即使字节码完全一样。
这也是插件系统能实现依赖隔离的基础。
# 10. loadClass 和 findClass
自定义类加载器时常见两个方法:
| 方法 | 作用 |
|---|---|
loadClass | 类加载入口,默认实现包含双亲委派逻辑 |
findClass | 当前加载器真正查找并定义类的位置 |
一般建议重写 findClass,不要轻易重写 loadClass。如果重写 loadClass,很容易破坏双亲委派,导致核心类安全或依赖隔离问题。
# 11. 自定义类加载器流程
loadClass("com.example.Plugin")
│
├─ 检查是否已经加载
│
├─ 委派父加载器加载
│
└─ 父加载器失败
└─ findClass()
├─ 读取字节码
├─ defineClass()
└─ 返回 Class 对象
# 12. 开发中常见问题
# 12.1 为什么 Spring Boot fat jar 也涉及类加载器
Spring Boot 可执行 jar 中依赖嵌套在 jar 内部,标准 URLClassLoader 不能直接按普通目录读取所有嵌套 jar,因此 Spring Boot 使用自己的加载机制处理嵌套 jar。
# 12.2 为什么热部署容易内存泄漏
热部署通常通过丢弃旧类加载器、创建新类加载器实现。如果旧类加载器仍被线程、静态变量、ThreadLocal、驱动注册表等引用,它加载的所有类都无法卸载。
# 13. Tips 快问快答
Q:String.class.getClassLoader() 为什么是 null?
A:String 由 Bootstrap ClassLoader 加载,启动类加载器在 Java 层通常用 null 表示。
Q:类加载器父子关系是继承吗?
A:不是,是委派关系。子加载器持有父加载器引用。
Q:为什么同名类不能互相强转?
A:如果由不同类加载器定义,它们在 JVM 类型系统中就是不同类型。
Q:自定义类加载器最常见用途是什么?
A:插件化、热部署、类隔离、加密 class 加载、动态字节码生成。
# 14. 总结
类加载器不仅负责加载类,还负责类隔离和命名空间管理。理解类加载器,是理解应用服务器、插件化、SPI、框架冲突、热部署和 classpath 问题的前提。