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包路径:
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参数,它决定了类加载的作用域。有三种主要用法:
-
使用BootstrapClassLoader加载器
Class.forName(name, true, null); // null表示BootstrapClassLoader -
使用ExtClassLoader加载器
ClassLoader extClassLoader = ClassLoader.getSystemClassLoader().getParent(); Class.forName(name, true, extClassLoader); -
使用AppClassLoader加载器
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); Class.forName(name, true, appClassLoader);
作用域分析
forName的作用域取决于指定的类加载器及其双亲委派机制:
- BootstrapClassLoader:只能加载
lib目录下的核心类 - ExtClassLoader:可以加载
lib/ext目录下的扩展类 - 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); // 成功
总结
-
Class.forName的作用域取决于使用的类加载器:null(BootstrapClassLoader):只能加载核心类ExtClassLoader:可以加载扩展类和核心类AppClassLoader:可以加载所有类(核心、扩展和应用程序类)
-
双亲委派机制会影响类的加载行为:
- 子加载器会先委托父加载器尝试加载
- 只有父加载器无法加载时,子加载器才会自己尝试
-
在实际应用中:
- 明确目标类由哪个类加载器负责
- 选择合适的
forName调用方式 - 考虑双亲委派机制的影响
参考
通过深入理解Class.forName的作用域机制,我们能够更准确地控制Java程序的类加载行为,在安全研究和开发中避免因类加载问题导致的错误。