跳到主要内容

类加载机制

类加载机制是JVM的核心功能之一,它负责将编译后的.class文件加载到JVM内存中,并进行链接和初始化,最终形成可以被JVM直接使用的Java类型。理解类加载机制对于理解Java程序的运行原理、解决类加载相关的问题至关重要。

类加载的时机

一个类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括七个阶段:

类加载生命周期:

┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌───────┐ ┌───────┐ ┌────────┐
│ 加载 │ → │ 验证 │ → │ 准备 │ → │ 解析 │ → │ 初始化 │ → │ 使用 │ → │ 卸载 │
└────────┘ └────────┘ └────────┘ └────────┘ └───────┘ └───────┘ └────────┘
↑ ↑ ↑
│ 链接阶段 │ │
└──────────────────────────┘

各阶段说明:
- 加载(Loading):将.class文件加载到内存
- 验证(Verification):验证字节码文件的正确性
- 准备(Preparation):为静态变量分配内存并设置初始值
- 解析(Resolution):将符号引用转换为直接引用
- 初始化(Initialization):执行<clinit>方法
- 使用(Using):程序正常运行
- 卸载(Unloading):从内存中移除类

类加载的触发条件

JVM规范没有强制约束何时进行加载,但严格规定了必须进行初始化的情况(只有主动引用才会触发):

  1. 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时

    • new关键字实例化对象
    • 读取或设置静态字段(被final修饰的除外)
    • 调用静态方法
  2. 使用java.lang.reflect包的方法对类进行反射调用时

  3. 初始化一个类时,如果其父类还没有初始化,则先初始化父类

  4. 虚拟机启动时,用户指定的主类(包含main方法的类)

  5. 使用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)

加载阶段是类加载过程的第一个阶段,主要完成三件事:

  1. 通过类的全限定名获取定义此类的二进制字节流
  2. 将字节流代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的Class对象,作为方法区这个类各种数据的访问入口
类加载过程:

┌─────────────┐
│ .class │ 1. 从各种来源读取字节流
│ 字节流 │ (文件/网络/动态生成)
└──────┬──────┘

│ 类加载器处理

┌─────────────┐
│ 类加载器 │ 2. JVM 类加载器处理字节流
│ (ClassLoader)│
└──────┬──────┘

├───────────────────┐
↓ ↓
┌─────────────┐ ┌─────────────┐
│ 方法区 │ │ 堆内存 │
│ 运行时数据 │ │ Class对象 │
│ 结构 │◄────│ (实例) │
└─────────────┘ └─────────────┘
│ │
│←─── Class 对象作为 ──→│
│ 访问入口 │

字节流的来源

  • 从本地文件系统加载
  • 从JAR包、WAR包中加载
  • 从网络获取
  • 动态代理生成
  • JSP编译生成

验证(Verification)

验证是链接阶段的第一步,确保被加载的类信息符合JVM规范,不会危害虚拟机自身安全。

验证内容

  1. 文件格式验证:验证字节流是否符合Class文件格式规范

    • 魔数0xCAFEBABE
    • 主次版本号
    • 常量池中的常量类型
  2. 元数据验证:对字节码描述的信息进行语义分析

    • 是否有父类
    • 是否继承了final类
    • 是否实现了所有抽象方法
  3. 字节码验证:通过数据流和控制流分析,确保程序语义合法

    • 类型转换是否正确
    • 跳转指令是否合法
  4. 符号引用验证:确保符号引用能正确解析

准备(Preparation)

准备阶段为类的静态变量分配内存,并设置初始值:

public class PrepareExample {
public static int value = 123; // 准备阶段:value = 0
public static final int CONSTANT = 123; // 准备阶段:CONSTANT = 123
}

注意

  • value在准备阶段被设置为0,而不是123
  • CONSTANT是final常量,在准备阶段就会被设置为123

解析(Resolution)

解析阶段将常量池内的符号引用替换为直接引用:

  • 符号引用:用一组符号来描述所引用的目标
  • 直接引用:直接指向目标的指针、相对偏移量或句柄

解析的类型

  1. 类或接口解析
  2. 字段解析
  3. 方法解析
  4. 接口方法解析

初始化(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.jarresources.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程序的稳定性和安全性。

工作原理

双亲委派模型的工作流程

  1. 当类加载器收到加载请求时,首先检查该类是否已被加载
  2. 如果未加载,将请求委派给父加载器
  3. 父加载器重复此过程,直到启动类加载器
  4. 如果父加载器无法完成加载,子加载器才尝试自己加载

源码分析

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;
}
}

双亲委派模型的优势

  1. 安全性:防止核心类库被篡改
// 即使自定义了java.lang.String,也不会被加载
package java.lang;

public class String {
// 这个类永远不会被加载!
// 因为启动类加载器已经加载了java.lang.String
}
  1. 避免重复加载:同一个类只会被加载一次

  2. 保证一致性:同一个类在不同加载器眼中是不同的类

打破双亲委派模型

某些场景需要打破双亲委派模型:

  1. SPI机制:JDBC、JNDI等
  2. 热部署: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容器,需要解决以下问题:

  1. 一个Web应用可能有不同版本的类库
  2. 不同的Web应用之间需要隔离
  3. Web应用需要能够使用JVM的核心类库

Tomcat的类加载器

  1. Common Class Loader:加载Tomcat和Web应用共享的类
  2. Catalina Class Loader:加载Tomcat自身的类
  3. Shared Class Loader:加载Web应用共享的类
  4. 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的核心功能:

  1. 类加载时机:主动引用和被动引用
  2. 类加载过程:加载、验证、准备、解析、初始化
  3. 类加载器层次:启动类加载器、扩展类加载器、应用类加载器
  4. 双亲委派模型:保证安全性和一致性
  5. 打破双亲委派:SPI、热部署等场景

理解类加载机制有助于解决类加载相关的各种问题,也是理解Java框架原理的基础。

参考资料