JVM加载机制

Mathieu 于 2025-07-29 发布

JVM加载机制

类加载机制

jvm虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

image.png

类加载器

image.png

默认使用的是应用程序类加载器

双亲委派模型

image.png

当一个类加载器需要加载一个类时,它会首先把加载请求委派给自己的父类加载器去尝试加载。这个过程会一直向上递归,直到达到最顶层的启动类加载器。

只有当父类加载器(以及它所有的祖先类加载器)都无法找到并加载这个类时,子类加载器才会尝试自己去加载。

如果任何一个父类加载器成功加载了该类,那么就会直接返回那个已经加载好的类。

破坏双亲委派

平时加载类,要“听父类加载器的话”;但遇到特殊情况,为了实现某些功能(比如父类要用子类提供的东西,或者要实现动态插拔),就“不再完全听父类加载器的话了”,而是由自己(或指定的方式)来加载。

代码演示:

  1. 新建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!");
    }
}

image.png

  1. 自定义类加载器

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;
        }
    }
}

原理说明:

  1. 绕过委派机制
    • 正常情况下,当 CustomClassLoader 被请求加载 MyClass 时,它应该先调用 super.loadClass("MyClass", resolve),将这个请求传递给其父类(通常是应用程序类加载器)。
    • 但在我们的代码中,当 name.equals("MyClass") 为真时,CustomClassLoader 直接调用了 loadClassData(name)defineClass(...) 来加载并定义 MyClass,而没有先调用 super.loadClass()
    • 这就意味着,它没有遵循“先委派给父类”的原则,而是自己抢先加载了
  2. 打破层级结构
    • 应用程序类加载器(AppClassLoader)是 CustomClassLoader 的默认父类。AppClassLoader 负责加载 classpath 中的类。
    • 如果 MyClass.class 文件不在 TestClassLoader 运行时的 classpath 中(我们特意将其放在了 classes 目录下,并通过 CustomClassLoaderclassPath 指定),那么 AppClassLoader 是无法找到 MyClass 的。
    • 正常双亲委派下,CustomClassLoader 会把请求委派给 AppClassLoaderAppClassLoader 找不到,最后才轮到 CustomClassLoader 自己加载。
    • 但现在,CustomClassLoader 在收到加载 MyClass 的请求时,直接跳过了 AppClassLoader 的查找过程,自己去 classPath 指定的目录加载了。
  3. 创建测试类来使用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的,其他加密只需要写好对应的解密代码即可