Java类加载器与双亲委派模型
文章目录
- 什么是类加载器
- Java类加载器体系
- 启动类加载器
- 扩展类加载器
- 应用类加载器
- 双亲委派模型
- 双亲委派的源码实现
- 双亲委派的好处
- 如何打破双亲委派模型
- 直接重写loadClass方法(自定义)
什么是类加载器
简单来说,类加载器就是负责把.class文件加载到JVM内存中,并转换成可以被JVM使用的Class对象的组件。
Java类加载器体系
Java中的类加载器按照层次结构分为几个级别,形成了一个树状的结构。
启动类加载器
这是最顶层的类加载器,负责加载Java的核心类库,比如rt.jar中的所有类(String、Object等)。它是用C++实现的,所以在Java代码中看不到具体的类,显示为null。
你可以通过这个方式查看启动类加载器加载了哪些路径:
public class BootstrapClassLoaderDemo {public static void main(String[] args) {System.out.println("启动类加载器加载的路径:");String[] bootClassPath = System.getProperty("sun.boot.class.path").split(";");for (String path : bootClassPath) {System.out.println(path);}}
}
扩展类加载器
扩展类加载器负责加载Java的扩展库,默认加载JAVA_HOME/lib/ext目录下的jar包。
public class ExtensionClassLoaderDemo {public static void main(String[] args) {ClassLoader extClassLoader = ClassLoader.getSystemClassLoader().getParent();System.out.println("扩展类加载器: " + extClassLoader);System.out.println("扩展类加载器加载的路径:");String[] extDirs = System.getProperty("java.ext.dirs").split(";");for (String dir : extDirs) {System.out.println(dir);}}
}
应用类加载器
这个是我们最常接触的类加载器,负责加载用户类路径(classpath)下的类。我们项目中写的类基本都是由它加载的。
public class ApplicationClassLoaderDemo {public static void main(String[] args) {ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();System.out.println("应用类加载器: " + appClassLoader);System.out.println("应用类加载器加载的路径:");String[] classPaths = System.getProperty("java.class.path").split(";");for (String path : classPaths) {System.out.println(path);}}
}
双亲委派模型
双亲委派模型是Java类加载器的核心机制,它的工作流程是这样的:
- 当一个类加载器收到类加载请求时,首先不会自己去加载这个类
- 而是把请求委派给父类加载器去完成
- 每一层都是如此,最终请求会传递到启动类加载器
- 只有当父加载器无法完成加载时,子加载器才会尝试自己加载
双亲委派的源码实现
我们来看看ClassLoader中loadClass方法的源码:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 首先检查类是否已经被加载Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {// 委派给父加载器c = parent.loadClass(name, false);} else {// 父加载器为null,委派给启动类加载器c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// 父加载器无法加载}if (c == null) {// 父加载器无法加载,自己尝试加载long t1 = System.nanoTime();c = findClass(name);// 记录统计信息sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}
}
双亲委派的好处
避免重复加载:如果一个类已经被父加载器加载过了,就不需要子加载器再加载一次。
安全性:核心类库只能由启动类加载器加载,避免了用户自定义的类替换掉核心类库的风险。
如何打破双亲委派模型
虽然双亲委派模型设计得很好,但现实总是复杂的。在某些场景下,我们确实需要打破这个规则。要打破双亲委派,关键是重写loadClass方法而不是findClass方法。因为双亲委派的逻辑就写在loadClass里面。
看看正常的双亲委派流程:
// ClassLoader.loadClass的简化逻辑
protected Class<?> loadClass(String name, boolean resolve) {// 1. 先检查是否已经加载Class<?> c = findLoadedClass(name);if (c == null) {// 2. 委派给父加载器(这里是关键!)if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}// 3. 父加载器加载失败,自己尝试if (c == null) {c = findClass(name);}}return c;
}
要打破双亲委派,我们就要改变这个流程,不再无条件地先委派给父加载器。
直接重写loadClass方法(自定义)
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 1. 先检查是否已经加载过Class<?> clazz = findLoadedClass(name);if (clazz != null) {return clazz;}// 2. 这里是关键:我们改变了加载顺序// 对于我们关心的包,先尝试自己加载if (name.startsWith("com.mycompany.")) {System.out.println("自定义加载器优先加载: " + name);try {clazz = findClass(name);if (clazz != null) {if (resolve) {resolveClass(clazz);}return clazz;}} catch (ClassNotFoundException e) {// 自己加载失败,再委派给父加载器System.out.println("自己加载失败,委派给父加载器: " + name);}}// 3. 对于系统类或者自己加载失败的类,还是走双亲委派if (getParent() != null) {clazz = getParent().loadClass(name);} else {clazz = getSystemClassLoader().loadClass(name);}if (resolve) {resolveClass(clazz);}return clazz;}}