一个反直觉的事实
JVM 里判断两个类是否相同,除了看 Class 文件,还要看加载它的类加载器是否相同。同一个 User.class,被加载器 A 和加载器 B 各加载一次,堆里就有两个互不相干的 Class 对象——instanceof 返回 false,互相赋值直接 ClassCastException。第一次听说的人多半以为是 bug,其实这是整个类加载体系的基石设计:加载器就是类的命名空间。
三层加载器
启动类加载器(Bootstrap,C++ 实现)负责 Java 核心库,在 JDK 9 后对应平台模块;平台类加载器(JDK 8 里叫扩展类加载器)负责扩展类;应用类加载器负责 classpath 上你自己写的类。三层各管一段、逐级向上委派,程序里默认的加载器就是应用类加载器。
工作模型:先问爹,再自己干
// ClassLoader.loadClass 的核心逻辑(简化)
protected Class<?> loadClass(String name, boolean resolve) {
// 1. 先查自己有没有加载过(缓存)
Class<?> c = findLoadedClass(name);
if (c == null) {
// 2. 有爹先问爹:父加载器能加载就交给父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
// 3. 爹加载不了,自己才动手(findClass)
if (c == null) {
c = findClass(name);
}
}
return c;
}这个模型买来两样东西:安全——自己写一个 java.lang.String 塞进 classpath 也不会生效,因为委派到最顶层时核心库早已加载,你的假货永远排不上号;唯一——同一个类全局只有一份,避免类型体系混乱。
打破规则的场景
双亲委派不是法律,是建议。三类场景会光明正大地打破它:SPI 与 JDBC——核心库里的 DriverManager 要加载 classpath 上的厂商驱动(父加载器"够不着"子加载器的类),解决办法是线程上下文类加载器,让父加载器反过来"借用"子加载器的手;Tomcat 的应用隔离——每个 webapp 一个独立加载器,先自己加载 webapp 内的类,加载不到才委派,两个应用里同名不同版的类互不干扰,卸载应用时整个加载器连同类一起回收;热部署与模块化——OSGi、代码热更新都靠"换加载器"实现类的替换与卸载。
自己写一个加载器
public class ReloadableLoader extends ClassLoader {
private final String dir;
public ReloadableLoader(String dir) { this.dir = dir; }
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = readClassFile(dir, name); // 自己定义字节流来源
if (bytes == null) throw new ClassNotFoundException(name);
return defineClass(name, bytes, 0, bytes.length);
}
}
// 注意:委派逻辑在 loadClass 里,继承时重写的是 findClass
// 只有需要破坏双亲委派(如 Tomcat)才重写 loadClass记住这个分工:重写 findClass 是遵守规则的扩展,重写 loadClass 是打破规则的革命。类加载的故事到这就齐了,下一篇进入系列的第二大段——垃圾回收的第一课:JVM 怎么判断一个对象是不是垃圾。
评论 (0)