详细介绍:JVM类加载
类加载的过程
1. 什么是类加载?
简单来说,类加载是指 Java 虚拟机(JVM)将类的.class通过文件(二进制数据)读入内存,并将其转换为 JVM 能够使用的 Class 对象的过程。这个 Class 对象是 Java 反射机制的基石,也是我们在程序中利用这个类的模板。
类加载并不仅仅指“加载”这一个动作,它包含了从查找字节码到类完全可用的一整个生命周期。
2. 类加载的过程(类的生命周期)
类加载的过程主要分为以下三个核心阶段:加载、链接、初始化。其中链接又细分为三个子阶段。
阶段一:加载
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的⼀个阶段,它和类加载Class Loading 是不同的,⼀个是加载 Loading 另⼀个是类加载 Class Loading,所以不要把⼆者搞混了。
此阶段重要做完三件事:
通过类的全限定名获取其定义的二进制字节流。
- 这不仅仅是从文件系统读取 .class 文件,还可以从 JAR、WAR 包、网络、数据库、运行时计算生成(动态代理)等多种来源获取。
将该字节流所代表的静态存储结构转化为办法区的运行时数据结构。
- 方法区(在 HotSpot JVM 8+ 中称为 Metaspace)存储了类的结构信息,如常量池、字段描述、技巧描述等。
在堆内存中生成一个代表这个类的 java.lang.Class 对象。
- 这个 Class 对象作为方法区中该类各种材料的访问入口。我们通过 MyClass.class 或 obj.getClass() 获取的就是这个对象。
注意:要靠类加载器来加载就是数组类本身不由类加载器创建,而是由 JVM 直接在内存中动态构造。但数组的元素类型(Component Type)最终还
阶段二:链接(Linking)
(1)验证(Verification)
验证是连接阶段的第⼀步,这⼀阶段的⽬的是确保Class⽂件的字节 流中涵盖的信息符合《Java虚拟机规范》的全部约束要求,保证这些信 息被当作代码运⾏后不会危害虚拟机⾃⾝的安全。
目的:确保被加载的类的字节流符合 JVM 规范,不会危害 JVM 的安全。
主要检查:
- 文件格式验证:魔数(CAFE BABE)、版本号等。
- 元内容验证:否实现了抽象方法等。就是类是否有父类(除了 Object)、是否是最终类被继承、
- 字节码验证:确保方式体中的代码逻辑正确(如类型转换安全)。
- 符号引用验证:发生在解析阶段,确保符号引用能够被正确解析。
(2)准备(Preparation)
目的:为类的静态变量(static variables) 分配内存并设置初始值。
关键点:
分配内存的仅包括类变量(被 static 修饰的变量),不包括实例变量。
设置的是数据类型的零值,而不是代码中显式赋予的值。
例如:public static int value = 123; 在准备阶段后,value 的初始值是 0,而不是 123。赋值为 123 的动作将在后面的初始化阶段执行。对于 static final修饰的常量(ConstantValue),如果它的类型是基础类型或 String,并且在编译期就能确定值,那么准备阶段就会直接将其初始化为指定的值。例如:public static final int value = 123; 在准备阶段后,value 的值就是 123。
(3)解析(Resolution)
目的:将常量池内的符号引用(Symbolic References) 替换为直接引用(Direct References)。
符号引用:一组用来描述所引用目标的符号,允许是任何形式的字面量,与虚拟机内存布局无关。
直接引用:可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。与虚拟机内存布局相关。
解析的目标通常包括:类或接口、字段、类方法、接口技巧、技巧类型等。
阶段三:初始化(Initialization)
目的:执行类的构造器< clinit >() 技巧的过程,真正开始执行类中定义的 Java 程序代码。
< clinit >() 途径是什么?
- 它是由编译器自动收集类中的所有类变量的赋值动作和静态代码块(static{}块) 中的语句合并产生的。
- 顺序由语句在源文件中出现的顺序决定。
初始化的时机(主动引用,触发初始化):
遇到 new, getstatic, putstatic, invokestatic 这四条字节码指令时。
对应代码场景:应用 new 关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。利用 java.lang.reflect 包的方法对类进行反射调用的时候。
当初始化一个类时,如果其父类还没有被初始化,则得先触发其父类的初始化。
虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。
不会导致初始化的情况(被动引用):
- 通过子类引用父类的静态字段,不会导致子类初始化。
- 通过数组定义来引用类,不会触发此类的初始化。如 MyClass[] arr = new MyClass[10];
- 引用一个类的常量(static final)不会触发初始化,因为常量在编译阶段就存入调用类的常量池了。
3. 类加载器(ClassLoader)
实际执行“加载”阶段动作的组件。就是类加载器
三层类加载器模型( parental delegation model, 双亲委派模型)
Java 保留了三种基本的类加载器:
启动类加载器(Bootstrap ClassLoader):
- C++ 实现,是 JVM 自身的一部分。
- 负责加载 <JAVA_HOME>/lib 目录下的核心类库(如 rt.jar, charsets.jar 等),或者被 -Xbootclasspath 参数指定的路径中的类。
- 无法被 Java 程序直接引用。
扩展类加载器(Extension ClassLoader):
- Java 搭建,是 sun.misc.Launcher$ExtClassLoader 类。
- 负责加载 <JAVA_HOME>/lib/ext 目录下的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。
- 开发者许可直接使用。
应用程序类加载器(Application ClassLoader):
- sun.misc.Launcher$AppClassLoader 类。就是Java 实现,
- 也叫系统类加载器(System ClassLoader)。
- 负责加载用户类路径(ClassPath)上所指定的类库。
- 它。就是是程序中默认的类加载器。要是没有自定义类加载器,ClassLoader.getSystemClassLoader() 返回的就
双亲委派模型
1. 什么是双亲委派模型?
如果⼀个类加载器收到了类加载的请求,它⾸先不会⾃⼰去尝试加载这个类,⽽是把这个请求委派给⽗类加载器去完成,每⼀个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当⽗加载器反馈⾃⼰⽆ 法结束这个加载请求(它的搜索范围中没有找到所需的类)时,⼦加载器才会尝试⾃⼰去完成加载。
从下到上检查:Application ClassLoader -> Extension ClassLoader -> Bootstrap ClassLoader。
从上到下尝试加载:只有当父加载器反馈自己无法完成这个加载请求(在自己的搜索范围没找到所需的类)时,子加载器才会尝试自己去加载。

