类加载器
类加载器是 JVM 的核心组件,负责将 class 文件加载到 JVM 中。理解类加载机制是深入掌握 Java 的关键。
一、类加载过程
整体流程
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 加载 │ -> │ 连接 │ -> │ 初始化 │
└─────────────┘ └─────────────┘ └─────────────┘
│
┌───────────────┼───────────────┐
↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 验证 │ │ 准备 │ │ 解析 │
└─────────┘ └─────────┘ └─────────┘1. 加载 (Loading)
做什么:
- 通过类全限定名获取二进制字节流
- 将字节流转化为方法区的运行时数据结构
- 在堆中生成 Class 对象
来源:
| 来源 | 说明 |
|---|---|
| 本地文件系统 | class 文件 |
| JAR 包 | 压缩文件中的 class |
| 网络 | Applet、RMI |
| 动态代理 | 运行时生成 |
| 其他文件 | JSP 编译后的 class |
2. 连接 (Linking)
验证
确保 Class 文件格式正确、语义合法。
验证内容:
├── 文件格式验证(魔数 0xCAFEBABE)
├── 元数据验证(是否有父类、是否实现抽象方法)
├── 字节码验证(数据流分析)
└── 符号引用验证(引用的类/方法是否存在)准备
为类变量分配内存并设置初始值。
// 准备阶段
public static int value = 123;
// value 在准备阶段 = 0,不是 123
public static final int CONST = 100;
// final 常量在准备阶段 = 100(编译期常量)解析
将符号引用替换为直接引用。
符号引用:字面量描述(如 "java/lang/Object")
直接引用:实际内存地址或偏移量3. 初始化 (Initialization)
执行类构造器 <clinit>() 方法。
public class InitDemo {
static int a = 1; // 赋值
static { // 静态块
a = 2;
}
// 编译后生成 <clinit>() 方法:
// a = 1;
// a = 2;
}初始化时机(主动引用):
| 场景 | 示例 |
|---|---|
| new实例化 | new MyClass() |
| 访问静态变量 | MyClass.value |
| 访问静态方法 | MyClass.method() |
| 反射 | Class.forName("MyClass") |
| 初始化子类 | 子类初始化触发父类 |
| 主类 | java MyClass |
不会初始化(被动引用):
// 1. 通过子类引用父类静态变量
class Parent { static int a = 1; }
class Child extends Parent {}
Child.a; // 只初始化 Parent,不初始化 Child
// 2. 通过数组定义引用类
Parent[] arr = new Parent[10]; // 不初始化 Parent
// 3. 引用常量
class Const { static final int A = 1; }
Const.A; // 不初始化 Const(编译期常量优化)二、类加载器层次
三层类加载器
┌─────────────────────────────────────────────┐
│ 启动类加载器 (Bootstrap) │
│ 加载核心类库 (rt.jar等) │
│ C++ 实现,无父加载器 │
└────────────────────┬────────────────────────┘
│
┌────────────────────┴────────────────────────┐
│ 扩展类加载器 (Extension) │
│ 加载扩展类库 (ext目录) │
│ Java 实现,父为 Bootstrap │
└────────────────────┬────────────────────────┘
│
┌────────────────────┴────────────────────────┐
│ 应用类加载器 (Application) │
│ 加载用户类路径 (classpath) │
│ Java 实现,父为 Extension │
└─────────────────────────────────────────────┘加载器详解
| 加载器 | 路径 | 父加载器 |
|---|---|---|
| Bootstrap | $JAVA_HOME/jre/lib/rt.jar等核心库 | null |
| Extension | $JAVA_HOME/jre/lib/ext/*.jar | Bootstrap |
| Application | classpath | Extension |
代码验证
public class ClassLoaderDemo {
public static void main(String[] args) {
// 获取类加载器
ClassLoader appLoader = ClassLoaderDemo.class.getClassLoader();
System.out.println("应用类加载器: " + appLoader);
// sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader extLoader = appLoader.getParent();
System.out.println("扩展类加载器: " + extLoader);
// sun.misc.Launcher$ExtClassLoader@1540e19d
ClassLoader bootLoader = extLoader.getParent();
System.out.println("启动类加载器: " + bootLoader);
// null(C++实现,Java中无对应对象)
// 核心类由 Bootstrap 加载
ClassLoader stringLoader = String.class.getClassLoader();
System.out.println("String的加载器: " + stringLoader);
// null
}
}三、双亲委派模型
工作原理
加载请求
↓
┌─────────────────────────────────────────────────┐
│ 应用类加载器 │
│ │
│ 1. 先委托父加载器加载 │
│ 2. 父加载器无法加载,自己再尝试加载 │
└─────────────────────────────────────────────────┘
↓ (委托)
┌─────────────────────────────────────────────────┐
│ 扩展类加载器 │
│ │
│ 1. 先委托父加载器加载 │
│ 2. 父加载器无法加载,自己再尝试加载 │
└─────────────────────────────────────────────────┘
↓ (委托)
┌─────────────────────────────────────────────────┐
│ 启动类加载器 │
│ │
│ 1. 尝试加载核心类库 │
│ 2. 找到则返回,找不到返回 null │
└─────────────────────────────────────────────────┘源码分析
// ClassLoader.loadClass() 核心逻辑
protected Class<?> loadClass(String name, boolean resolve) {
synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 2. 委托父加载器
c = parent.loadClass(name, false);
} else {
// 3. 父为null,委托 Bootstrap
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器找不到,忽略
}
if (c == null) {
// 4. 父加载器都找不到,自己加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}双亲委派的好处
1. 安全性
防止核心类被篡改:
// 自定义 java.lang.String
package java.lang;
public class String {
public String() {
// 恶意代码
}
}
// 由于双亲委派,Bootstrap 会加载 rt.jar 中的 String
// 自定义的 String 永远不会被加载2. 唯一性
保证类的全局唯一性:
无论哪个加载器加载 java.lang.Object
最终都由 Bootstrap 加载
JVM 中只有一份 Object.class四、打破双亲委派
何时需要打破?
| 场景 | 说明 |
|---|---|
| SPI 机制 | JDBC 等接口由 Bootstrap 加载,实现类在 classpath |
| 热部署 | 需要卸载和重新加载类 |
| 隔离容器 | Tomcat 等容器需要隔离不同应用 |
方式一:重写 loadClass
public class CustomClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 打破双亲委派:先自己加载,失败再委托父加载器
try {
return findClass(name);
} catch (ClassNotFoundException e) {
return super.loadClass(name);
}
}
}方式二:线程上下文加载器
JDBC 的实现方式:
// DriverManager 在 rt.jar,由 Bootstrap 加载
// Driver 实现类(如 MySQL Driver)在 classpath
public class DriverManager {
static {
// 使用线程上下文类加载器
// 默认是 AppClassLoader
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// ...
return null;
});
}
}
// ServiceLoader.load() 使用上下文类加载器
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}方式三:OSGi 模块化
OSGi 类加载机制:
├── 每个模块(Bundle)有自己的类加载器
├── 加载顺序:本模块 → 依赖模块 → 父加载器
└── 实现了类加载器的网状结构五、Tomcat 类加载机制
Tomcat 类加载器层次
┌─────────────────────────────────────────────────┐
│ Bootstrap (启动类加载器) │
└────────────────────┬────────────────────────────┘
│
┌────────────────────┴────────────────────────────┐
│ System (系统类加载器) │
│ 加载 Tomcat 自身类 │
└────────────────────┬────────────────────────────┘
│
┌────────────────────┴────────────────────────────┐
│ Common (公共类加载器) │
│ 加载 Tomcat 和应用共享的类 │
└────────────────────┬────────────────────────────┘
│
┌────────────┴────────────┐
↓ ↓
┌───────────────┐ ┌───────────────┐
│ Catalina │ │ Shared │
│ 类加载器 │ │ 类加载器 │
│ (Tomcat内部) │ │ (应用共享) │
└───────────────┘ └───────┬───────┘
│
┌────────────┴────────────┐
↓ ↓
┌─────────────┐ ┌─────────────┐
│ WebApp1 │ │ WebApp2 │
│ 类加载器 │ │ 类加载器 │
└─────────────┘ └─────────────┘隔离原理
每个 WebApp 有独立的类加载器:
- 不同应用可以使用不同版本的类库
- 应用之间互不影响
- 实现了 Web 应用的隔离
六、常见面试题
Q1: 类加载器为什么采用双亲委派?
安全性:
- 防止核心类库被篡改
- 自定义的
java.lang.String无法被加载
唯一性:
- 保证类的全局唯一性
- 不同加载器加载同一个类,得到的是不同的 Class 对象
Q2: 如何自定义类加载器?
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadClassData(name);
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
private byte[] loadClassData(String name) throws IOException {
String fileName = classPath + name.replace('.', '/') + ".class";
FileInputStream fis = new FileInputStream(fileName);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
fis.close();
return bos.toByteArray();
}
}
// 使用
MyClassLoader loader = new MyClassLoader("/custom/path/");
Class<?> clazz = loader.loadClass("com.example.MyClass");Q3: JDBC 如何打破双亲委派?
问题:
- DriverManager 由 Bootstrap 加载
- Driver 实现类(如 MySQL Driver)在 classpath
- Bootstrap 无法加载 classpath 下的类
解决方案:
- 使用线程上下文类加载器
- ServiceLoader.load() 使用 AppClassLoader 加载实现类Q4: 为什么不同加载器加载同一个类是不同的类?
// 同一个类文件,不同加载器加载
MyClassLoader loader1 = new MyClassLoader("/path/");
MyClassLoader loader2 = new MyClassLoader("/path/");
Class<?> c1 = loader1.loadClass("com.example.Test");
Class<?> c2 = loader2.loadClass("com.example.Test");
System.out.println(c1 == c2); // false
// 在 JVM 中,类的唯一标识 = 全限定名 + 类加载器
// c1 和 c2 虽然全限定名相同,但加载器不同,所以是不同的类Q5: 如何判断两个类是否相同?
JVM 判断两个类相同的条件:
- 全限定名相同
- 类加载器相同
// 即使是同一个 class 文件
// 不同类加载器加载后,instanceof 返回 false
Object obj = c1.newInstance();
System.out.println(obj instanceof c2); // false小结
| 概念 | 要点 |
|---|---|
| 类加载过程 | 加载 → 连接(验证、准备、解析) → 初始化 |
| 三层加载器 | Bootstrap → Extension → Application |
| 双亲委派 | 先委托父加载器,父加载不了再自己加载 |
| 打破双亲委派 | 重写 loadClass / 上下文加载器 / OSGi |
| Tomcat 隔离 | 每个 WebApp 独立的类加载器 |
核心原则:
- 双亲委派保证安全性和唯一性
- 必要时可打破双亲委派(SPI、热部署、容器隔离)
- 类的唯一性由全限定名 + 类加载器共同决定