类加载机制
类加载机制是JVM的核心功能之一,它负责将编译后的.class文件加载到JVM内存中,并进行链接和初始化,最终形成可以被JVM直接使用的Java类型。理解类加载机制对于理解Java程序的运行原理、解决类加载相关的问题至关重要。
类加载的时机
一个类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括七个阶段:
类加载生命周期:
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌───────┐ ┌───────┐ ┌────────┐
│ 加载 │ → │ 验证 │ → │ 准备 │ → │ 解析 │ → │ 初始化 │ → │ 使用 │ → │ 卸载 │
└────────┘ └────────┘ └────────┘ └────────┘ └───────┘ └───────┘ └────────┘
↑ ↑ ↑
│ 链接阶段 │ │
└──────────────────────────┘
各阶段说明:
- 加载(Loading):将.class文件加载到内存
- 验证(Verification):验证字节码文件的正确性
- 准备(Preparation):为静态变量分配内存并设置初始值
- 解析(Resolution):将符号引用转换为直接引用
- 初始化(Initialization):执行<clinit>方法
- 使用(Using):程序正常运行
- 卸载(Unloading):从内存中移除类
类加载的触发条件
JVM规范没有强制约束何时进行加载,但严格规定了必须进行初始化的情况(只有主动引用才会触发):
-
遇到new、getstatic、putstatic、invokestatic这四条字节码指令时
- new关键字实例化对象
- 读取或设置静态字段(被final修饰的除外)
- 调用静态方法
-
使用java.lang.reflect包的方法对类进行反射调用时
-
初始化一个类时,如果其父类还没有初始化,则先初始化父类
-
虚拟机启动时,用户指定的主类(包含main方法的类)
-
使用JDK7引入的动态语言支持时
public class ClassInitExample {
public static void main(String[] args) {
// 触发类初始化的例子
// 1. new关键字
new MyClass(); // 触发MyClass初始化
// 2. 访问静态字段
System.out.println(MyClass.STATIC_FIELD); // 触发初始化
// 3. 访问静态方法
MyClass.staticMethod(); // 触发初始化
// 4. 反射
Class.forName("com.example.MyClass"); // 触发初始化
}
}
class MyClass {
public static final String CONSTANT = "常量"; // 不会触发初始化
public static String STATIC_FIELD = "静态字段"; // 会触发初始化
static {
System.out.println("MyClass初始化");
}
public static void staticMethod() {}
}
被动引用
以下情况不会触发类初始化,称为被动引用:
public class PassiveReference {
public static void main(String[] args) {
// 1. 通过子类引用父类的静态字段,只会初始化父类
System.out.println(ChildClass.PARENT_FIELD);
// 2. 通过数组定义来引用类,不会初始化类
ParentClass[] array = new ParentClass[10];
// 3. 引用常量,不会初始化类(编译期常量传播)
System.out.println(ConstClass.HELLO_WORLD);
}
}
class ParentClass {
public static String PARENT_FIELD = "父类字段";
static {
System.out.println("ParentClass初始化");
}
}
class ChildClass extends ParentClass {
static {
System.out.println("ChildClass初始化");
}
}
class ConstClass {
public static final String HELLO_WORLD = "hello world";
static {
System.out.println("ConstClass初始化"); // 不会执行
}
}
类加载的过程
加载(Loading)
加载阶段是类加载过程的第一个阶段,主要完成三件事:
- 通过类的全限定名获取定义此类的二进制字节流
- 将字节流代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的Class对象,作为方法区这个类各种数据的访问入口
类加载过程:
┌─────────────┐
│ .class │ 1. 从各种来源读取字节流
│ 字节流 │ (文件/网络/动态生成)
└──────┬──────┘
│
│ 类加载器处理
↓
┌─────────────┐
│ 类加载器 │ 2. JVM 类加载器处理字节流
│ (ClassLoader)│
└──────┬──────┘
│
├───────────────────┐
↓ ↓
┌─────────────┐ ┌─────────────┐
│ 方法区 │ │ 堆内存 │
│ 运行时数据 │ │ Class对象 │
│ 结构 │◄────│ (实例) │
└─────────────┘ └─────────────┘
│ │
│←─── Class 对象作为 ──→│
│ 访问入口 │
字节流的来源:
- 从本地文件系统加载
- 从JAR包、WAR包中加载
- 从网络获取
- 动态代理生成
- JSP编译生成
验证(Verification)
验证是链接阶段的第一步,确保被加载的类信息符合JVM规范,不会危害虚拟机自身安全。
验证内容:
-
文件格式验证:验证字节流是否符合Class文件格式规范
- 魔数0xCAFEBABE
- 主次版本号
- 常量池中的常量类型
-
元数据验证:对字节码描述的信息进行语义分析
- 是否有父类
- 是否继承了final类
- 是否实现了所有抽象方法
-
字节码验证:通过数据流和控制流分析,确保程序语义合法
- 类型转换是否正确
- 跳转指令是否合法
-
符号引用验证:确保符号引用能正确解析
准备(Preparation)
准备阶段为类的静态变量分配内存,并设置初始值:
public class PrepareExample {
public static int value = 123; // 准备阶段:value = 0
public static final int CONSTANT = 123; // 准备阶段:CONSTANT = 123
}
注意:
value在准备阶段被设置为0,而不是123CONSTANT是final常量,在准备阶段就会被设置为123
解析(Resolution)
解析阶段将常量池内的符号引用替换为直接引用:
- 符号引用:用一组符号来描述所引用的目标
- 直接引用:直接指向目标的指针、相对偏移量或句柄
解析的类型:
- 类或接口解析
- 字段解析
- 方法解析
- 接口方法解析
初始化(Initialization)
初始化阶段是类加载过程的最后一步,真正开始执行类中定义的Java代码:
public class InitializationExample {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
class SuperClass {
public static int value = 123;
static {
System.out.println("SuperClass初始化");
}
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass初始化");
}
}
// 输出:
// SuperClass初始化
// 123
// SubClass不会初始化,因为使用的是父类的静态字段
类加载器
类加载器负责加载类的字节码文件。JVM中的类加载器层次结构如下:
启动类加载器(Bootstrap Class Loader)
- 由C++实现,是JVM的一部分
- 负责加载Java核心类库(
JAVA_HOME/lib/rt.jar、resources.jar等) - 不继承
java.lang.ClassLoader - 加载路径:
sun.boot.class.path
public class BootstrapLoaderExample {
public static void main(String[] args) {
// 核心类库由启动类加载器加载
Class<?> stringClass = String.class;
System.out.println(stringClass.getClassLoader()); // 输出:null
}
}
扩展类加载器(Extension Class Loader)
- Java语言实现,继承自
URLClassLoader - 负责加载Java扩展库(
JAVA_HOME/lib/ext目录下的jar包) - 加载路径:
java.ext.dirs
public class ExtensionLoaderExample {
public static void main(String[] args) {
// 获取扩展类加载器
ClassLoader extLoader = ClassLoader.getSystemClassLoader().getParent();
System.out.println(extLoader);
}
}
应用类加载器(Application Class Loader)
- 也称为系统类加载器
- 负责加载用户类路径(classpath)上的类
- 是程序默认的类加载器
public class AppLoaderExample {
public static void main(String[] args) {
// 获取应用类加载器
ClassLoader appLoader = ClassLoader.getSystemClassLoader();
System.out.println(appLoader);
// 用户自定义的类由应用类加载器加载
System.out.println(AppLoaderExample.class.getClassLoader());
}
}
自定义类加载器
通过继承java.lang.ClassLoader类实现自定义类加载器:
import java.io.*;
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(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, e);
}
}
private byte[] loadClassData(String name) throws IOException {
String fileName = classPath + File.separator +
name.replace('.', File.separatorChar) + ".class";
try (InputStream is = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
}
}
}
双亲委派模型
双亲委派模型是JVM类加载器的工作机制,保证了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. 委派给启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法加载,继续
}
if (c == null) {
// 4. 自己尝试加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派模型的优势
- 安全性:防止核心类库被篡改
// 即使自定义了java.lang.String,也不会被加载
package java.lang;
public class String {
// 这个类永远不会被加载!
// 因为启动类加载器已经加载了java.lang.String
}
-
避免重复加载:同一个类只会被加载一次
-
保证一致性:同一个类在不同加载器眼中是不同的类
打破双亲委派模型
某些场景需要打破双亲委派模型:
- SPI机制:JDBC、JNDI等
- 热部署:OSGi、Tomcat等
// JDBC打破双亲委派的例子
public class DriverManager {
// DriverManager由启动类加载器加载
// 但需要加载用户classpath下的JDBC驱动
// 所以使用了线程上下文类加载器
static {
loadInitialDrivers();
}
private static void loadInitialDrivers() {
// 使用线程上下文类加载器加载驱动
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers =
ServiceLoader.load(Driver.class);
// ...
return null;
}
});
}
}
Tomcat的类加载机制
Tomcat作为Web容器,需要解决以下问题:
- 一个Web应用可能有不同版本的类库
- 不同的Web应用之间需要隔离
- Web应用需要能够使用JVM的核心类库
Tomcat的类加载器:
- Common Class Loader:加载Tomcat和Web应用共享的类
- Catalina Class Loader:加载Tomcat自身的类
- Shared Class Loader:加载Web应用共享的类
- WebApp Class Loader:每个Web应用独立的类加载器
类加载常见问题
ClassNotFoundException
当应用程序尝试通过字符串名称加载类时,找不到类定义:
try {
Class.forName("com.example.NonExistClass");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
解决方法:
- 检查类名是否正确
- 检查类是否在classpath中
- 检查依赖是否正确引入
NoClassDefFoundError
当JVM尝试加载一个类时,该类在编译时存在,但运行时找不到:
public class NoClassDefFoundErrorExample {
public static void main(String[] args) {
// 如果MyClass在编译时存在但运行时不存在
MyClass obj = new MyClass(); // 抛出NoClassDefFoundError
}
}
解决方法:
- 检查运行时classpath
- 检查类初始化是否失败
ClassCastException
类型转换异常:
public class ClassCastExceptionExample {
public static void main(String[] args) {
Object obj = new String("hello");
Integer num = (Integer) obj; // ClassCastException
}
}
类冲突问题
不同版本的同一个类导致的问题:
// 项目依赖A和B,A依赖commons-lang 2.0,B依赖commons-lang 3.0
// 可能导致NoSuchMethodError或NoSuchFieldError
解决方法:
- 使用
mvn dependency:tree分析依赖树 - 排除冲突的依赖
- 使用
<dependencyManagement>统一版本
小结
类加载机制是JVM的核心功能:
- 类加载时机:主动引用和被动引用
- 类加载过程:加载、验证、准备、解析、初始化
- 类加载器层次:启动类加载器、扩展类加载器、应用类加载器
- 双亲委派模型:保证安全性和一致性
- 打破双亲委派:SPI、热部署等场景
理解类加载机制有助于解决类加载相关的各种问题,也是理解Java框架原理的基础。