Java 类加载机制详解
Java 类加载机制是 JVM(Java 虚拟机)的核心组成部分,负责将编译后的 .class 文件(字节码)从磁盘、网络或其他来源加载到内存中,并转换为可执行的 Java 类对象。整个过程遵循双亲委派模型,确保安全性和一致性。
1. 类加载的生命周期(Class Lifecycle)
一个类从加载到卸载,主要经历以下 5 个阶段:
- 加载(Loading)
- 查找并读取字节码文件(.class),通过类加载器将字节码加载到 JVM 的方法区(JDK 8 前是永久代,JDK 8+ 是元空间)。
- 生成对应的
java.lang.Class对象(在堆中)。
- 连接(Linking)
- 验证(Verification):确保字节码的安全性(格式、指令合法性等),防止恶意代码。
- 准备(Preparation):为类的静态变量分配内存,并设置默认初始值(int=0, boolean=false, 引用=null 等)。
- 解析(Resolution):将常量池中的符号引用转换为直接引用(可选,延迟解析)。
- 初始化(Initialization)
- 执行类的静态初始化代码:静态变量赋值 + 静态代码块。
- 初始化发生在类首次主动使用时(如 new、访问静态成员、反射等)。
<clinit>方法(编译器自动生成)。
- 使用(Using)
- 卸载(Unloading)
- 当类不再被任何对象引用,且类加载器也被回收时,由 GC 卸载(自定义类加载器才可能被卸载)。
2. 类加载器(ClassLoader)
Java 默认使用三层类加载器(Bootstrap + Extension + Application),构成层次结构:
| 类加载器名称 | 加载路径 | 特点 |
|---|---|---|
| Bootstrap ClassLoader(启动类加载器) | $JAVA_HOME/lib 中的核心类库(如 rt.jar:Object、String、System 等) | 用 C++ 实现,无 Java 对象引用(null) |
| Extension ClassLoader(扩展类加载器) | $JAVA_HOME/lib/ext 目录下的 jar | 父是 Bootstrap |
| Application ClassLoader(应用类加载器,也称 System ClassLoader) | classpath 指定的路径(项目中的 classes、依赖 jar) | 父是 Extension,通常加载我们写的代码 |
此外还有:
- 自定义类加载器:继承
ClassLoader,重写findClass()方法。 - 线程上下文类加载器(Thread Context ClassLoader):用于打破双亲委派(如 SPI 机制:JDBC、Servlet 容器)。
3. 双亲委派模型(Parental Delegation Model)
这是 Java 类加载机制的核心设计,工作流程:
- 当一个类加载器收到类加载请求时,不会自己先尝试加载。
- 而是将请求向上委托给父类加载器(递归)。
- 只有当父类加载器无法找到该类时,当前类加载器才会尝试自己加载。
优点:
- 安全性:防止用户自定义恶意类覆盖核心类(如自定义 java.lang.String)。
- 避免重复加载:同一个类只会被加载一次(由最高层的加载器加载)。
- 命名空间隔离:不同类加载器加载的同名类是不同的(Class 对象不同)。
打破双亲委派的情况:
- 重写
loadClass()方法(不推荐)。 - SPI 机制(Service Provider Interface):如 JDBC DriverManager 使用线程上下文类加载器加载驱动。
- 模块化系统(Java 9+ 的模块路径)。
- 应用容器(如 Tomcat、Spring Boot)实现隔离。
4. 类加载的触发时机(首次主动使用)
以下操作会触发类初始化(执行 <clinit>):
new创建对象- 访问静态变量或静态方法(包括赋值)
- 反射:
Class.forName("com.test.MyClass")(默认初始化,可传 false 跳过) - 初始化子类时,先初始化父类
- 启动类(包含 main 方法的类)被加载时
- 使用
java.lang.invoke.MethodHandle的 REF_getStatic 等
不会触发初始化的情况:
- 访问常量(编译期常量,如 final static int A = 5)
- 子类访问父类的静态字段(只初始化父类)
Class.forName("xxx", false, loader)
5. 示例代码演示
class Parent {
static {
System.out.println("Parent static block");
}
public static int value = 100;
}
class Child extends Parent {
static {
System.out.println("Child static block");
}
}
public class Test {
public static void main(String[] args) {
System.out.println(Child.value); // 输出:Parent static block → 100
// Child static block 不会执行,因为 Child 类未被主动使用
}
}
6. 常见面试题
- 一个类会被加载几次?
正常情况下只加载一次(由某个类加载器加载)。 - 如何打破双亲委派?
自定义类加载器,重写loadClass(),或使用线程上下文类加载器。 - Tomcat 如何实现 Web 应用隔离?
每个 Web 应用使用独立的 WebappClassLoader,优先加载本应用的类,打破双亲委派。 - 为什么 String 类不能被自定义覆盖?
因为它由 Bootstrap ClassLoader 加载,用户自定义的 String 在不同命名空间。
总结
Java 类加载机制的核心是:
- 生命周期:加载 → 连接(验证+准备+解析) → 初始化 → 使用 → 卸载
- 双亲委派模型:安全 + 避免重复加载
- 类加载器层次:Bootstrap → Extension → Application → 自定义
理解类加载机制对深入掌握 JVM、Spring 容器、热部署、插件化开发至关重要。