启动类加载器:加载 JDK 中 lib ⽬录中 Java 的核⼼类库,即$JAVA_HOME/lib⽬录。 扩展类加载器。加载 lib/ext ⽬录下的类。
应⽤程序类加载器:加载我们写的应⽤程序。
⾃定义类加载器:根据⾃⼰的需求定制类加载器。
2.优点:
避免类的重复加载:确保一个类在 JVM 中全局唯一。⽐如 A 类和 B 类都有⼀个⽗类 C 类,那么当 A 启动时就会将 C 类加载起来,那
么在 B 类进⾏加载时就不需要在重复加载 C 类了。安全:防止核心 API 库被随意篡改。比如用户自定义了一个java.lang.Object 类,如果没有双亲委派,它会被加载,从而破坏 Java 体系。但有了它,这个请求会最终委派给启动类加载器,而启动类加载器加载的是核心的 Object 类,用户的这个类就无法被加载。
3. 打破双亲委派模型
在某些场景下,需要打破双亲委派模型,实现自己的类加载逻辑,例如:
从非标准来源加载类(如网络、数据库)。
搭建热部署、热替换(如 Tomcat 为每个 Web 应用提供独立的类加载器)。
对类进行加密,必须在加载时解密。
如何实现:
通常继承 java.lang.ClassLoader 类,然后重写 findClass(String name) 方法。在这个方法中,编写如何获取类的字节码并调用 defineClass 方法来定义类的逻辑。
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 1. 根据 name 和 classPath,找到 .class 文件,读取为字节数组 byte[]
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
// 2. 调用 defineClass 方法,将字节数组转换为 Class 对象
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
// 实现从特定路径(如文件、网络)读取 .class 文件的逻辑
// 返回字节数组
// ... (具体实现省略)
return null;
}
}
总结
| 概念 | 描述 |
|---|---|
| 过程 加载 | (获取字节流 -> 方式区 -> 生成Class对象) -> 链接(验证 -> 准备 -> 解析) -> 初始化(执行 ) |
| 类加载器 | Bootstrap(核心库) -> Extension(扩展库) -> Application(用户ClassPath) |
| 双亲委派 | 子加载器将加载请求委派给父加载器,父加载器无法完成时子加载器才自己加载。保证了安全性和唯一性。 |
| 自定义 | 继承 ClassLoader,重写 findClass 方法,用于实现非标准来源的类加载、热部署等高级功能。 |
理解类加载机制是深入理解 JVM 工作原理、实现高级特性和处理复杂类冲突问题的关键。
浙公网安备 33010602011771号