类的加载

2020/12/11 JVM

# Java 类加载机制详解

# 1. 什么是类加载?

类加载(Class Loading)是指 JVM 将 .class 文件中的字节码数据加载到内存中,并进行验证、准备、解析和初始化,最终形成可以被 JVM 直接使用的 Java 类型的过程。

类加载器 (ClassLoader) 负责实现类加载过程。

# 2. 类加载的过程

类加载过程是一个复杂的过程,主要包括以下 5 个阶段:

  1. 加载(Loading)
  2. 验证(Verification)
  3. 准备(Preparation)
  4. 解析(Resolution)
  5. 初始化(Initialization)

# 2.1 加载(Loading)

  • 作用: 将 .class 文件中的字节码数据加载到内存中。
  • 过程:
    1. 通过类的全限定名获取该类的二进制字节流。
    2. 将该字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    3. 在内存中生成一个代表该类的 java.lang.Class 对象,作为该类各种数据的访问入口。
  • 类加载器参与: 加载阶段由类加载器完成。

# 2.2 验证(Verification)

  • 作用: 确保被加载的类的正确性,保证 JVM 的安全。
  • 过程:
    1. 文件格式验证: 验证字节流是否符合 .class 文件格式规范,例如是否以魔数开头。
    2. 元数据验证: 验证类的元数据信息是否合法,例如是否存在父类、是否实现了接口等。
    3. 字节码验证: 验证字节码指令是否合法,例如类型转换是否正确、操作数栈是否溢出等。
    4. 符号引用验证: 验证符号引用是否可以正确解析,例如被引用的类是否存在、方法是否存在等。
  • 重要性: 验证阶段非常重要,它可以防止恶意代码的执行,保证 JVM 的安全。

# 2.3 准备(Preparation)

  • 作用: 为类的静态变量分配内存,并设置默认初始值(零值)。
  • 过程:
    • 为类的静态变量(static 变量)在方法区中分配内存。
    • 将静态变量设置为默认初始值,例如 int 类型为 0,boolean 类型为 false,引用类型为 null
    • 注意: 实例变量不会在此阶段分配内存,它们会在对象实例化时分配在堆中。
    • 常量: 如果静态变量被 final 修饰,那么在准备阶段就会被赋予指定的值,而不是默认初始值。

# 2.4 解析(Resolution)

  • 作用: 将类中的符号引用转换为直接引用。
  • 过程:
    • 符号引用: 以符号形式描述所引用的目标,符号可以是任何形式的字面量,只要能无歧义地定位到目标即可。例如,一个方法调用可以使用方法名、参数类型等来表示。
    • 直接引用: 指向目标的指针、偏移量或者句柄。 直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例中解析出来的直接引用一般不会相同。
    • 解析阶段就是将这些符号引用转换为 JVM 可以直接使用的直接引用。
  • 类型:
    • 类或接口的解析: 确定类或接口的具体类型。
    • 字段解析: 确定字段在内存中的位置。
    • 方法解析: 确定方法的入口地址。
    • 接口方法解析: 确定接口方法的具体实现。

# 2.5 初始化(Initialization)

  • 作用: 执行类的初始化代码,为类的静态变量赋予正确的初始值。
  • 过程:
    • 执行类构造器 <clinit>() 方法。
    • <clinit>() 方法由编译器自动收集类中所有静态变量的赋值动作和静态语句块(static {} 块)中的语句合并产生。
    • <clinit>() 方法只会被执行一次。
    • 如果一个类没有静态变量赋值操作或者静态语句块,那么编译器可以不为这个类生成 <clinit>() 方法。
    • 父类优先: 如果一个类有父类,那么 JVM 会先执行父类的 <clinit>() 方法,然后再执行子类的 <clinit>() 方法。
    • 多线程同步: JVM 会保证一个类的 <clinit>() 方法在多线程环境中被正确地同步、加锁。这意味着如果多个线程同时去初始化一个类,那么只会有一个线程能够执行这个类的 <clinit>() 方法,其他线程都需要等待。

# 3. 类加载器 (ClassLoader)

类加载器是 JVM 的重要组成部分,负责将 .class 文件加载到内存中。

# 3.1 类加载器的分类

  • 启动类加载器(Bootstrap ClassLoader):
    • 负责加载 JVM 核心类库,例如 java.lang.* 等。
    • 由 C++ 实现,是 JVM 自身的一部分。
    • 不继承自 java.lang.ClassLoader
  • 扩展类加载器(Extension ClassLoader):
    • 负责加载 JVM 扩展类库,例如 jre/lib/ext 目录下的类库。
    • 由 Java 实现,继承自 java.lang.ClassLoader
  • 应用程序类加载器(Application ClassLoader):
    • 负责加载应用程序的类,也就是 ClassPath 路径下的类。
    • 由 Java 实现,继承自 java.lang.ClassLoader
    • 也称为系统类加载器。
  • 自定义类加载器(Custom ClassLoader):
    • 可以根据需要自定义类加载器,例如加载网络上的类、加密后的类等。
    • 需要继承自 java.lang.ClassLoader,并重写 findClass() 方法。

# 3.2 类加载器的双亲委派模型

  • 定义: 当一个类加载器收到类加载请求时,它不会自己去加载这个类,而是将这个请求委派给父类加载器去完成。 每一层的类加载器都是如此,因此所有的加载请求最终都会到达顶层的启动类加载器中。 只有当父类加载器无法完成加载请求时,子类加载器才会尝试自己去加载。
  • 优点:
    • 安全性: 可以避免恶意代码替换 JVM 核心类库,例如 java.lang.String
    • 稳定性: 可以保证类的唯一性,避免重复加载同一个类。
  • 实现:
    • 通过 ClassLoader 类的 loadClass() 方法实现。
    • loadClass() 方法会首先检查类是否已经被加载,如果已经被加载,则直接返回该类的 Class 对象。
    • 如果没有被加载,则会尝试委派给父类加载器去加载。
    • 如果父类加载器无法加载,则会调用自己的 findClass() 方法去加载。

# 3.3 如何破坏双亲委派模型

虽然双亲委派模型在大多数情况下都工作良好,但在某些特殊情况下,可能需要破坏双亲委派模型。 常见的破坏方式有:

  • 自定义类加载器,重写 loadClass() 方法: 不先委派给父类加载器,而是直接调用自己的 findClass() 方法加载。
  • 使用线程上下文类加载器 (Thread Context ClassLoader): 当父类加载器需要加载的类,但该类又需要调用子类加载器加载的资源时,可以使用线程上下文类加载器。 例如,JNDI、JDBC 等。

# 4. 类的生命周期

一个类从被加载到 JVM 中开始,到卸载出内存为止,整个生命周期包括:

  1. 加载(Loading)
  2. 连接(Linking)
    • 包括验证、准备、解析三个阶段。
  3. 初始化(Initialization)
  4. 使用(Using)
  5. 卸载(Unloading)

其中,验证、准备、解析三个阶段统称为连接(Linking)。

# 5. 总结

类加载机制是 JVM 的核心机制之一,它负责将 .class 文件加载到内存中,并进行各种验证和准备工作,最终形成可以被 JVM 直接使用的 Java 类型。 理解类加载机制有助于我们更好地理解 JVM 的运行原理,进行性能优化,排查问题。 通过学习类加载机制,可以深入了解 Java 语言的底层机制。

Last Updated: 2025/6/20 17:20:46