类加载器命名空间/类的唯一性
命名空间:每个类加载器都有自己的「宇宙」
你有没有想过:能不能在同一个 JVM 中加载两个 java.lang.String?答案是不一定——取决于类加载器。
这一节来深入理解类加载器的命名空间和类的唯一性。
命名空间的概念
命名空间(Namespace)是 JVM 为每个类加载器维护的一张「类名表」。
命名空间结构:
┌─────────────────────────────────────────┐
│ Bootstrap ClassLoader 的命名空间 │
│ java.lang.String │
│ java.lang.Object │
│ java.io.File │
│ ... │
├─────────────────────────────────────────┤
│ Extension ClassLoader 的命名空间 │
│ com.sun.xxx.internal.X │
│ ... │
├─────────────────────────────────────────┤
│ Application ClassLoader 的命名空间 │
│ com.example.User │
│ com.example.Order │
│ ... │
├─────────────────────────────────────────┤
│ MyClassLoader 的命名空间 │
│ com.example.MyClass │
│ ... │
└─────────────────────────────────────────┘关键点:不同命名空间之间,类是隔离的。同一个全限定名在不同命名空间中可能指向不同的类。
类的唯一性判定
JVM 中类的唯一性由以下两者共同决定:
类的唯一性 = 类加载器实例 + 类的全限定名同一个类 ≠ 类名相同
同一个类 = 类加载器实例 + 类名完全相同java
// 示例:两个类加载器加载同一个类
ClassLoader loader1 = new MyClassLoader();
ClassLoader loader2 = new MyClassLoader();
Class<?> c1 = loader1.loadClass("com.example.User");
Class<?> c2 = loader2.loadClass("com.example.User");
// c1 != c2!它们的类加载器不同!全盘委托机制
当一个类加载器要加载类时,它会先委托父加载器去尝试加载,只有父加载器无法完成时,自己才去加载。
全盘委托(也叫双亲委派):
Application ClassLoader 要加载 User 类
│
▼
委托给 Extension ClassLoader
│
▼
委托给 Bootstrap ClassLoader
│
▼
Bootstrap 无法加载(不在其负责范围内)
│
▼
返回给 Extension ClassLoader
│
▼
Extension 无法加载(不在其负责范围内)
│
▼
返回给 Application ClassLoader
│
▼
Application 自己加载 User 类全盘委托的意义
全盘委托保证了 Java 核心类库的安全性:
尝试用自定义类加载器加载 java.lang.String
│
▼
委托到 Bootstrap ClassLoader
│
▼
Bootstrap 成功加载 java.lang.String
│
▼
自定义类加载器使用 Bootstrap 加载的 String
│
▼
✓ 安全:自定义 String 不会污染核心类库如果没有全盘委托,自定义类加载器可能加载一个恶意的 java.lang.String,替换掉核心类库中的实现——这是非常危险的。
全盘委托的副作用
全盘委托也会带来一些限制:
java
// 场景:插件系统,两个不同版本的同名类
class PluginA {
class V1 { void method() { System.out.println("V1"); } }
}
class PluginB {
class V2 { void method() { System.out.println("V2"); } }
}
// 两个 V1 和 V2 不能在同一个 JVM 中共存
// 因为 Application ClassLoader 只能加载一个 V1.class这催生了 OSGi 和热部署框架的需求——它们需要打破全盘委托。
命名空间的隔离性
不同命名空间的类之间是严格隔离的:
java
// 命名空间隔离示例
class LoaderA extends ClassLoader {
public Class loadClass(String name) {
return findClass(name); // 不走双亲委派
}
}
class LoaderB extends ClassLoader {
public Class loadClass(String name) {
return findClass(name); // 不走双亲委派
}
}
// LoaderA 和 LoaderB 加载的同名类是不同的类跨命名空间访问的限制
一个命名空间中的类,可以访问另一个命名空间中的类吗?
java
// 规则:
// 1. 子命名空间的类可以看到父命名空间中的类
// Application ClassLoader 的类能看到 Extension/Bootstrap 中的类
// 2. 父命名空间的类看不到子命名空间中的类
// Bootstrap ClassLoader 的类看不到 Application 中的类
// 3. 同级命名空间的类通常互相不可见
// LoaderA 加载的类看不到 LoaderB 加载的类可见性关系(箭头表示「可见」):
Bootstrap → Extension → Application → 自定义加载器
↑ ↑ ↑
│ │ │
看不到 看不到 看不到
子空间 子空间 子空间Class 对象的关系
不同命名空间中的类,它们的 Class 对象之间是什么关系?
java
Class<?> c1 = loader1.loadClass("com.example.User");
Class<?> c2 = loader2.loadClass("com.example.User");
Class<?> c3 = loader1.loadClass("com.example.User");
// c1 == c3(同一个类加载器实例,加载同一个类,Class 对象相同)
// c1 != c2(不同的类加载器实例,Class 对象不同)类加载器实例 + 类名 → Class 对象
loader1 + "User" → Class 对象 A
loader2 + "User" → Class 对象 B(A != B)
loader1 + "User" → Class 对象 A(loader1 已加载,直接返回)类加载器实例与 Class 对象的关系
┌────────────────────────────────────────────┐
│ Class 对象 A │
│ │ │
│ ├── 类名:com.example.User │
│ ├── 加载器:loader1 │
│ ├── 类加载器实例 A 的命名空间 │
│ └── 方法区数据 │
├────────────────────────────────────────────┤
│ Class 对象 B │
│ │ │
│ ├── 类名:com.example.User │
│ ├── 加载器:loader2 │
│ ├── 类加载器实例 B 的命名空间 │
│ └── 方法区数据 │
└────────────────────────────────────────────┘
A != B(即使类名相同,加载器不同,Class 对象不同)打破命名空间隔离的场景
场景一:JDBC 驱动的加载
JDBC 的 DriverManager 使用 Thread Context ClassLoader(TCCL)来加载驱动:
java
// JDBC 的设计哲学
// 核心 API 在 java.sql 中,由 Bootstrap ClassLoader 加载
// 驱动实现由各厂商提供,在 classpath 中,由 Application ClassLoader 加载
// DriverManager 需要能加载不同加载器的驱动
// 解决方案:使用 TCCL
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
Driver driver = (Driver) Class.forName("com.mysql.jdbc.Driver", true, tccl);场景二:热部署
Tomcat 和 OSGi 通过销毁类加载器来卸载旧版本类:
Tomcat 热部署流程:
1. 新请求到来,检测到有新版本的类
2. 创建新的 WebappClassLoader(新的命名空间)
3. 新的类加载器加载新版本类
4. 旧类加载器没有引用了
5. 旧类加载器可以被 GC 回收
6. 旧类加载器加载的类随之卸载
7. 新旧版本类共存(在不同命名空间)命名空间与反射
反射操作可能遇到跨命名空间的问题:
java
// 问题:跨命名空间的类型转换
Class<?> aClass = loaderA.loadClass("com.example.User");
Class<?> bClass = loaderB.loadClass("com.example.User");
// aClass != bClass
// 虽然都叫 User,但它们是不同的类型!
Object obj = aClass.newInstance();
// obj instanceof com.example.User(loaderA 的 User) → true
// obj instanceof com.example.User(loaderB 的 User) → ClassCastException核心问题:同一个类名在不同命名空间中代表不同的 Class 对象,类型检查会失败。
本节小结
命名空间和类的唯一性核心要点:
| 维度 | 说明 |
|---|---|
| 命名空间 | 每个类加载器实例维护一个命名空间 |
| 类的唯一性 | 类加载器实例 + 全限定名共同决定 |
| 全盘委托 | 加载类时先委托父加载器,只有父无法完成时才自己加载 |
| 可见性 | 父命名空间看不到子命名空间,子命名空间能看到父命名空间 |
| 打破隔离 | TCCL、OSGi、热部署等场景需要打破隔离 |
| 反射问题 | 跨命名空间的同名类,Class 对象不同,类型转换会失败 |
理解命名空间,才能理解类加载器的隔离机制,以及为什么需要打破双亲委派。
下一节,我们来看 双亲委派机制破坏/热替换。
