Java中的类加载机制详解
字数 3386 2025-11-03 18:01:32

Java中的类加载机制详解

类加载机制是Java虚拟机(JVM)将类的字节码文件(.class文件)加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型的过程。这个过程是Java实现“一次编写,到处运行”和动态扩展能力的核心。

一、 类加载的时机

JVM规范并没有强制规定类在什么时候必须加载,但对初始化阶段有严格规定。有且仅有以下6种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  1. 遇到newgetstaticputstaticinvokestatic这四条字节码指令时(对应代码场景:使用new关键字实例化对象、读取或设置一个类的静态字段、调用一个类的静态方法)。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候。
  3. 当初始化一个类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
  6. 当一个接口中定义了JDK 8新加入的默认方法(default方法)时,如果有这个接口的实现类发生了初始化,那么该接口要在其之前被初始化。

二、 类加载的过程

类加载的全生命周期包括:加载(Loading)验证(Verification)准备(Preparation)解析(Resolution)初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)。我们重点关注前5个核心阶段。

1. 加载

  • 任务:查找并载入类的二进制字节流(.class文件)到JVM内存中。
  • 过程
    • 通过一个类的全限定名(如java.lang.String)来获取定义此类的二进制字节流。
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    • 在内存中(堆区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  • 细节:获取字节流的方式非常灵活,可以从ZIP/JAR/WAR包、网络、运行时计算生成(动态代理)、由其他文件生成(JSP)等。

2. 验证

  • 目的:确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息不会危害虚拟机自身的安全。
  • 主要步骤
    • 文件格式验证:验证字节流是否符合Class文件格式的规范(例如,魔数、版本号等)。
    • 元数据验证:对字节码描述的信息进行语义分析,保证其符合Java语言规范(例如,这个类是否有父类,是否继承了不允许被继承的类等)。
    • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的(例如,保证跳转指令不会跳转到方法体以外的字节码指令上)。
    • 符号引用验证:发生在解析阶段,确保解析动作能正常执行(例如,通过字符串描述的全限定名是否能找到对应的类)。

3. 准备

  • 任务:为类变量(静态变量)分配内存并设置初始值
  • 关键点
    • 分配内存的仅包括类变量,不包括实例变量,实例变量会在对象实例化时随着对象一起分配在Java堆中。
    • 设置的初始值通常是数据类型的零值。例如:
      • public static int value = 123; 在准备阶段后,value的值为0,而不是123
    • 特殊情况:如果类字段的字段属性表中存在ConstantValue属性(即被final static修饰),那么在准备阶段变量value就会被初始化为ConstantValue属性所指定的值。
      • public final static int value = 123; 在准备阶段后,value的值就是123

4. 解析

  • 任务:将常量池内的符号引用替换为直接引用的过程。
  • 符号引用:以一組符号来描述所引用的目标,符号可以是任何形式的字面量,与虚拟机实现的内存布局无关。
  • 直接引用:可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄,与虚拟机实现的内存布局相关。
  • 解析目标:主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行。

5. 初始化

  • 任务:执行类构造器<clinit>()方法的过程。这是类加载过程的最后一步。
  • <clinit>()方法是什么
    • 它是由编译器自动收集类中的所有类变量的赋值动作静态语句块(static{}块)中的语句合并产生的。编译器收集的顺序是由语句在源文件中出现的顺序决定的。
    • 它不需要显式定义。
  • 初始化步骤详解
    1. JVM会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。
    2. 由于父类的<clinit>()方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
    3. <clinit>()方法对于类或接口不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
    4. 接口也有<clinit>()方法,但执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。
    5. JVM会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步。这意味着多个线程同时去初始化一个类,只有一个线程能去执行这个类的<clinit>()方法,其他线程都需要阻塞等待。

三、 类加载器

类加载器是实现“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作的代码模块。

1. 双亲委派模型
Java虚拟机从三层类加载器构成:

  • 启动类加载器(Bootstrap Class Loader):最顶层,由C++实现,负责加载<JAVA_HOME>/lib目录下的核心类库(如rt.jar)。
  • 扩展类加载器(Extension Class Loader):由Java实现,负责加载<JAVA_HOME>/lib/ext目录下的扩展类库。
  • 应用程序类加载器(Application Class Loader):由Java实现,负责加载用户类路径(ClassPath)上指定的类库。一般情况下,这是程序中默认的类加载器。

工作流程(双亲委派)

  1. 当一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
  2. 每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中。
  3. 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

优势

  • 安全性:可以确保Java核心库的类型安全。比如,用户自定义一个java.lang.Object类,通过双亲委派,最终会由启动类加载器去加载核心库里的Object类,而不是用户自定义的,从而防止核心API被篡改。
  • 避免重复加载:保证了类的全局唯一性。

2. 破坏双亲委派模型
在某些场景下需要破坏这个模型,例如:

  • SPI(Service Provider Interface)机制:如JDBC。核心接口在rt.jar中由启动类加载器加载,但具体实现由各个厂商提供,位于ClassPath下。这就需要启动类加载器去请求应用程序类加载器完成加载,使用了线程上下文类加载器(Thread Context ClassLoader)来逆向委派。
  • OSGiJNDI等模块化热部署技术。
Java中的类加载机制详解 类加载机制是Java虚拟机(JVM)将类的字节码文件(.class文件)加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型的过程。这个过程是Java实现“一次编写,到处运行”和动态扩展能力的核心。 一、 类加载的时机 JVM规范并没有强制规定类在什么时候必须加载,但对 初始化 阶段有严格规定。有且仅有以下6种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始): 遇到 new 、 getstatic 、 putstatic 或 invokestatic 这四条字节码指令时(对应代码场景:使用 new 关键字实例化对象、读取或设置一个类的静态字段、调用一个类的静态方法)。 使用 java.lang.reflect 包的方法对类进行反射调用的时候。 当初始化一个类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。 当使用JDK 7新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic , REF_putStatic , REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。 当一个接口中定义了JDK 8新加入的默认方法( default 方法)时,如果有这个接口的实现类发生了初始化,那么该接口要在其之前被初始化。 二、 类加载的过程 类加载的全生命周期包括: 加载(Loading) 、 验证(Verification) 、 准备(Preparation) 、 解析(Resolution) 、 初始化(Initialization) 、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为 连接(Linking) 。我们重点关注前5个核心阶段。 1. 加载 任务 :查找并载入类的二进制字节流(.class文件)到JVM内存中。 过程 : 通过一个类的 全限定名 (如 java.lang.String )来获取定义此类的二进制字节流。 将这个字节流所代表的静态存储结构转化为 方法区 的运行时数据结构。 在内存中(堆区)生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。 细节 :获取字节流的方式非常灵活,可以从ZIP/JAR/WAR包、网络、运行时计算生成(动态代理)、由其他文件生成(JSP)等。 2. 验证 目的 :确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息不会危害虚拟机自身的安全。 主要步骤 : 文件格式验证 :验证字节流是否符合Class文件格式的规范(例如,魔数、版本号等)。 元数据验证 :对字节码描述的信息进行语义分析,保证其符合Java语言规范(例如,这个类是否有父类,是否继承了不允许被继承的类等)。 字节码验证 :通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的(例如,保证跳转指令不会跳转到方法体以外的字节码指令上)。 符号引用验证 :发生在解析阶段,确保解析动作能正常执行(例如,通过字符串描述的全限定名是否能找到对应的类)。 3. 准备 任务 :为 类变量(静态变量) 分配内存并设置 初始值 。 关键点 : 分配内存的仅包括类变量,不包括实例变量,实例变量会在对象实例化时随着对象一起分配在Java堆中。 设置的初始值通常是数据类型的 零值 。例如: public static int value = 123; 在准备阶段后, value 的值为 0 ,而不是 123 。 特殊情况 :如果类字段的字段属性表中存在 ConstantValue 属性(即被 final static 修饰),那么在准备阶段变量 value 就会被初始化为 ConstantValue 属性所指定的值。 public final static int value = 123; 在准备阶段后, value 的值就是 123 。 4. 解析 任务 :将常量池内的 符号引用 替换为 直接引用 的过程。 符号引用 :以一組符号来描述所引用的目标,符号可以是任何形式的字面量,与虚拟机实现的内存布局无关。 直接引用 :可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄,与虚拟机实现的内存布局相关。 解析目标 :主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行。 5. 初始化 任务 :执行类构造器 <clinit>() 方法的过程。这是类加载过程的最后一步。 <clinit>() 方法是什么 : 它是由编译器自动收集类中的所有 类变量的赋值动作 和 静态语句块( static{} 块) 中的语句合并产生的。编译器收集的顺序是由语句在源文件中出现的顺序决定的。 它不需要显式定义。 初始化步骤详解 : JVM会保证在子类的 <clinit>() 方法执行前,父类的 <clinit>() 方法已经执行完毕。 由于父类的 <clinit>() 方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作。 <clinit>() 方法对于类或接口不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成 <clinit>() 方法。 接口也有 <clinit>() 方法,但执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。 JVM会保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁、同步。这意味着多个线程同时去初始化一个类,只有一个线程能去执行这个类的 <clinit>() 方法,其他线程都需要阻塞等待。 三、 类加载器 类加载器是实现“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作的代码模块。 1. 双亲委派模型 Java虚拟机从三层类加载器构成: 启动类加载器(Bootstrap Class Loader) :最顶层,由C++实现,负责加载 <JAVA_HOME>/lib 目录下的核心类库(如 rt.jar )。 扩展类加载器(Extension Class Loader) :由Java实现,负责加载 <JAVA_HOME>/lib/ext 目录下的扩展类库。 应用程序类加载器(Application Class Loader) :由Java实现,负责加载用户类路径(ClassPath)上指定的类库。一般情况下,这是程序中默认的类加载器。 工作流程(双亲委派) : 当一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求 委派给父类加载器 去完成。 每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中。 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。 优势 : 安全性 :可以确保Java核心库的类型安全。比如,用户自定义一个 java.lang.Object 类,通过双亲委派,最终会由启动类加载器去加载核心库里的 Object 类,而不是用户自定义的,从而防止核心API被篡改。 避免重复加载 :保证了类的全局唯一性。 2. 破坏双亲委派模型 在某些场景下需要破坏这个模型,例如: SPI(Service Provider Interface)机制 :如JDBC。核心接口在 rt.jar 中由启动类加载器加载,但具体实现由各个厂商提供,位于ClassPath下。这就需要启动类加载器去请求应用程序类加载器完成加载,使用了 线程上下文类加载器(Thread Context ClassLoader) 来逆向委派。 OSGi 、 JNDI 等模块化热部署技术。