深入解析 Java 的类加载机制
引言
Java 语言的跨平台能力、"一次编写,到处运行"的特性,其基石便是 Java 虚拟机(JVM) 和 类加载机制(Class Loading)。类加载是 Java 程序运行的核心环节,它决定了何时、如何将类的字节码加载到内存中,并最终形成可以被 JVM 执行的组件。
一、什么是类加载?
简单来说,类加载就是 JVM 将类的 .class 文件中的二进制数据读入内存,并进行校验、转换、初始化,最终形成可以被虚拟机直接使用的 Java 类型的过程。
一个关键特点是:类是在运行期间第一次被主动使用时才被动态加载的,而不是在程序启动时就加载所有类。这种"按需加载"的方式极大地节省了内存空间。
二、类的生命周期:七步走
一个类型从被加载到虚拟机内存开始,到卸载出内存为止,它的完整生命周期会经历以下七个阶段:
加载 (Loading):查找并导入 Class 文件。
验证 (Verification):确保加载的 Class 文件的字节流是合法、安全的。
准备 (Preparation):为类变量分配内存并设置默认初始值。
解析 (Resolution):将常量池内的符号引用转换为直接引用。
初始化 (Initialization):执行类构造器
<clinit>()
方法,真正初始化类变量和其他资源。使用 (Using):程序在运行时主动使用这个类。
卸载 (Unloading):从内存中释放类的数据。
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。而解析阶段则较为特殊,它为了支持 Java 的动态绑定特性,可以在初始化阶段之后再开始。
三、类加载的详细过程(五步拆解)
类加载过程主要就是前五个阶段,可以通过一句口诀记忆:“家宴准备了西式菜” → 家(加载) 宴(验证) 准备(准备) 了西(解析) 式(初始化) 菜。
1. 加载
加载是类加载过程的第一个阶段,主要完成三件事:
获取二进制字节流:通过类的全限定名(如
java.lang.Object
)来获取其定义的二进制字节流。这是最灵活的一步,来源可以是 JAR、WAR 包、网络、运行时动态生成(如动态代理)、JSP 文件等。转换存储结构:将这个字节流所代表的静态存储结构转换为方法区(Metaspace) 的运行时数据结构。
生成 Class 对象:在内存中(堆上)生成一个代表这个类的
java.lang.Class
对象,作为程序访问方法区中类型数据的入口。
2. 验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全。主要包括:文件格式验证、元数据验证、字节码验证和符号引用验证。
3. 准备
此阶段正式为类变量分配内存(在方法区中)并设置默认初始值(零值),而非代码中显式赋予的值。
例如:
public static int value = 123;
在准备阶段过后,value
的值为0
,而非123
。特例:如果类变量是常量,那么在准备阶段就会被初始化为代码中指定的值。例如:
public static final int value = 123;
在准备阶段后,value
的值就是123
。
4. 解析
虚拟机将常量池内的符号引用(一组符号来描述所引用的目标)替换为直接引用(直接指向目标的指针、相对偏移量或能间接定位到目标的句柄)的过程。这一步与动态绑定密切相关。
5. 初始化
这是类加载过程的最后一步,真正开始执行类中定义的 Java 程序代码(字节码)。此阶段是执行类构造器 <clinit>()
方法的过程。
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。虚拟机会保证一个类的
<clinit>()
方法在多线程环境中被正确地加锁和同步。这意味着多个线程同时去初始化一个类时,只有一个线程能执行该方法,其他线程会被阻塞。
四、类加载的时机:何时会触发初始化?
JVM 规范严格规定了有且只有以下六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前完成),这被称为主动引用:
遇到
new
,getstatic
,putstatic
,invokestatic
这四条字节码指令时。对应代码场景分别是:创建新实例、读取静态字段(非final
)、设置静态字段(非final
)、调用静态方法。使用
java.lang.reflect
包的方法对类进行反射调用时。当初始化一个类时,发现其父类还未初始化,则先触发其父类的初始化。
虚拟机启动时,用户指定的包含
main()
方法的主类。JDK 7+:当使用动态语言支持时。
JDK 8+:当一个接口定义了
default
默认方法,其实现类初始化时,该接口会先被初始化。
与之相对,被动引用不会触发初始化。经典例子:
通过子类引用父类的静态字段,不会导致子类初始化。
通过数组定义来引用类(如
MyClass[] arr = new MyClass[10];
),不会触发该类的初始化。引用编译期常量,会直接存入调用类的常量池,不会触发定义常量的类的初始化。
五、类加载器:结构的基石
1. 类与类加载器
判断两个类是否“相等”,不仅要求类本身来自同一个 Class 文件,还必须由同一个类加载器加载。每一个类加载器都拥有一个独立的类名称空间。这是实现诸如 OSGi、热部署等技术的基础。
2. 三类加载器
启动类加载器:由 C++ 实现,是 JVM 的一部分。负责加载
\lib
目录下的核心类库。无法被 Java 程序直接引用。扩展类加载器:由 Java 实现,负责加载
\lib\ext
目录或java.ext.dirs
系统变量指定路径下的所有类库。开发者可直接使用。应用程序类加载器:也称系统类加载器。负责加载用户类路径(ClassPath)上所指定的类库。它是程序中默认的类加载器。
六、双亲委派模型:保证秩序与安全
1. 工作机制
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的"父"类加载器(不是继承关系,而是组合关系)。
当一个类加载器收到了类加载的请求时,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器去完成。只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
2. 重要作用
保证Java核心库的安全与稳定:例如,无论哪个加载器试图加载
java.lang.Object
类,最终都会被委派给启动类加载器,从而保证了所有程序中使用的Object
类都是同一个核心类,避免了用户自定义一个恶意的Object
类造成混乱。避免类的重复加载:委派机制保证了类的全局唯一性。
七、对象的创建过程( 五步 )
类加载完成后,便可创建对象实例。对象创建在 JVM 中是一个精细的过程:
类加载检查:遇到
new
指令,检查符号引用代表的类是否已被加载、解析和初始化过。分配内存:在堆中为新生对象划分一块确定大小的内存空间(方式有"指针碰撞"和"空闲列表"两种)。
初始化零值:将分配到的内存空间(不包括对象头)都初始化为零值。
设置对象头:将对象的类元信息、哈希码、GC分代年龄等数据存储在对象头中。
执行
<init>
方法:按照程序员的意愿,执行构造方法中的代码,进行初始化,一个真正可用的对象才算完全产生。