Java类加载原理中为何要设计双亲委派机制
首先,给大家演示两个示例代码,我们自定义一个与Java核心类库中java.lang.String类名相同的代码:
package java.lang;
/**
* 自定义java.lang.String类
*
* @author 编程老司机
* @date 2023-06-16
*/
public class String {
static {
System.out.println("加载自定义的String类");
}
public String(){
System.out.println("初始化自定义的String对象");
}
public static void main(String[] args) {
String s = new String();
}
}
运行结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
从运行结果我们看到,程序运行报错了。错误原因是什么呢?下面我们来分析一下:
看过我前面一篇文章《JVM源码分析:深入剖析JavaMain方法中的LoadMainClass的实现》的读者可能知道,main方法主类是由应用程序类加载器加载的。应用程序类加载默认是实现了双亲委派机制的,所以应用程序类加载器会委托父加载器引导类加载器进行java.lang.String类的加载,引导类加载器是只认识Java核心类库中的java.lang.String类,该类是不存在main方法的。所以程序就报main方法不存在的错误。
接着,我们来看下一个示例代码:
/**
* 自定义类加载器,演示沙箱安全机制
*
* @author 编程老司机
* @date 2023-06-16
*/
public class CustomClassLoaderDemo {
/**
* 自定义类加载器
*/
static class CustomClassLoader extends ClassLoader {
private String classDir;
public CustomClassLoader(String classDir) {
this.classDir = classDir;
}
/**
* 读取类文件
* @param className 类名
* @return
* @throws IOException
*/
private byte[] loadClassFile(String className) throws IOException {
className = className.replaceAll("\\.", "/");
String classFilePath = classDir + "/" + className + ".class";
FileInputStream fis = new FileInputStream(classFilePath);
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
/**
* 重写loadClass方法,实现自己的加载逻辑,去掉其中实现双亲委派机制的几行代码:
* if (parent != null) {
* c = parent.loadClass(name, false);
* } else {
* c = findBootstrapClassOrNull(name);
* }
* @param name 类名
* @param resolve
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
try {
byte[] data = loadClassFile(className);
// 将字节数组数据转为Class对象
return defineClass(className, data, 0, data.length);
} catch (Exception e) {
throw new ClassNotFoundException("类加载失败", e);
}
}
public static void main(String args[]) throws Exception {
// 初始化自定义类加载器,会调用默认构造函数,
// 将应用程序类加载器设置为自定义加载器的父加载器
CustomClassLoader classLoader = new CustomClassLoader("D:\\");
// 我们已经事先编译了自定义的java.lang.String类,
// 并将类文件放在D盘根目录下
Class clazz = classLoader.loadClass("java.lang.String");
// 尝试创建一个自定义的java.lang.String类对象
Object obj = clazz.newInstance();
}
}
}
上述代码运行结果:
Exception in thread "main" java.lang.ClassNotFoundException: 类加载失败
at CustomClassLoaderDemo$CustomClassLoader.findClass(CustomClassLoaderDemo.java:85)
at CustomClassLoaderDemo$CustomClassLoader.loadClass(CustomClassLoaderDemo.java:63)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at CustomClassLoaderDemo$CustomClassLoader.main(CustomClassLoaderDemo.java:95)
Caused by: java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at CustomClassLoaderDemo$CustomClassLoader.findClass(CustomClassLoaderDemo.java:83)
... 3 more
从运行结果我们看到,程序运行报错了。错误信息是“ java.lang.SecurityException: Prohibited package name: java.lang”,意思我们触发不满足安全机制的要求,定义了禁止使用的包名java.lang,进入报错的方法,看该方法的源码,我们得知,凡是以包名java.开头的都是不允许使用的:
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// 禁止使用"java."开头的类名
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
通过以上两个示例说明,为何Java要设计双亲委派机制:
1. 避免类的重复加载:当父加载器已经加载了指定类时,子加载器就不会再加载一次,保证被加载类的唯一性;
2. 沙箱安全机制:防止Java核心API库被随意篡改。通俗点说,当Java核心类库中的类(比如:java.lang.String)的类名与用户自定义类的类名相同有冲突时,Java不会加载用户自定义的类。