一文学会Java类加载
在加载阶段(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中, 并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出ClassFormatError。
在java中类加载的过程可分为三个阶段:加载、链接、初始化
1、加载
在加载阶段(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中, 并映射为JVM认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如jar文件、class文件,甚至是网络数据源等;如果输入数据不是ClassFile的结构,则会抛出ClassFormatError。
加载是类加载的第一个过程,在这个阶段,将完成一下三件事情:
- 通过一个类的全限定名获取该类的二进制流。
- 将该二进制流中的静态存储结构转化为方法去运行时数据结构。
- 在内存中生成该类的 Class 对象,作为该类的数据访问入口。
==在加载阶段中,可以使用jvm内置的类加载器进行加载,也可以由用户自定义的类加载器进行加载==
2、链接
在连接阶段又可分为三个过程,分别是:验证、准备、解析
2.1 验证
验证的目的是为了确保 Class 文件的字节流中的信息不回危害到虚拟机,在该阶段主要完成以下验证
- 文件格式验证(魔数0xCAFEBABE、版本号)
- 元数据验证(继承final类?抽象方法实现?)
- 字节码验证(栈溢出?类型转换合法?)
- 符号引用验证(是否存在对应类/方法/依赖)
2.2 准备
准备阶段是正式为类中定义的变量分配内存并设置初始值的阶段
在准备阶段中,部分字段数据会赋值为默认值,也有部分字段数据会直接赋值,具体如下:
- 静态变量(
static修饰)的内存分配在方法区(元空间)。 - 初始值为类型默认值(如
int为0,boolean为false,引用类型为null)。 - 若存在
final static常量(编译期已知值),直接赋实际值(如final static int x = 123;)。
2.3 解析
**解析阶段(Resolution)是连接阶段(Linking)的核心步骤之一,主要负责将符号引用(Symbolic References)转换为直接引用(Direct References)**使JVM能够直接定位到内存中的具体目标(如类、字段、方法等)。解析阶段是类加载过程中实现动态绑定和运行时代码执行的关键环节。
符号引用 vs 直接引用
- 符号引用(Symbolic Reference) 符号引用是一组符号(字符串),用来描述被引用的目标。例如:
- 类的全限定名(如
java.lang.Object) - 方法名和描述符(如
getName()Ljava/lang/String;) - 字段名和类型(如
count:I) 符号引用在编译时生成,保存在.class文件的常量池(Constant Pool)中,与内存布局无关。
- 直接引用(Direct Reference) 直接引用是JVM可以直接使用的内存地址、偏移量或句柄。例如:
- 指向方法区中类元数据的指针
- 方法在方法表中的索引
- 字段在对象内存中的偏移量 直接引用与JVM的内存模型和运行时结构紧密相关。
3、初始化阶段
初始化阶段(Initialization)是类加载过程的最后一步,负责执行类的初始化代码,包括静态变量的显式赋值和静态代码块(static{})的逻辑。此阶段是类真正可用的标志,直接影响程序的运行时行为。以下从核心机制、触发条件、执行顺序、安全风险等方面展开详细说明。
3.1 初始化阶段的核心任务
- 显式赋值静态变量:为静态变量赋予代码中定义的值(覆盖准备阶段的默认零值)。
- 执行静态代码块:运行类中所有
static{}代码块的逻辑。 - 初始化父类/父接口:确保父类和父接口(若未初始化)先完成初始化。
- 线程安全保证:JVM通过锁机制确保多线程环境下类仅初始化一次。
3.2 触发初始化的条件
初始化在类首次被**主动使用(Active Use)**时触发。主动使用的场景包括:
创建类的实例
new MyClass(); // 触发MyClass初始化访问类的静态变量(非final常量)
int value = MyClass.staticVar; // 若staticVar非final,触发初始化调用类的静态方法
MyClass.staticMethod(); // 触发初始化反射调用类
Class.forName("com.example.MyClass"); // 默认触发初始化子类初始化触发父类初始化
class Child extends Parent {}
new Child(); // 若Parent未初始化,先触发Parent初始化程序入口类
public class Main { public static void main(String[] args) {} } // Main类被初始化在某些场景下不会创建类的初始化
通过子类引用父类的静态字段
class Parent { static int x = 1; }
class Child extends Parent {}
int x = Child.x; // 仅触发Parent初始化,Child不初始化通过数组定义类
MyClass[] arr = new MyClass[10]; // 数组类由JVM生成,不会触发MyClass初始化访问final静态常量(编译期常量)
class Constants { final static int MAX = 100; }
int max = Constants.MAX; // 常量值直接内联到调用类,不触发初始化4、类加载器
Java 类加载器(ClassLoader)是类加载机制的核心组件,负责动态加载类的字节码并生成对应的 Class 对象。类加载器的设计直接影响类的隔离性、安全性以及应用的灵活性,类加载器是在加载阶段进行使用的,以下是java中不同的类加载器以及区别。
Bootstrap ClassLoader(启动类加载器)
Bootstrap ClassLoader由C/C++实现,是JVM的一部分,主要负责加载 JAVA_HOME/lib 目录下的核心类库(如 rt.jar、charsets.jar),它是一个唯一没有父加载器的的类加载器,当一个类的类加载器为Bootstrap ClassLoader时,获取该类的类加载器会为null,如
ClassLoader classLoader = String.class.getClassLoader(); //获取String类的类加载器
System.out.println(classLoader); //输入为nullExtension ClassLoader(扩展类加载器)
该类加载器由java实现,继承自 sun.misc.Launcher$ExtClassLoader,主要负责加载 JAVA_HOME/lib/ext 目录或 java.ext.dirs 系统变量指定路径的扩展类,可以用来加载 JDK 扩展功能(如加密算法、XML 解析器)
Application ClassLoader(应用类加载器)
该类加载器由Java 实现,继承自 sun.misc.Launcher$AppClassLoader,用于加载用户类路径(ClassPath)下的类,该类加载器是默认的类加载器。
类加载器的核心方法
类加载的入口 loadClass
loadClass是类加载的入口方法,负责协调双亲委派模型(Parent Delegation Model),它主要负责的功能如下:
- 检查是否已加载:通过
findLoadedClass检查类是否已被当前类加载器加载。 - 委派父加载器:若未加载,优先调用父类加载器的
loadClass方法。 - 调用
findClass:若父类加载失败,调用findClass尝试自行加载。 - 触发解析:根据
resolve参数决定是否调用resolveClass完成类的链接。
核心代码如下:
protected Class<?> loadClass(String name, boolean resolve) {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
// 2. 委派父加载器
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
// 3. 父加载失败,调用findClass
if (c == null) {
c = findClass(name);
}
}
// 4. 解析类(若需要)
if (resolve) {
resolveClass(c);
}
return c;
}findClass
findClass是类加载器的扩展点,需要子类重写以定义如何从特定来源加载字节码(如文件、网络、加密数据等,它主要负责的功能如下:
- 加载字节码:根据类名定位并读取字节码数据(如
.class文件)。 - 调用
defineClass:将字节码转换为Class对象。
核心代码:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = loadClassBytes(name); // 自定义加载逻辑
return defineClass(name, bytes, 0, bytes.length);
}defineClass 将字节码转换为Class对象
defineClass是JVM提供的底层方法,负责将字节数组转换为Class对象。其核心任务包括:
- 验证字节码格式:确保符合JVM规范(如魔数、版本号)。
- 解析类结构:将字节码转换为方法区的运行时数据结构。
- 生成
Class对象:在堆中创建Class实例,作为访问类元数据的入口。
resolveClass 完成类的解析
resolveClass触发类的解析阶段(属于链接阶段的一部分),负责将符号引用转换为直接引用。其核心任务包括:
- 验证类的依赖:确保父类、接口已加载。
- 解析符号引用:将常量池中的类、方法、字段引用解析为内存地址或偏移量。
- 分配内存:为静态变量分配内存(实际在准备阶段完成)。
更多功能方法参考如下表:
方法名 | 功能描述 | 调用时机 | 是否可重写 |
| 类加载的入口方法,实现双亲委派机制: | 调用 | 可重写(需谨慎,可能破坏双亲委派) |
| 实际加载类的逻辑,由子类实现。 |
| 必须重写(自定义加载器时) |
| 将字节数组转换为 |
| 不可重写(final 方法) |
| 执行类的链接(Linking),包括验证、准备和解析阶段。 | 类加载后,需解析时调用(如使用 | 可重写(极少需要) |
| 检查类是否已被当前加载器加载,避免重复加载。 |
| 可重写(一般无需) |
| 返回父类加载器,实现双亲委派层级。 | 获取类加载器父子关系时调用 | 不可重写(final 方法) |
| 查找资源(如配置文件),遵循类加载器的资源加载规则。 | 调用 | 可重写(自定义资源路径时) |
| 实际查找资源的逻辑,由子类实现。 |
| 可重写(自定义资源加载时) |
5、 双亲委派模型
双亲委派模型是Java类加载机制的核心规则,采用层级化的类加载器结构(Bootstrap→Extension→Application)和自底向上的委派逻辑:当一个类加载请求发生时,子加载器不会立即加载,而是逐层向上委托父加载器尝试加载,只有当所有父加载器均无法完成时,子加载器才会自行加载。这种设计避免核心类库(如java.lang.String)被篡改,确保类全局唯一性(防止重复加载),同时提升安全性(如阻止恶意代码冒充系统类),但为支持SPI、热部署等场景,可通过线程上下文类加载器(TCCL)或自定义加载器打破委派。
┌─────────────┐ ┌───────────────┐
│ 子加载器收到请求 │ → 委派 → │ 父加载器尝试加载 │
└─────────────┘ └───────────────┘
↑ │
│ 父加载器失败 ▼
└───────────────── 子加载器自行加载具体的步骤为:
- 向上委派:子加载器不立即加载类,而是递归将请求交给父加载器。
- 父加载器处理:
- 若父加载器能加载,直接返回
Class对象。 - 若父加载器无法加载,继续向上委派,直到
Bootstrap加载器。
- 子加载器兜底:若所有父加载器均无法加载,子加载器才尝试自己加载。
类加载器的核心逻辑在ClassLoader.loadClass()方法中:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 递归委派给父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 到达 Bootstrap 加载器(父加载器为 null)
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法加载,继续后续逻辑
}
// 3. 父加载器未找到,由当前加载器加载
if (c == null) {
c = findClass(name);
}
}
// 4. 解析类(如链接阶段)
if (resolve) {
resolveClass(c);
}
return c;
}
}特别情况
在某些场景中,需要打破双亲委派模型的机制,比如Tomcat中,每个Web应用使用独立的类加载器,优先加载自己目录下的类,而不是直接委托给父加载器,这样可以实现不同Web应用之间的类隔离,避免类冲突,Java的SPI机制中,核心接口由Bootstrap类加载器加载,但具体实现类需要由线程上下文类加载器加载,这需要打破双亲委派。在热部署的场景中,如果每次修改代码后都遵循双亲委派,父加载器已经加载过旧版本的类,子加载器就无法重新加载新类,导致无法热更新。因此,需要自定义类加载器,优先自己加载,绕过父加载器的缓存。
Tomcat 的 Web 应用类加载器(WebAppClassLoader)
Tomcat 的 Web 应用类加载器(WebAppClassLoader)是其实现 多应用隔离 和 热部署 的核心组件,通过打破双亲委派模型的默认规则,解决了 Web 应用中常见的类冲突和版本隔离问题。WebAppClassLoader 通过重写 loadClass() 方法实现自定义加载逻辑:
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已加载
Class<?> clazz = findLoadedClass(name);
if (clazz != null) return clazz;
// 2. 优先尝试从本地加载(WEB-INF/classes 或 WEB-INF/lib)
try {
clazz = findClass(name); // 本地加载
if (clazz != null) return clazz;
} catch (ClassNotFoundException e) {
// 本地未找到,继续委派给父加载器
}
// 3. 若本地未找到,委派给父加载器(Common/Shared)
if (clazz == null) {
ClassLoader parent = getParent();
if (parent != null) {
clazz = parent.loadClass(name);
} else {
throw new ClassNotFoundException(name);
}
}
// 4. 解析类
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
}SPI机制
Java 的 SPI(Service Provider Interface)机制 是一种服务发现与动态扩展机制,允许框架或核心库定义接口规范,由第三方或用户提供具体实现,并在运行时自动加载这些实现类。SPI 的核心目标是解耦接口与实现,
SPI 的典型应用场景
JDBC 驱动加载
核心接口如JDBC的Driver接口位于rt.jar中,由Bootstrap加载器加载。而具体的实现类如MySQL的Driver实现则在应用类路径下,由应用类加载器(AppClassLoader)加载。根据双亲委派模型,父加载器无法加载子加载器的类,因此Bootstrap加载器无法直接加载应用类路径中的实现类,导致SPI无法正常工作。
针对上述问题,场景的解决方法是通过线程上下文类加载器(TCCL) 进行加载
TCCL允许父加载器(Bootstrap)通过 TCCL 主动使用子加载器(如 AppClassLoader)加载类。
// 在核心类(如 DriverManager)中获取 TCCL
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
// 使用 TCCL 加载实现类
Class<?> driverClass = contextClassLoader.loadClass("com.mysql.jdbc.Driver");具体的流程逻辑为:
1. 用户调用 DriverManager.getConnection(url)
↓
2. DriverManager 初始化,触发加载所有驱动
↓
3. 通过 ServiceLoader.load(Driver.class) 加载驱动类
↓
4. ServiceLoader 使用 TCCL(AppClassLoader)加载 META-INF/services/java.sql.Driver 文件中的驱动类名
↓
5. TCCL 加载 com.mysql.jdbc.Driver(位于应用类路径)
↓
6. 驱动类注册到 DriverManager关键代码
// ServiceLoader.load() 源码
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取 TCCL(默认为 AppClassLoader)
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(service, cl);
}
// ServiceLoader 使用 TCCL 加载实现类
public Iterator<S> iterator() {
return new Iterator<>() {
private final Iterator<Provider<S>> knownProviders = providers.iterator();
public boolean hasNext() {
// 通过 TCCL 加载实现类
if (knownProviders.hasNext()) return true;
return lookupIterator.hasNext();
}
// ...
};
}6、如何编写一个自定义的解密类加载器
编写自定义类加载器通常需要继承ClassLoader类,并重写findClass方法,从指定位置读取类的字节码,然后调用defineClass方法生成Class对象。具体实现代码如下:
编写一个HelloWorld类
package org.example;
import java.io.IOException;
public class HelloWorld {
//该类在实例化的时候,会自动触发static代码块,弹出计算器
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void sayHello(){
System.out.println("Hello World");
}
public static void main(String[] args) {
System.out.println("Hello World");
}
}编写一个Class文件加密代码
下面类会为指定的Class文件(HelloWorld.class)生成加密后的字节码文件HelloWorld.enc
package org.example;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.util.Arrays;
public class ClassEncryptor {
public static void main(String[] args) throws Exception {
String key = "R0x7e"; // 加密密钥(需与 TestClassLoader 一致)
//
String inputClassPath = "F:\\project\\JavaCodeAudit\\JavaSecurity\\JavaSecurity\\LearnCloassLodaer\\target\\classes\\org\\example\\HelloWorld.class";
String outputPath = "HelloWorld.enc";
// 读取原始类文件
byte[] classBytes = Files.readAllBytes(Paths.get(inputClassPath));
// 加密
byte[] encrypted = encrypt(classBytes, key);
// 保存加密文件
Files.write(Paths.get(outputPath), encrypted);
}
//加密函数
private static byte[] encrypt(byte[] data, String key) throws Exception {
// 使用 SHA-128 生成 16 字节的密钥(128 位)
byte[] keyBytes = MessageDigest.getInstance("SHA-1") // SHA-1 生成 20 字节,截取前 16 字节
.digest(key.getBytes());
keyBytes = Arrays.copyOf(keyBytes, 16); // 强制截取为 16 字节(128 位)
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
return cipher.doFinal(data);
}
}编写一个类加载器
package org.example;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
/**
* 自定义类加载器 - 从 AES 加密文件加载类
* 功能:支持解密加密的 .class 文件并加载类
* 使用方式:
* 1. 加密类文件:将编译后的 .class 文件用 AES 加密,保存为 .enc 文件
* 2. 将加密文件放在项目根目录的 encrypted 文件夹下
* 3. 通过 TestClassLoader 加载类
*/
public class TestClassLoader extends ClassLoader {
private final String key; // AES 加密密钥
private final String basePath; // 加密文件存放的基础路径
public TestClassLoader(String key, String basePath) {
this.key = key;
this.basePath = basePath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 1. 从加密文件读取字节码
byte[] encryptedBytes = loadEncryptedClass(name);
// 2. 解密字节码
byte[] decryptedBytes = decrypt(encryptedBytes);
// 3. 定义类
return defineClass(name, decryptedBytes, 0, decryptedBytes.length);
} catch (Exception e) {
throw new ClassNotFoundException("Failed to load class: " + name, e);
}
}
// 读取加密的类文件
private byte[] loadEncryptedClass(String className) throws Exception {
String path = className.replace('.', File.separatorChar) + ".enc";
File file = new File(basePath + File.separator + path);
try (FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
}
}
// AES 解密
private byte[] decrypt(byte[] encryptedData) throws Exception {
// 生成合法的 AES 密钥(128/256位)
byte[] keyBytes = MessageDigest.getInstance("SHA-1")
.digest(key.getBytes(StandardCharsets.UTF_8));
keyBytes = Arrays.copyOf(keyBytes, 16); // 强制截取为 16 字节(128 位)
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
return cipher.doFinal(encryptedData);
}
}使用类加载器加载加密的字节码文件
package org.example;
public class Main {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
TestClassLoader r0x7e = new TestClassLoader("R0x7e", "HelloWorld.enc");
Class<?> aClass = r0x7e.loadClass("org.example.HelloWorld");
HelloWorld helloWorld = (HelloWorld) aClass.newInstance();
helloWorld.sayHello();
}
}执行过上面的方法之后,就可以看到弹出了计算器,并控制台输出了HelloWorld,但是上面的HelloWorld对象的类加载还是AppClassLoader,而不是TestClassLoader,这是因为双亲委派模型的原因。
需要注意的一些问题
类的唯一性
同一类被不同类加载器加载会被视为不同的类(如MyClass@ClassLoaderA vs MyClass@ClassLoaderB)。
被动引用
某些情况不会触发初始化(如通过子类访问父类静态字段,或通过数组定义类)。
接口初始化
接口的初始化不要求父接口全部初始化(除非主动使用父接口)。
类卸载
类需满足无实例、无Class对象被引用、类加载器可被回收,才能被卸载(由JVM决定)。