JVM加载机制
类加载机制
jvm
虚拟机把描述类的数据从Class
文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java
类型,这就是虚拟机的类加载机制。
类加载器
默认使用的是应用程序类加载器
双亲委派模型
当一个类加载器需要加载一个类时,它会首先把加载请求委派给自己的父类加载器去尝试加载。这个过程会一直向上递归,直到达到最顶层的启动类加载器。
只有当父类加载器(以及它所有的祖先类加载器)都无法找到并加载这个类时,子类加载器才会尝试自己去加载。
如果任何一个父类加载器成功加载了该类,那么就会直接返回那个已经加载好的类。
破坏双亲委派
平时加载类,要“听父类加载器的话”;但遇到特殊情况,为了实现某些功能(比如父类要用子类提供的东西,或者要实现动态插拔),就“不再完全听父类加载器的话了”,而是由自己(或指定的方式)来加载。
代码演示:
- 新建
MyClass
文件并编译为Class
// MyClass.java
public class MyClass {
public MyClass() {
System.out.println("MyClass 实例被创建了!");
System.out.println("MyClass 的类加载器是: " + MyClass.class.getClassLoader());
}
public void sayHello() {
System.out.println("你好,我是 MyClass!");
}
}
- 自定义类加载器
MyClassLoader
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
// 这里仍然可以设置父类为 null,表示其父加载器是 BootstrapClassLoader。
// 关键在于 loadClass 方法的实现。
super(null); // 或者 super(ClassLoader.getSystemClassLoader().getParent());
this.classPath = classPath;
}
// 修正后的 loadClass 方法:既能“破坏”又能正常加载核心类
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1. 首先检查这个类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// 2. 对于核心 Java 类(如 java.lang.Object, java.lang.String 等),
// 必须通过父类加载器(最终是 Bootstrap ClassLoader)来加载。
// 这里我们通过判断包名来决定是否委派给父类。
// 如果类名以 "java." 或 "javax." 开头,就直接委派给父类。
if (name.startsWith("java.") || name.startsWith("javax.")) {
// 委派给父类加载器,让它去加载这些核心类
return super.loadClass(name, resolve);
}
// 3. 尝试加载 MyClass,直接由当前 MyClassLoader 加载(这里是“破坏”点)
if (name.equals("MyClass")) {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException("Class " + name + " not found in custom path.");
}
// defineClass 方法将字节数组转换为 Class 对象
c = defineClass(name, classData, 0, classData.length);
if (resolve) {
resolveClass(c);
}
return c;
}
// 4. 对于其他非核心库的类,如果不是 MyClass,则仍然委派给父类加载器。
// 这确保了 MyClass 之外的其他应用程序类也能被正常加载(如果它们在父类加载器的路径中)。
return super.loadClass(name, resolve);
}
// 实际从文件系统加载类字节码的方法
private byte[] loadClassData(String name) {
// 将类名转换为文件路径,例如 "MyClass" -> "MyClass.class"
// 或者 "com.example.MyClass" -> "com/example/MyClass.class"
String fileName = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
File file = new File(fileName);
if (!file.exists()) {
return null; // 文件不存在
}
try (InputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
} catch (IOException e) {
System.err.println("Error loading class data for " + name + ": " + e.getMessage());
return null;
}
}
}
原理说明:
- 绕过委派机制:
- 正常情况下,当
CustomClassLoader
被请求加载MyClass
时,它应该先调用super.loadClass("MyClass", resolve)
,将这个请求传递给其父类(通常是应用程序类加载器)。 - 但在我们的代码中,当
name.equals("MyClass")
为真时,CustomClassLoader
直接调用了loadClassData(name)
和defineClass(...)
来加载并定义MyClass
,而没有先调用super.loadClass()
。 - 这就意味着,它没有遵循“先委派给父类”的原则,而是自己抢先加载了。
- 正常情况下,当
- 打破层级结构:
- 应用程序类加载器(
AppClassLoader
)是CustomClassLoader
的默认父类。AppClassLoader
负责加载classpath
中的类。 - 如果
MyClass.class
文件不在TestClassLoader
运行时的classpath
中(我们特意将其放在了classes
目录下,并通过CustomClassLoader
的classPath
指定),那么AppClassLoader
是无法找到MyClass
的。 - 正常双亲委派下,
CustomClassLoader
会把请求委派给AppClassLoader
,AppClassLoader
找不到,最后才轮到CustomClassLoader
自己加载。 - 但现在,
CustomClassLoader
在收到加载MyClass
的请求时,直接跳过了AppClassLoader
的查找过程,自己去classPath
指定的目录加载了。
- 应用程序类加载器(
- 创建测试类来使用
MyClassLoader
加载MyClass
import java.io.File; // 导入 File 类
public class demo01 {
public static void main(String[] args) throws Exception {
// 明确指定 MyClass.class 所在的目录
String classPath = "E:" + File.separator + "Web_Pentest" + File.separator + "javasec" + File.separator + "demo" + File.separator + "JavaClassLoader" + File.separator + "src";
System.out.println("MyClass.class 查找路径: " + classPath);
// 创建 MyClassLoader 实例
MyClassLoader customLoader = new MyClassLoader(classPath);
System.out.println("尝试使用 MyClassLoader 加载 MyClass...");
// 使用自定义类加载器加载 MyClass
Class<?> myClass = customLoader.loadClass("MyClass");
// 创建 MyClass 的实例
Object obj = myClass.getDeclaredConstructor().newInstance();
// 调用 MyClass 的方法
myClass.getMethod("sayHello").invoke(obj);
System.out.println("\n--- 对比 ---");
// 获取当前 demo01 类的类加载器(通常是应用程序类加载器)
System.out.println("demo01 的类加载器是: " + demo01.class.getClassLoader());
// 获取 String 类的类加载器(Bootstrap ClassLoader,返回 null)
System.out.println("String 类的类加载器是: " + String.class.getClassLoader());
}
}
自定义加载器
MyClass2
:
// CustomLoadedClass.java
// 这个文件应该被编译,但不要放到程序的 classpath 中,
// 而是放到一个我们自定义类加载器能找到的目录。
public class MyClass2 {
public MyClass2() {
System.out.println("MyClass2 实例被创建了!");
System.out.println("MyClass2 的类加载器是: " + this.getClass().getClassLoader());
}
public void greet() {
System.out.println("你好,我是由自定义加载器加载的!");
}
}
MyClassLoader2
:
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class MyClassLoader2 extends ClassLoader {
private String path;
public MyClassLoader2(String path) {
super(null);//不写的话默认编译也会带
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = path + name.replace('.', File.separatorChar) + ".class";
byte[] bytes;
try {
bytes = Files.readAllBytes(Paths.get(fileName));
} catch (IOException e) {
throw new RuntimeException(e);
}
Class<?> aClass = defineClass(name, bytes, 0, bytes.length);
return aClass;
}
}
demo2
:
public class demo02 {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
MyClassLoader2 cl = new MyClassLoader2("E:\\Web_Pentest\\javasec\\demo\\JavaClassLoader\\src\\");
Class<?> myClass2 = cl.loadClass("MyClass2");;
Object o = myClass2.newInstance();
}
}
加载Base64编码的Class
将MyClass2.class
进行base64
编码
powershell
命令
[System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes("MyClass2.class")) | Out-File -Encoding ASCII MyClass2.base64
MyClassLoaderBase64
:
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets; // 新增:用于字符串编码
import java.util.Base64; // 新增:用于 Base64 解码
public class MyClassLoaderBase64 extends ClassLoader {
private String path;
public MyClassLoaderBase64(String path) {
super(null); // 保持不变,显式设置父加载器为 Bootstrap ClassLoader
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 关键改动:现在查找的是 .base64 后缀的文件
String fileName = path + name.replace('.', File.separatorChar) + ".base64"; // 修改:从 .class 改为 .base64
byte[] decodedBytes; // 存储 Base64 解码后的原始类字节
try {
// 1. 从文件读取 Base64 编码的字符串
// Files.readAllBytes 返回的是文件内容的字节,这里假设 Base64 字符串是以 UTF-8 编码存储的
byte[] base64EncodedBytes = Files.readAllBytes(Paths.get(fileName));
String base64String = new String(base64EncodedBytes, StandardCharsets.UTF_8);
// 2. 对 Base64 字符串进行解码
decodedBytes = Base64.getDecoder().decode(base64String);
} catch (IOException e) {
// 文件读取错误,或者文件不存在
System.err.println("读取 Base64 编码的类文件失败: " + fileName + " - " + e.getMessage());
throw new ClassNotFoundException("无法从指定路径加载类数据: " + name, e);
} catch (IllegalArgumentException e) {
// Base64 解码失败,说明文件内容不是有效的 Base64 编码
System.err.println("Base64 解码失败 (文件内容不是有效的 Base64 编码): " + fileName + " - " + e.getMessage());
throw new ClassNotFoundException("无效的 Base64 类数据: " + name, e);
}
// 3. 使用解码后的字节数据定义类
Class<?> aClass = defineClass(name, decodedBytes, 0, decodedBytes.length);
return aClass;
}
}
demo03
:
public class demo03 {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
MyClassLoaderBase64 cl = new MyClassLoaderBase64("E:\\Web_Pentest\\javasec\\demo\\JavaClassLoader\\src\\");
Class<?> myClass2 = cl.loadClass("MyClass2");;
Object o = myClass2.newInstance();
}
}
也是完全ok的,其他加密只需要写好对应的解密代码即可