类的身份由“二进制类名 + 定义它的类加载器”共同决定。两个加载器各自定义的 com.example.Plugin 即使字节完全相同,
JVM 也把它们视为不同类型。这条规则解释了插件隔离、热部署和许多 ClassCastException。
Table of contents
Open Table of contents
查看内置类加载器
下载并运行类加载器样例下载:
mvn package
java -Xlog:class+load=info -cp target/classes dev.elaine.jvmlab.ClassLoaderSample
关键输出类似:
业务类: jdk.internal.loader.ClassLoaders$AppClassLoader@...
平台类: jdk.internal.loader.ClassLoaders$PlatformClassLoader@...
核心类: Bootstrap ClassLoader
Bootstrap ClassLoader 由 JVM 实现,Java API 通常用 null 表示它。Platform ClassLoader 加载平台模块,Application
ClassLoader 加载应用类路径上的业务类。
父优先委派解决什么问题
ClassLoader.loadClass 通常先询问父加载器,父加载器找不到时才调用当前加载器的 findClass。这叫父优先委派,
常被称为双亲委派。
它带来两项重要性质:
- 核心类优先由受信任的加载器定义,应用不能用自己的
java.lang.String替换平台类。 - 父加载器可见的共享 API 只定义一次,子加载器中的插件能通过同一接口交互。
父加载器不是 Java 继承关系,而是类查找委托关系。Java 25 的 ClassLoader API明确描述了这种委派模型。
隔离插件的边界
插件系统通常让每个插件拥有子加载器。共享的 SPI 接口放在父加载器,插件实现和私有依赖放在子加载器:
Application ClassLoader
└── Plugin API: dev.elaine.spi.Plugin
├── PluginLoader-A → 插件 A 与其依赖
└── PluginLoader-B → 插件 B 与其依赖
如果每个子加载器也各自定义一份 Plugin,插件对象将无法转换成宿主接口。排查时打印对象类名和加载器:
System.out.println(value.getClass().getName());
System.out.println(value.getClass().getClassLoader());
什么时候会打破父优先
应用服务器、OSGi 和插件容器可能采用子优先策略,让应用携带的库覆盖容器版本。Java SPI 的线程上下文类加载器则允许 父层代码发现子层实现。这些都是有意设计,不是“委派失效”。
自定义加载器时优先覆写 findClass,除非你确实需要改变委派顺序。错误覆写 loadClass 可能重复定义核心 API、产生并发
加载死锁,或破坏包与模块边界。
类卸载的必要条件
类只有在定义它的加载器和该加载器定义的全部类都不可达时才可能卸载。线程上下文加载器、静态集合、JDBC 驱动注册、
定时任务和 ThreadLocal 都可能让插件加载器长期存活。
使用 -Xlog:class+load=info,class+unload=info 观察事实。不要把“关闭插件”误当成“类一定已经卸载”。
下一步
阅读运行时数据区与 Java 内存模型不要混淆,分清内存 放在哪里与线程何时能看见写入。