--- title: "02-类加载机制" created: 2025-11-26 tags: - Java --- # 类加载机制 ## **核心概念** > Java 类加载器负责将 `.class` 字节码加载到 JVM 中并转化为 Class 对象,整个过程分为「**加载 → 链接(验证+准备+解析)→ 初始化**」,并采用「**双亲委派**」模型来保证类唯一性、安全性与高效性。 > **双亲委派模型**: > Java 中的类加载器形成一棵树。**每个类加载器在加载类时,不是自己先找,而是先把请求交给父加载器**;只有当所有父加载器都表示“我找不到”时,它才自己去从磁盘/网络等位置查找并加载字节码。这个“层层往上托关系”的流程,就叫**双亲委派模型**。 > 这样可以保证: > > - JDK 核心类只能由顶层的 BootstrapClassLoader 加载,防止用户篡改; > - 同一个类(同一 ClassLoader + 全限定名)在 JVM 中只有一个版本,保证类型唯一; > - 类一旦被父加载器加载过,子加载器就不会重复加载,提升性能。 > **🎯 核心判定规则**:两个类相等的充要条件 = **同一个类加载器** + **完全相同的全限定名** --- ## **一、为什么需要类加载机制与双亲委派?** ### **1.1 问题背景(没有双亲委派会怎样)** - **唯一性破坏**:同一个类被不同类加载器加载会产生多个类型,`instanceof` 和类型转换失效 - **安全隐患**:用户可以随意编写 `java.lang.String` 并被加载,篡改核心类库 - **性能浪费**:每次都从磁盘读取字节码、重复验证、重复加载 ### **1.2 设计动机** - **安全性**:保证 JVM 核心类库安全(`java.*` 包只能由 Bootstrap 加载) - **避免重复**:同一个类只加载一次(缓存机制) - **职责分离**:实现类加载的优先级与分工协作 --- ## **二、类加载全过程详解** ### **2.1 生命周期七阶段** ``` 加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载 └────────链接────────┘ ``` #### **1️⃣ 加载(Loading)** - 通过全限定名查找字节码文件(来源:文件系统/jar/网络/动态生成) - 将字节码读入**方法区**(JDK8 后是元空间 Metaspace) - 在**堆中**生成对应的 `java.lang.Class` 对象作为方法区数据的访问入口 #### **2️⃣ 链接(Linking)** | **阶段** | **作用** | **典型错误** | | --- | --- | --- | | **验证** | 检查字节码安全性、魔数 `0xCAFEBABE`、版本兼容性 | `VerifyError` | | **准备** | 为**静态变量**分配内存,赋**默认值**(`int→0`, `Object→null`) ⚠️ **final static 常量直接赋初始值** | - | | **解析** | 将符号引用转为直接引用(内存地址) 可选阶段,支持动态绑定时延迟解析 | `NoSuchMethodError` | **关键区别**: ```java public static int value = 123; // 准备阶段:value=0,初始化阶段:value=123 public static final int VALUE = 123; // 准备阶段:VALUE=123(编译期常量) ``` #### **3️⃣ 初始化(Initialization)** 执行 `()` 方法(静态变量赋值 + static 块,按代码顺序执行) **触发初始化的 6 种时机**(主动引用): 1. `new` 创建对象 2. 访问/修改静态变量(被 final 修饰的常量除外) 3. 调用静态方法 4. 反射调用 `Class.forName("com.test.User")` 5. 初始化子类会先触发父类初始化 6. 启动类(包含 `main` 方法的类) **不会触发初始化**(被动引用): ```java // ❌ 通过子类引用父类静态字段 System.out.println(Child.parentValue); // ❌ 通过数组定义引用类 Parent[] arr = new Parent[10]; // ❌ 引用常量(编译期常量折叠) System.out.println(Config.CONSTANT); ``` ### **2.2 完整工作流程图** ``` 请求加载类:com.example.User ↓ 1. findLoadedClass() ← 检查是否已加载(缓存) ↓ 未加载 2. 【双亲委派】parent.loadClass() ← 委托给父加载器 ↓ Bootstrap → Extension → Application → Custom ↓ 逐层向上委托 ↓ 父加载器无法加载(ClassNotFoundException) 3. findClass() ← 当前加载器自己加载 ↓ 4. defineClass() ← 将字节码转为 Class 对象 ↓ 5. 链接:验证 → 准备 → 解析 ↓ 6. 初始化:执行 () ↓ 返回 Class 对象 ``` --- ## **三、双亲委派模型详解** ### **3.1 三层默认类加载器(JDK 8)** | **加载器名称** | **加载路径** | **父加载器** | **可被程序获取** | **Java 实现** | | --- | --- | --- | --- | --- | | **Bootstrap ClassLoader** | `$JAVA_HOME/jre/lib/rt.jar` 核心类库(String、Object) | 无 | ❌ 返回 `null` | C++ 实现 | | **Extension ClassLoader** | `$JAVA_HOME/jre/lib/ext/*.jar` | Bootstrap | ✅ | `sun.misc.Launcher$ExtClassLoader` | | **Application ClassLoader** | `classpath` 指定路径 | Extension | ✅ `ClassLoader.getSystemClassLoader()` | `sun.misc.Launcher$AppClassLoader` | | **Custom ClassLoader** | 自定义路径 | Application | ✅ | 用户自定义 | **JDK 9+ 变化**: - 模块化后,Extension 被 **Platform ClassLoader** 取代 - 加载路径改为模块路径(`--module-path`) ### **3.2 源码分析(**`java.lang.ClassLoader.loadClass`**)** ```java protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 【步骤1】先检查是否已经加载过(缓存机制) Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { // 【步骤2】双亲委派:先让父加载器加载 c = parent.loadClass(name, false); } else { // 【步骤3】parent=null 说明到顶层,交给 Bootstrap c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 父加载器加载失败,不处理异常 } if (c == null) { // 【步骤4】父加载器都无法加载,自己尝试加载 c = findClass(name); // ⭐ 自定义加载器重写此方法 } } // resolve=true 时进行链接中的解析阶段 if (resolve) { resolveClass(c); } return c; } } ``` **关键方法对比**: | **方法** | **作用** | **是否遵循双亲委派** | **自定义时是否重写** | | --- | --- | --- | --- | | `loadClass()` | 完整加载流程(包含双亲委派逻辑) | ✅ | ❌ 不推荐 | | `findClass()` | 实际查找字节码的逻辑 | - | ✅ **推荐重写** | | `defineClass()` | 将字节数组转为 Class 对象 | - | ❌ final 方法 | ### **3.3 典型案例** ```java // 案例1:核心类由 Bootstrap 加载 Class c1 = String.class; System.out.println(c1.getClassLoader()); // null(代表 Bootstrap) // 案例2:自定义 java.lang.String 会加载失败 // java.lang.SecurityException: Prohibited package name: java.lang public class java.lang.String { } // ⚠️ 包名保护机制 // 案例3:验证类加载器层级关系 ClassLoader appLoader = ClassLoader.getSystemClassLoader(); ClassLoader extLoader = appLoader.getParent(); ClassLoader bootLoader = extLoader.getParent(); System.out.println("App: " + appLoader); // sun.misc.Launcher$AppClassLoader System.out.println("Ext: " + extLoader); // sun.misc.Launcher$ExtClassLoader System.out.println("Boot: " + bootLoader); // null // 案例4:打破双亲委派(SPI 机制) DriverManager.class.getClassLoader(); // null(Bootstrap 加载) com.mysql.jdbc.Driver.class.getClassLoader(); // AppClassLoader 加载 // 通过 ThreadContextClassLoader 实现跨加载器调用 ``` --- ## **四、如何破坏/自定义双亲委派?** ### **4.1 三种常见方式** | **方式** | **实现方式** | **典型场景** | **原理** | | --- | --- | --- | --- | | **自定义 ClassLoader** | 重写 `loadClass()` 破坏委托逻辑 | Tomcat、Spring Boot | 先尝试自己加载,再委托父加载器 | | **Thread ContextClassLoader** | `Thread.currentThread().setContextClassLoader()` | JDBC、JNDI、SPI | 父加载器调用子加载器 | | **热替换/热部署** | 创建新 ClassLoader 丢弃旧的 | HotSwap、OSGi | 同名类不同加载器=不同类 | ### **4.2 Tomcat 类加载器结构(破坏双亲委派)** ``` Bootstrap ClassLoader ↓ Extension ClassLoader ↓ Application ClassLoader ↓ Common ClassLoader ← Tomcat/lib(所有 Web 应用共享) ├─ Catalina ClassLoader ← Tomcat 服务器内部类 ├─ Shared ClassLoader ← 多个 Web 应用共享 └─ WebappClassLoader1 ← /WEB-INF/classes & lib(应用隔离) WebappClassLoader2 ← 不同版本的同一个库可以共存 ``` **Tomcat 加载顺序(破坏双亲委派)**: 1. Bootstrap/Extension 加载的类 2. **先加载** `/WEB-INF/classes`(优先使用应用自己的类) 3. **再加载** `/WEB-INF/lib/*.jar` 4. 最后才委托给父加载器 **为什么这样设计?** - 🔧 **隔离性**:不同 Web 应用使用不同版本的 Spring 互不影响 - 🔄 **灵活性**:支持热部署,只需替换对应的 WebappClassLoader - ⚡ **性能**:避免重复加载同一个库 ### **4.3 SPI 机制(Service Provider Interface)** **问题**:`DriverManager` 由 Bootstrap 加载,如何加载 `classpath` 下的 MySQL 驱动? **解决方案**:线程上下文类加载器 ```java // DriverManager 源码(精简版) static { loadInitialDrivers(); // 静态块自动执行 } private static void loadInitialDrivers() { // 获取线程上下文类加载器(默认是 AppClassLoader) ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class); Iterator driversIterator = loadedDrivers.iterator(); while(driversIterator.hasNext()) { driversIterator.next(); // 触发 com.mysql.jdbc.Driver 加载 } } ``` **关键代码**: ```java // 设置线程上下文类加载器 Thread.currentThread().setContextClassLoader(appClassLoader); // ServiceLoader 内部使用上下文类加载器 ClassLoader cl = Thread.currentThread().getContextClassLoader(); Class.forName("com.mysql.jdbc.Driver", false, cl); ``` --- ## **五、类加载器相关面试高频题** ### **Q1:双亲委派模型的作用是什么?** **答**: 1. **避免类重复加载**:同一类只加载一次,通过 `findLoadedClass()` 缓存 2. **保证核心 API 不被篡改**:`java.lang.String` 只能由 Bootstrap 加载 3. **保证类型唯一性**:同一个 ClassLoader + 全限定名 → 唯一的 Class 对象 ### **Q2:为什么自定义类加载器需要重写** `findClass` **而不是** `loadClass`**?** **答**: - 重写 `loadClass` 会**彻底破坏双亲委派机制**,失去安全保障 - 推荐只重写 `findClass`,保留双亲委派的好处 - `loadClass` 已经实现了完整的委派逻辑,只需实现"如何查找字节码"的细节 ### **Q3:**`Class.forName()` **和** `ClassLoader.loadClass()` **的区别?** | **区别点** | `Class.forName()` | `ClassLoader.loadClass()` | | --- | --- | --- | | **是否初始化** | 默认执行静态块(可传 `initialize=false`) | **不执行初始化**(只加载+链接) | | **使用加载器** | 当前类的加载器(可指定) | 指定 ClassLoader | | **典型用途** | JDBC 驱动加载、Spring IOC | 热加载、插件系统、不想触发初始化 | | **方法签名** | `Class.forName(String name, boolean initialize, ClassLoader loader)` | | --- ⬅️ [[01-JVM与内存模型|JVM 与内存模型]] 🏠 [[00-Java|00-Java]] ➡️ [[03-垃圾回收与GC算法|垃圾回收与GC算法]]