继上一篇博文深入探讨了类加载器的类型、初始化过程和如何自定义类加载器之后,本篇博文将聚焦于Java类加载机制的核心原则——双亲委派模型。我们将详细介绍双亲委派机制的实现方式、其在Java类加载中的作用,以及如何打破这一机制。
类加载器在加载类的过程中,遵循以下加载机制:
双亲委派的核心是向上递归检查,向下传递加载,委派过程如下:
上述过程具体在java.lang.ClassLoader
中的loadClass方法中实现,相关核心代码如下:
public abstract class ClassLoader {
// 持有父加载器的引用
private final ClassLoader parent;
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
// 如果没有被加载,就委托给父类加载或者委派给启动类加载器加载`
try {
if (parent != null) {
// 如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
// 如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
// 调用URLClassLoader的findClass方法在自身加载器的类路径里查找并加载该类
c = findClass(name);
}
}
if (resolve) {
// 这⼀段就是加载过程中的链接Linking部分,分为验证、准备,解析三个部分
// 运行时加载类,默认是无法进行链接步骤的。
resolveClass(c);
}
return c;
}
private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) {
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// 不允许加载核心类
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;
}
}
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("**************My String Class**************");
}
}
运行结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
正如上面我们提到了java.lang.ClassLoader
中的loadClass方法实现了双亲委派机制。因此要想打破双亲委派,可以重写loadClass方法实现自己的加载逻辑,不委派给双亲加载。
简单示例如下:
public class MyClassLoaderTest {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
/**
* 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 把双亲委派机制反过来,先到子类加载器中加载,加载不到再去父类加载器中加载。
Class<?> c = null;
synchronized (getClassLoadingLock(name)) {
c = findLoadedClass(name);
if(c == null){
c = findClass(name);
if(c == null){
c = super.loadClass(name,resolve);
}
}
}
return c;
}
}
public static void main(String args[]) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/test");
//尝试用自己改写类加载机制去加载自己写的com.User.class
Class clazz = classLoader.loadClass("com.User");
Object obj = clazz.newInstance();
Method method= clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
打破双亲委派为开发者提供了定制类加载行为的能力,实现版本隔离、热加载等功能,以满足特定应用场景的需求。在Tomcat中,打破双亲委派机制的效果尤为显著,它为每个Web应用程序分配了独立的类加载器,实现了以下关键功能:
更多的细节以及关于Tomcat中类加载器的应用将在后续的Tomcat系列博文中进行详细讲解。这里仅简要介绍了打破双亲委派的作用和在Tomcat中的应用场景,以展示其在Java类加载器系统中的重要性。
双亲委派机制是Java类加载机制的核心原则,通过层次化的类加载器结构,确保了类的安全性和唯一性。然而在特定的应用场景下,如Tomcat中的Web应用程序隔离,打破双亲委派模型是有意义的。但同时,打破双亲委派机制也带来了额外的复杂性和潜在风险,因此开发者应权衡利弊,确保在提升灵活性的同时,维护应用的稳定性和安全性。