继上一篇博文深入探讨了类加载器的类型、初始化过程和如何自定义类加载器之后,本篇博文将聚焦于Java类加载机制的核心原则——双亲委派模型。我们将详细介绍双亲委派机制的实现方式、其在Java类加载中的作用,以及如何打破这一机制。
1 类加载机制
类加载器在加载类的过程中,遵循以下加载机制:
- 懒加载机制:jar包或war包里的类不是一次性全部加载的,是使用到时才加载
- 双亲委派机制:向上依次查找是否已加载过该类,再向下依次让各父加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制:各类加载器加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从自己的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
- 沙箱安全机制:不允许应用程序加载JDK内部的系统类,比如java.开头的类
- 全盘委托机制:当一个ClassLoader负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,但也会遵循双亲委派加载的机制,除非显式使用另外一个类加载器来载入
2 双亲委派机制
2.1 双亲委派过程
双亲委派的核心是向上递归检查,向下传递加载,委派过程如下:
- 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
- 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false)).或者是调用bootstrap类加载器来加载。
- 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法进行类加载。在findClass调用过程中,会调用preDefineClass方法,限制不允许加载自定义的java.包下的类
- 如果没有找到,会抛出ClassNotFoundException异常
上述过程具体在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;
}
}
2.2 双亲委派作用
- 避免类的重复加载。当父ClassLoader已经加载该类时,子ClassLoader不会重复加载,保证被加载类的唯一性
- 保证核心.class不能被篡改。自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改。示例如下:
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
3 打破双亲委派
3.1 打破双亲委派具体实现
正如上面我们提到了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());
}
}
3.2 打破双亲委派的应用
打破双亲委派为开发者提供了定制类加载行为的能力,实现版本隔离、热加载等功能,以满足特定应用场景的需求。在Tomcat中,打破双亲委派机制的效果尤为显著,它为每个Web应用程序分配了独立的类加载器,实现了以下关键功能:
- Web应用程序隔离:每个Web应用程序都由独立的类加载器加载,保证了应用程序之间的互不影响。
- 热部署和热替换:开发者可以在应用程序运行时更新类文件,Tomcat的类加载器会自动加载最新的类,实现热部署。
- 安全性增强:通过限制Web应用程序类加载器的权限,Tomcat增强了系统的安全性,防止了潜在的安全风险。
更多的细节以及关于Tomcat中类加载器的应用将在后续的Tomcat系列博文中进行详细讲解。这里仅简要介绍了打破双亲委派的作用和在Tomcat中的应用场景,以展示其在Java类加载器系统中的重要性。
4 总结
双亲委派机制是Java类加载机制的核心原则,通过层次化的类加载器结构,确保了类的安全性和唯一性。然而在特定的应用场景下,如Tomcat中的Web应用程序隔离,打破双亲委派模型是有意义的。但同时,打破双亲委派机制也带来了额外的复杂性和潜在风险,因此开发者应权衡利弊,确保在提升灵活性的同时,维护应用的稳定性和安全性。