类加载之forName作用域
字数 1476 2025-08-25 22:58:35

Java类加载机制之Class.forName作用域详解

前言

在Java安全研究和开发中,我们经常需要使用Class.forName方法来动态加载类。然而,不同的调用方式会导致不同的类加载行为,理解forName方法的作用域对于正确使用它至关重要。本文将深入剖析Class.forName的作用域机制。

类加载器基础

Java中有三种主要的类加载器:

  1. BootstrapClassLoader(启动类加载器/根加载器)

    • 负责加载<JAVA_HOME>\lib目录中的核心类库
    • 或被-Xbootclasspath参数指定的路径中的类库
    • 由C/C++实现,无法被Java程序直接引用
  2. ExtClassLoader(扩展类加载器)

    • 位于sun.misc.Launcher$ExtClassLoader
    • 负责加载<JAVA_HOME>/lib/ext目录中的类库
    • java.ext.dir系统变量指定路径中的类库
  3. AppClassLoader(应用程序类加载器/系统类加载器)

    • 负责加载系统类路径(CLASSPATH)中指定的类库

可以通过以下代码查看各类加载器加载的jar包路径:

import java.net.URL;
import java.net.URLClassLoader;

public class printPath {
    public static void main(String[] args) {
        URL[] urls;
        System.out.println("BootstrapClassLoader Load Path:");
        urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL url : urls) {
            System.out.println(url);
        }

        System.out.println("ExtClassLoader Load Path:");
        urls = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
        for (URL url : urls) {
            System.out.println(url);
        }

        System.out.println("AppClassLoader Load Path:");
        urls = ((URLClassLoader) ClassLoader.getSystemClassLoader()).getURLs();
        for (URL url : urls) {
            System.out.println(url);
        }
    }
}

也可以通过双亲委派机制获取这三种类加载器:

public class getClassLoader {
    public static void main(String[] args) {
        ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader();
        ClassLoader extClassLoader = Thread.currentThread().getContextClassLoader().getParent();
        ClassLoader bootStrapClassLoader = Thread.currentThread().getContextClassLoader().getParent().getParent();

        System.out.println(appClassLoader);
        System.out.println(extClassLoader);
        System.out.println(bootStrapClassLoader);
    }
}

Class.forName方法详解

Class.forName有两种重载形式:

第一种重载

public static Class<?> forName(String className)

实际上是第二种重载的简写形式,其内部实现为:

Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);

关键点在于ClassLoader.getClassLoader(caller),它获取调用类的类加载器,然后使用该加载器加载目标类。

第二种重载

public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

重点关注loader参数,它决定了类加载的作用域。有三种主要用法:

  1. 使用BootstrapClassLoader加载器

    Class.forName(name, true, null); // null表示BootstrapClassLoader
    
  2. 使用ExtClassLoader加载器

    ClassLoader extClassLoader = ClassLoader.getSystemClassLoader().getParent();
    Class.forName(name, true, extClassLoader);
    
  3. 使用AppClassLoader加载器

    ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
    Class.forName(name, true, appClassLoader);
    

作用域分析

forName的作用域取决于指定的类加载器及其双亲委派机制:

  1. BootstrapClassLoader:只能加载lib目录下的核心类
  2. ExtClassLoader:可以加载lib/ext目录下的扩展类
  3. AppClassLoader:可以加载CLASSPATH中的所有类

双亲委派机制的影响:

  • 子加载器加载类时会先委托父加载器尝试加载
  • 只有父加载器无法加载时,子加载器才会自己尝试加载

测试示例:

public class forNameTest {
    public static void main(String[] args) {
        ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader();
        ClassLoader extClassLoader = Thread.currentThread().getContextClassLoader().getParent();
        
        // 测试rt.jar中的类(由BootstrapClassLoader加载)
        String name1 = "apple.applescript.AppleScriptEngine";
        // 测试ext目录下的类(由ExtClassLoader加载)
        String name2 = "sun.security.ec.CurveDB"; 
        // 测试CLASSPATH中的类(由AppClassLoader加载)
        String name3 = "com.intellij.rt.ant.execution.AntMain2";

        testLoading(name1, appClassLoader, extClassLoader);
        testLoading(name2, appClassLoader, extClassLoader);
        testLoading(name3, appClassLoader, extClassLoader);
    }
    
    private static void testLoading(String className, 
                                   ClassLoader appLoader, 
                                   ClassLoader extLoader) {
        try {
            Class.forName(className, false, null); // Bootstrap
            System.out.println(className + " can be loaded by BootstrapClassLoader");
        } catch (Exception e) {
            System.out.println(className + " cannot be loaded by BootstrapClassLoader");
        }
        
        try {
            Class.forName(className, false, extLoader);
            System.out.println(className + " can be loaded by ExtClassLoader");
        } catch (Exception e) {
            System.out.println(className + " cannot be loaded by ExtClassLoader");
        }
        
        try {
            Class.forName(className, false, appLoader);
            System.out.println(className + " can be loaded by AppClassLoader");
        } catch (Exception e) {
            System.out.println(className + " cannot be loaded by AppClassLoader");
        }
    }
}

