Java_Sec 类的动态加载

xekOnerR Sleep.. zzzZZzZ

https://drun1baby.top/2022/06/03/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E7%A1%80%E7%AF%87-05-%E7%B1%BB%E7%9A%84%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD/

类加载器及双亲委派

什么是类加载器

类加载器, Java ClassLoader , 主要作用就是加载 Class 文件

1
Test test = new Test();

Test 本身是一个抽象类,通过 new 操作将其实例化,类加载器做的就是这个工作

类加载过程

加载
链接 (验证、准备、解析)
初始化
使用
卸载

加载器类型

  • 引导类加载器(BootstrapClassLoader),底层由 C++编写,属于JVM 的一部分
    不继承 java.lang.ClassLoader 类, 也没有父加载器

  • 扩展类加载器(ExtensionClassLoader)
    由 sun.misc.Launcher$ExtClassLoader 类实现

  • App类加载器(AppClassLoader)
    由 sun.misc.Launcher$AppClassLoader 实现

双亲委派机制

是 Java 类加载器在加载类的时候遵循的一种”优先向上级请示”的策略
为了保证类加载的安全性和唯一性而设计的
Bootstrap ClassLoader -> Extension ClassLoader -> Application ClassLoader -> Custom ClassLoader

执行流程:

  • 检查缓存:先看自己是否已经加载过这个类,如果加载过直接返回。
  • 向上委派:如果没有,它不会尝试自己加载,而是把请求转发给父类加载器
  • 循环往复:父类加载器接到请求后,重复上述逻辑,一直传到最顶层的 Bootstrap ClassLoader
  • 向下派发
    • 如果顶层加载器在它的搜索范围(如 rt.jar)里找到了,就直接返回。
    • 如果没找到,它会告诉子加载器:“我处理不了,你自己试一下”。
    • 子加载器尝试加载,失败了再往下传。如果最终所有加载器都找不到,就会抛出 ClassNotFoundException

各代码块执行顺序

构造代码块 {} :不管调用哪种构造方法,都会执行到构造代码块中
静态代码块 static{}:不管调用哪种静态方法,都会调用到静态代码块

Test 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static class Test {  
public static int staticVar;
public int instanceVar;
{
System.out.println("构造代码块");
}
static {
System.out.println("静态代码块");
}
Test(){
System.out.println("无参构造器");
}

Test(int instanceVar) {
System.out.println("有参构造器");
}

public static void staticAction(){
System.out.println("静态方法");
}
}

实例化对象

