要深入理解Java程序的运行,就必须掌握类加载机制。它负责将.class字节码文件转换JVM内存中的运行时数据结构,是整个JVM运行的基石。下图清晰地勾勒出了Java类加载机制的核心脉络:

一、Java程序的宏观执行流程
一个Java程序的旅程始于.java源文件。javac编译器将其编译为平台无关的.class字节码文件。随后,Java虚拟机(JVM)加载这些字节码,并通过解释执行或即时编译(JIT)将其转换为特定操作系统的机器指令来运行。其宏观流程可概括如下:

二、类的生命周期与类加载过程详解
我们常说的“类加载”是一个广义概念,它对应着类生命周期中的“加载”和“连接”两大阶段。下图完整展示了从源代码到类卸载的全过程:

具体而言,类加载过程可细分为五个紧密衔接的核心阶段。
1. 加载(Loading)
加载是类加载过程的第一步,主要完成三件事:
- 通过类的全限定名获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的
java.lang.Class对象,作为访问方法区中该类数据的入口。
注意:Class文件的来源非常广泛,不仅限于本地文件系统,还可以来自网络、ZIP/JAR包、运行时动态生成(如动态代理)等。
2. 验证(Verification)
此阶段目的是确保Class文件的字节流信息符合JVM规范,保证其不会危害虚拟机自身安全。它包含四个检验动作:
- 文件格式验证:验证字节流是否符合Class文件格式规范,如魔数、主次版本号等。
- 元数据验证:对字节码描述的信息进行语义分析,确保符合Java语言规范,例如检查是否有父类、是否继承了final类等。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法且符合逻辑的,例如保证跳转指令不会跳转到方法体之外。
- 符号引用验证:发生在解析阶段,确保后续的解析动作能正常执行,例如检查通过全限定名是否能找到对应的类、访问权限是否合法等。
3. 准备(Preparation)
此阶段正式为类变量(被static修饰的变量) 分配内存(在方法区中)并设置初始值。
- 重要概念:此阶段设置的是数据类型的零值。例如,对于
public static int value = 123;,在准备阶段后value的初始值是0,而非123。真正的赋值动作putstatic指令被编译在<clinit>()方法中,于初始化阶段执行。
- 特殊情况:如果类字段被
static final修饰(即常量),且在字段属性表中存在ConstantValue属性,那么在准备阶段变量就会被直接初始化为指定的值。例如,public static final int value = 123;在准备阶段后,value的值就是123。
4. 解析(Resolution)
此阶段JVM将常量池内的符号引用替换为直接引用。
- 符号引用:以一组符号(如全限定名)来描述所引用的目标,与虚拟机内存布局无关。
- 直接引用:可以是直接指向目标的指针、相对偏移量或能间接定位到目标的句柄,与虚拟机内存布局相关。
- 解析策略:
- 静态解析:如果符号引用在加载时已明确具体目标,则在解析阶段完成替换。
- 动态解析:对于编译期无法确定的引用(如多态、接口实现),则推迟到第一次实际使用时(运行时)再解析。
5. 初始化(Initialization)
这是类加载的最后一步,才开始真正执行类中定义的Java程序代码(字节码)。JVM会执行由编译器自动收集类中所有类变量赋值动作和静态语句块(static{}块)合并生成的<clinit>()方法。JVM保证在子类的<clinit>()方法执行前,其父类的<clinit>()方法已经执行完毕。
三、Java类加载器
类加载器是实现“通过类的全限定名获取该类的二进制字节流”这个动作的代码模块。JVM提供了三层类加载器,它们以组合关系协同工作,是JVM系统的重要组成部分:
-
启动类加载器(Bootstrap Class Loader):
- 由C++实现,是JVM自身的一部分。
- 负责加载
<JAVA_HOME>/lib目录下,或被-Xbootclasspath参数指定路径中的核心类库(如rt.jar)。
- 是所有类加载器的父加载器(顶层)。
-
扩展类加载器(Extension Class Loader):
- 由Java实现,对应
sun.misc.Launcher$ExtClassLoader类。
- 负责加载
<JAVA_HOME>/lib/ext目录下,或java.ext.dirs系统变量指定路径中的所有类库。
-
应用程序类加载器(Application Class Loader):
- 由Java实现,对应
sun.misc.Launcher$AppClassLoader类。
- 负责加载用户类路径(ClassPath)上指定的类库。它是程序中默认的类加载器。
除了这三个系统类加载器,开发者还可以通过继承java.lang.ClassLoader类,定制自己的类加载器,以满足如热部署、从网络或加密文件中加载类等特殊需求。
四、双亲委派模型
1. 工作流程
双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都应有自己的父类加载器(通过组合而非继承实现)。其工作流程如下:
当一个类加载器收到类加载请求时,它首先不会尝试自己加载,而是将请求委派给父类加载器去完成。每一层类加载器都如此处理,因此所有加载请求最终都应传送到顶层的启动类加载器。只有当父加载器反馈自己无法完成加载请求(在搜索范围内未找到所需类)时,子加载器才会尝试自己去加载。
2. 核心优势
- 避免类的重复加载:确保一个类在JVM全局唯一。父加载器加载过后,子加载器不会再加载。
- 保证程序安全:防止核心API库被篡改。例如,用户自定义
java.lang.Object类,双亲委派机制会保证最终由启动类加载器加载核心的Object类,从而阻止恶意代码注入。
3. 破坏双亲委派模型
双亲委派模型并非强制性约束,在特定场景下会被打破,例如:
- SPI(Service Provider Interface)机制:如JDBC。核心接口在
rt.jar中由启动类加载器加载,但具体实现(如MySQL驱动)在ClassPath下,需由应用类加载器加载。为此引入了线程上下文类加载器来实现逆向委托。
- OSGi、JNDI等模块化或热部署技术,也需要动态调整类加载器的委托关系,以实现更灵活的类加载策略。
|