Java_Sec 类的动态加载
类加载器及双亲委派
什么是类加载器
类加载器, 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 | static class Test { |
实例化对象
1 | public class Demo { |
首先调用的是静态代码块 static() ,其次是构造代码块 {} , 最后是构造方法 Test()
当执行 new Test(); 操作之前,一个类在使用前 JVM 必须先加载到内存中。
所以类加载优先于对象实例化,类被加载,静态代码块 static{} 会被立即触发,只会被调用一次。
类加载过后,执行 new Test() 创建一个对象。构造代码块 {} 被执行。
最后是构造函数 Test()
调用静态方法
1 | public static void main(String[] args) { |
不实例化对象直接调用静态方法,会先调用类中的静态代码块,然后调用静态方法。
对类中的静态变量进行赋值
1 | public static void main(String[] args) { |
在对静态成员变量赋值前,会调用静态代码块
使用 class 获取类
1 | public static void main(String[] args) { |
利用 class 关键字获取类,自会进行加载,但不会进行初始化,也就是什么也不会输出。
使用 forName 获取类
forName 有三个参数:
1 | public static Class<?> forName(String name, boolean initialize, ClassLoader loader) |
- 直接用 forName
1 | public static void main(String[] args) throws ClassNotFoundException { |
- 带参数的 forName
1 | public static void main(String[] args) throws ClassNotFoundException { |
1 | public static void main(String[] args) throws ClassNotFoundException { |
Class.forName(className) 和 Class.forName(className, true, ClassLoader.getSystemClassLoader()) 等价,这两个方法都会调用类中的静态代码块,如果将第二个参数设置为 false,那么就不会调用静态代码块
ClassLoader.loadClass
1 | public static void main(String[] args) throws ClassNotFoundException { |
这里的 ClassLoader.loadClass 是不会触发任何输出语句的,也就是只加载了类,没有进行初始化。
动态加载字节码
类加载器原理
还是以这段代码为例子,进行 Debug
1 | public static void main(String[] args) throws ClassNotFoundException { |
注意:如果这里无法步入,可以尝试 Alt + Shift + F7 来步入。
原因是 IDEA 默认勾选了 Debugger 的 Stepping 下的 Do not step into the classes,自己全部取消勾选也可以。
跟进后先是走到了抽象类ClassLoader.loadClass()
return 多给了一个参数 false,继续跟进回到了 AppClassLoader
中间步过一些安全检查代码,走到 return super.loadClass(cn, resolve); , 继续跟进
接下来就是双亲委派的核心逻辑了。
一层一层向上找二进制字节码文件,但是到 BootClassLoader 也没有找到后,一层一层的返回给 AppClassLoader
继续往下走,走到 this=AppClassLoader 的 findClassOnClassPathOrNull 的时候跟进一下
因为向上委派失败,所以要在本地寻找 .class 字节码文件,然后返回一个 Resource 对象
最后调用了 defineClass
这边是 URLClassLoader 重写的 defineClass 方法,主要是做一些安全判断之类的
最后还是会调用原来的 defineClass 方法
跟进
也就是在 BuiltinClassLoader 的父类 SecureClassLoader
可以看到这边又调用了一个 defineClass,跟进后发现回到了 ClassLoader 中
Class<?> c = defineClass1(this, name, b, off, len, protectionDomain, source); 就是调用最后的 defineClass 方法
也就是获得到 class
最后逐层返回 c,结束流程
总结
继承关系:
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 | import java.io.IOException; |
点小锤子编译一下后就可以拿到 .class 文件, 放在适合的目录下。
File 协议
1 | public class Test { |
可以看到,在删除 java 文件只保留 class 的情况下,指定 file 文件夹路径就可以实现弹计算器
HTTP 协议
也可以通过 http 协议来远程加载 class 类,这里我用 python 起一个临时 http.server,用 PHP 或者其他的都可以。
1 | python -m http.server 9999 |
1 | public static void main(String[] args) throws Exception { |
HTTP / File + JAR
需要先将之前的 class 文件打包一下
1 | jar -cvf calc.jar calc.class |
1 | public static void main(String[] args) throws Exception { |
如果要从远程改为本地 file,直接修改即可,这里不做演示。
利用 ClassLoader #defineClass 直接加载字节码
不管是加载远程 class 文件,还是本地的 class 或 jar 文件,Java 都经历的是下面这三个方法调用。
laodClass(): 类加载的入口,负责执行双亲委派逻辑;先查缓存,然后再向上级查询,如果没有,则走到 findClass()
-> findClass(): 委派失败后执行,利用 URLClassPath 在硬盘上寻找对应的 .class 文件,然后读取为字节流
-> defineClass(): 底层 native 方法,负责将 findClass 拿到的字节流转换为 java.lang.Class 对象
默认的 ClassLoader#defineClass 是一个 native 方法,逻辑在 JVM 的C语言代码中。
这里的参数分别为:name 为类名,b 为字节码数组,off 为偏移量,len 为字节码数组的长度。
但是该类是一个 protected 方法,所以我们要调用的话只能通过反射的方式去调用
1 | public class Test { |
可以看到成功弹窗
注意: 如果这里有 java.lang.reflect.InaccessibleObjectException 报错是因为 jdk9+的强封装机制,解决方法和分析URLDNS 链子时候遇到的方法相同,添加 VM options 即可
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.