1
2
3
4
5
6
7
8
public class Demo {  
public static void main(String[] args) {
new Test();
}

// 静态代码块
// 构造代码块
// 构造方法

首先调用的是静态代码块 static() ,其次是构造代码块 {} , 最后是构造方法 Test()

当执行 new Test(); 操作之前,一个类在使用前 JVM 必须先加载到内存中。
所以类加载优先于对象实例化,类被加载,静态代码块 static{} 会被立即触发,只会被调用一次
类加载过后,执行 new Test() 创建一个对象。构造代码块 {} 被执行。
最后是构造函数 Test()

调用静态方法

1
2
3
4
5
6
public static void main(String[] args) {  
Test.staticAction();
}

// 静态代码块
// 静态方法

不实例化对象直接调用静态方法,会先调用类中的静态代码块,然后调用静态方法。

对类中的静态变量进行赋值

1
2
3
4
5
public static void main(String[] args) {  
Test.staticVar = 1;
}

// 静态代码块

在对静态成员变量赋值前,会调用静态代码块

使用 class 获取类

1
2
3
public static void main(String[] args) {  
Class c = Test.class;
}

利用 class 关键字获取类,自会进行加载,但不会进行初始化,也就是什么也不会输出。

使用 forName 获取类

forName 有三个参数:

1
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
  • 直接用 forName
1
2
3
4
5
public static void main(String[] args) throws ClassNotFoundException {  
Class.forName("DynamicClassLoader.Demo$Test");
}

// 静态代码块
  • 带参数的 forName
1
2
3
4
5
public static void main(String[] args) throws ClassNotFoundException {  
Class.forName("DynamicClassLoader.Demo$Test", true, ClassLoader.getSystemClassLoader());
}

// 静态代码块
1
2
3
public static void main(String[] args) throws ClassNotFoundException {  
Class.forName("DynamicClassLoader.Demo$Test", false, ClassLoader.getSystemClassLoader());
}

Class.forName(className)Class.forName(className, true, ClassLoader.getSystemClassLoader()) 等价,这两个方法都会调用类中的静态代码块,如果将第二个参数设置为 false,那么就不会调用静态代码块

ClassLoader.loadClass

1
2
3
4
public static void main(String[] args) throws ClassNotFoundException {  
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
Class<?> aClass = systemClassLoader.loadClass("DynamicClassLoader.Demo$Test");
}

这里的 ClassLoader.loadClass 是不会触发任何输出语句的,也就是只加载了类,没有进行初始化。

动态加载字节码

类加载器原理

还是以这段代码为例子,进行 Debug

1
2
3
4
public static void main(String[] args) throws ClassNotFoundException {  
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
Class<?> aClass = systemClassLoader.loadClass("DynamicClassLoader.Demo$Test");
}
JAVA/attachments/Pasted image 20260131175205.png

注意:如果这里无法步入,可以尝试 Alt + Shift + F7 来步入。
原因是 IDEA 默认勾选了 Debugger 的 Stepping 下的 Do not step into the classes,自己全部取消勾选也可以。

跟进后先是走到了抽象类ClassLoader.loadClass()
JAVA/attachments/Pasted image 20260131180255.png

return 多给了一个参数 false,继续跟进回到了 AppClassLoader

JAVA/attachments/Pasted image 20260131180642.png

中间步过一些安全检查代码,走到 return super.loadClass(cn, resolve); , 继续跟进
接下来就是双亲委派的核心逻辑了。

JAVA/attachments/Pasted image 20260131184406.png

一层一层向上找二进制字节码文件,但是到 BootClassLoader 也没有找到后,一层一层的返回给 AppClassLoader
继续往下走,走到 this=AppClassLoaderfindClassOnClassPathOrNull 的时候跟进一下

JAVA/attachments/Pasted image 20260131185631.png

因为向上委派失败,所以要在本地寻找 .class 字节码文件,然后返回一个 Resource 对象

最后调用了 defineClass
JAVA/attachments/Pasted image 20260131190422.png

这边是 URLClassLoader 重写的 defineClass 方法,主要是做一些安全判断之类的
最后还是会调用原来的 defineClass 方法
JAVA/attachments/Pasted image 20260131190600.png

跟进
JAVA/attachments/Pasted image 20260131190724.png

也就是在 BuiltinClassLoader 的父类 SecureClassLoader
JAVA/attachments/Pasted image 20260131190744.png

可以看到这边又调用了一个 defineClass,跟进后发现回到了 ClassLoader 中
JAVA/attachments/Pasted image 20260131190858.png

Class<?> c = defineClass1(this, name, b, off, len, protectionDomain, source); 就是调用最后的 defineClass 方法
也就是获得到 class

最后逐层返回 c,结束流程
JAVA/attachments/Pasted image 20260131191140.png

总结

继承关系:
ClassLoader: 定义了最基本的框架,比如 loadClass 的双亲委派逻辑
-> SecureClassLoader: 做各种安全的支持
-> URLClassLoader: 实现了从 URL 路径(file / jar / url)来读取 .class 文件的内容
-> AppClassLoader: 继承了上述所有搜索和安全能力, 负责加载ClassPath下的业务代码

类加载流程 :
laodClass(): 类加载的入口,负责执行双亲委派逻辑;先查缓存,然后再向上级查询,如果没有,则走到 findClass()
-> findClass(): 委派失败后执行,利用 URLClassPath 在硬盘上寻找对应的 .class 文件,然后读取为字节流
-> defineClass(): 底层 native 方法,负责将 findClass 拿到的字节流转换为 java.lang.Class 对象

利用 URLClassLoader 加载远程 class 文件

URLClassLoader 实际上是我们平时默认使用的 AppClassLoader 的父类,所以,我们解释 URLClassLoader 的工作过程实际上就是在解释默认的 Java 类加载器的工作流程。

  • URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻找.class文件
  • URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件
  • URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类。

calc.java【注意这边不要包含包名,不然会导致在加载 class 文件的时候找不到包名】

1
2
3
4
5
6
7
8
9
10
11
import java.io.IOException;  

public class calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

点小锤子编译一下后就可以拿到 .class 文件, 放在适合的目录下。

File 协议

1
2
3
4
5
6
7
public class Test {  
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///H:\\0x0B_FUXIAN\\Java_class_tmp\\")});
Class<?> c = urlClassLoader.loadClass("calc");
c.newInstance();
}
}

可以看到,在删除 java 文件只保留 class 的情况下,指定 file 文件夹路径就可以实现弹计算器
JAVA/attachments/Pasted image 20260131195637.png

HTTP 协议

也可以通过 http 协议来远程加载 class 类,这里我用 python 起一个临时 http.server,用 PHP 或者其他的都可以。

1
python -m http.server 9999
1
2
3
4
5
public static void main(String[] args) throws Exception {  
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://localhost:9999/")});
Class<?> c = urlClassLoader.loadClass("calc");
c.newInstance();
}
JAVA/attachments/Pasted image 20260131200021.png

HTTP / File + JAR

需要先将之前的 class 文件打包一下

1
jar -cvf calc.jar calc.class
1
2
3
4
5
public static void main(String[] args) throws Exception {  
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:http://localhost:9999/calc.jar!/")});
Class<?> c = urlClassLoader.loadClass("calc");
c.newInstance();
}
JAVA/attachments/Pasted image 20260131200400.png

如果要从远程改为本地 file,直接修改即可,这里不做演示。

利用 ClassLoader #defineClass 直接加载字节码

不管是加载远程 class 文件,还是本地的 class 或 jar 文件,Java 都经历的是下面这三个方法调用。

laodClass(): 类加载的入口,负责执行双亲委派逻辑;先查缓存,然后再向上级查询,如果没有,则走到 findClass()
-> findClass(): 委派失败后执行,利用 URLClassPath 在硬盘上寻找对应的 .class 文件,然后读取为字节流
-> defineClass(): 底层 native 方法,负责将 findClass 拿到的字节流转换为 java.lang.Class 对象

默认的 ClassLoader#defineClass 是一个 native 方法,逻辑在 JVM 的C语言代码中。

JAVA/attachments/Pasted image 20260131200930.png

这里的参数分别为:
name 为类名,b 为字节码数组,off 为偏移量,len 为字节码数组的长度。

但是该类是一个 protected 方法,所以我们要调用的话只能通过反射的方式去调用

1
2
3
4
5
6
7
8
9
10
public class Test {  
public static void main(String[] args) throws Exception {
ClassLoader cl = ClassLoader.getSystemClassLoader();
Method defineMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineMethod.setAccessible(true);
byte[] code = Files.readAllBytes(Path.of("H:\\0x0B_FUXIAN\\Java_class_tmp\\calc.class"));
Class c = (Class) defineMethod.invoke(cl, "calc", code, 0, code.length);
c.newInstance();
}
}

可以看到成功弹窗
JAVA/attachments/Pasted image 20260131210511.png

注意: 如果这里有 java.lang.reflect.InaccessibleObjectException 报错是因为 jdk9+的强封装机制,解决方法和分析URLDNS 链子时候遇到的方法相同,添加 VM options 即可
JAVA/attachments/Pasted image 20260131210453.png

1
--add-opens java.base/java.lang=ALL-UNNAMED

Unsafe 加载字节码

Unsafe中也存在 defineClass() 方法,本质上也是 defineClass 加载字节码的方式
(高版本已被移除)

后续遇到再补

TemplatesImpl 加载字节码

后续遇到再补

利用 BCEL ClassLoader 加载字节码

后续遇到再补

  • Title: Java_Sec 类的动态加载
  • Author: xekOnerR
  • Created at : 2026-01-29 23:48:54
  • Updated at : 2026-01-31 21:22:22
  • Link: https://xekoner.xyz/2026/01/29/Java-sec-类的动态加载/
  • License: This work is licensed under CC BY-NC-SA 4.0.