实际案例

在0ctf2022 onlyjdk题目中,遇到了一个典型问题:

// 尝试加载jdk.nashorn.internal.codegen.DumpBytecode
// 这个类位于nashorn.jar中,由ExtClassLoader加载
Class.forName("jdk.nashorn.internal.codegen.DumpBytecode", false, null); // 会失败

失败原因:使用BootstrapClassLoader(null)无法加载ExtClassLoader负责的nashorn.jar中的类。

解决方案是使用能够访问ExtClassLoader的类加载器:

// 使用当前线程的上下文类加载器(AppClassLoader)
ClassLoader cl = Thread.currentThread().getContextClassLoader();
if (cl == null) {
    cl = ClassLoader.getSystemClassLoader();
}
Class.forName("jdk.nashorn.internal.codegen.DumpBytecode", true, cl); // 成功

总结

  1. Class.forName的作用域取决于使用的类加载器:

    • null(BootstrapClassLoader):只能加载核心类
    • ExtClassLoader:可以加载扩展类和核心类
    • AppClassLoader:可以加载所有类(核心、扩展和应用程序类)
  2. 双亲委派机制会影响类的加载行为:

    • 子加载器会先委托父加载器尝试加载
    • 只有父加载器无法加载时,子加载器才会自己尝试
  3. 在实际应用中:

    • 明确目标类由哪个类加载器负责
    • 选择合适的forName调用方式
    • 考虑双亲委派机制的影响

参考

  1. Java类加载机制详解
  2. Java安全研究中的类加载问题

通过深入理解Class.forName的作用域机制,我们能够更准确地控制Java程序的类加载行为,在安全研究和开发中避免因类加载问题导致的错误。

Java类加载机制之Class.forName作用域详解 前言 在Java安全研究和开发中,我们经常需要使用 Class.forName 方法来动态加载类。然而,不同的调用方式会导致不同的类加载行为,理解 forName 方法的作用域对于正确使用它至关重要。本文将深入剖析 Class.forName 的作用域机制。 类加载器基础 Java中有三种主要的类加载器: BootstrapClassLoader(启动类加载器/根加载器) 负责加载 <JAVA_HOME>\lib 目录中的核心类库 或被 -Xbootclasspath 参数指定的路径中的类库 由C/C++实现,无法被Java程序直接引用 ExtClassLoader(扩展类加载器) 位于 sun.misc.Launcher$ExtClassLoader 负责加载 <JAVA_HOME>/lib/ext 目录中的类库 或 java.ext.dir 系统变量指定路径中的类库 AppClassLoader(应用程序类加载器/系统类加载器) 负责加载系统类路径(CLASSPATH)中指定的类库 可以通过以下代码查看各类加载器加载的jar包路径: 也可以通过双亲委派机制获取这三种类加载器: Class.forName方法详解 Class.forName 有两种重载形式: 第一种重载 实际上是第二种重载的简写形式,其内部实现为: 关键点在于 ClassLoader.getClassLoader(caller) ,它获取调用类的类加载器,然后使用该加载器加载目标类。 第二种重载 重点关注 loader 参数,它决定了类加载的作用域。有三种主要用法: 使用BootstrapClassLoader加载器 使用ExtClassLoader加载器 使用AppClassLoader加载器 作用域分析 forName 的作用域取决于指定的类加载器及其双亲委派机制: BootstrapClassLoader :只能加载 lib 目录下的核心类 ExtClassLoader :可以加载 lib/ext 目录下的扩展类 AppClassLoader :可以加载CLASSPATH中的所有类 双亲委派机制的影响: 子加载器加载类时会先委托父加载器尝试加载 只有父加载器无法加载时,子加载器才会自己尝试加载 测试示例: 实际案例 在0ctf2022 onlyjdk题目中,遇到了一个典型问题: 失败原因:使用BootstrapClassLoader(null)无法加载ExtClassLoader负责的nashorn.jar中的类。 解决方案是使用能够访问ExtClassLoader的类加载器: 总结 Class.forName 的作用域取决于使用的类加载器: null (BootstrapClassLoader):只能加载核心类 ExtClassLoader :可以加载扩展类和核心类 AppClassLoader :可以加载所有类(核心、扩展和应用程序类) 双亲委派机制会影响类的加载行为: 子加载器会先委托父加载器尝试加载 只有父加载器无法加载时,子加载器才会自己尝试 在实际应用中: 明确目标类由哪个类加载器负责 选择合适的 forName 调用方式 考虑双亲委派机制的影响 参考 Java类加载机制详解 Java安全研究中的类加载问题 通过深入理解 Class.forName 的作用域机制,我们能够更准确地控制Java程序的类加载行为,在安全研究和开发中避免因类加载问题导致的错误。