Java类加载避免重复加载的核心是同一ClassLoader实例不能两次define同名类,依赖其内置缓存和JVM链接规则:loadClass先查缓存,命中则直接返回;重复define会抛LinkageError。

Java 类加载过程中避免重复加载同一个类,核心在于遵守 JVM 的类唯一性约束:**同一个 ClassLoader 实例不能两次 define 同一名字的类**。这不是靠“检查是否已加载”来实现的,而是由类加载器自身的缓存机制和 JVM 的链接规则共同保障的。
类加载器自带缓存,无需手动判断
每个 ClassLoader 实例内部维护一个已加载类的缓存(通常通过 `defineClass` 之后自动注册到 `classes` 表中)。当调用 `loadClass(String name)` 时,ClassLoader 默认会先查这个缓存——如果已存在同名类,直接返回,不会再次解析或 define。
- 这是双亲委派流程中自然发生的步骤,不依赖重写逻辑
- 即使你自定义了 `findClass`,只要没绕过缓存直接调 `defineClass`,就不会重复加载
- 手动在 `findClass` 中先查 `loadClass(name)` 再决定是否读字节码,反而可能引发死循环或委托错乱
关键红线:不要对同一 ClassLoader 多次调用 defineClass
真正导致 `LinkageError: duplicate class definition` 的,是同一个 ClassLoader 实例在未卸载原类的情况下,再次执行 `defineClass(name, bytes, ...)`。常见于:
- 热部署场景中反复 new 同一个 ClassLoader 并加载相同类名(但不同字节码)
- 插件框架未隔离 ClassLoader 实例,多个插件共用一个加载器
- 在 `findClass` 中捕获 `ClassNotFoundException` 后又尝试 fallback 到其他路径并再次 define
解决方式不是“避免读取”,而是确保每次加载都使用**新实例**的 ClassLoader,且每个实例只负责一类版本的类。
立即学习“Java免费学习笔记(深入)”;
白名单式 loadClass 重写,守住委托边界
若需打破双亲委派(如插件隔离),必须重写 `loadClass`,但重点不在“跳过父加载”,而在“精准控制谁该由自己加载”:
- 开头就按包名判断:如 `name.startsWith("com.myapp.plugin.v2.")` → 走 `findClass`
- 所有 `java.*`、`javax.*`、`org.apache.catalina.*` 等系统/容器类,必须立刻 `super.loadClass(name)` 委派
- `findClass` 只做一件事:从指定路径读 `.class` 字节流,然后 `defineClass`;读不到就抛 `ClassNotFoundException`,不自行委派
资源与线程上下文也要同步隔离
仅改 `loadClass` 不够。如果插件代码里调用 `MyClass.class.getResource("/config.json")` 或 `Thread.currentThread().getContextClassLoader()`,而这些方法仍走 AppClassLoader,就会加载错资源或触发跨加载器类型转换失败。
- 重写 `getResource` 和 `getResources`,确保路径查找也基于当前加载器的 URL
- 执行插件逻辑前,临时设置 `Thread.currentThread().setContextClassLoader(this)`
- 执行完及时恢复,避免污染宿主环境


















