要了解双亲委派,就需要先了解类加载器,可以参考[[类的生命周期及类加载器]]一文。
1. 双亲委派机制是什么?
双亲委派机制指的是:当一个类加载器收到加载类的任务时,会向上查找查找是否加载过,再由顶向下进行加载。
即,当某个类加载器收到加载任务,如果自己没有加载过此类,则会向上委派加载任务给自己的父加载器,如此重复知道委托到启动类加载器,然后再尝试加载类,如果当前类加载器无法加载,则任务下发给自己的子类加载器。过程如下图:

举例说明
假设现在用应用程序类加载器(Application ClassLoader)来加载当前项目classpath目录下的 Demo9.class文件。步骤如下:
- Application ClassLoader收到加载任务后,先查询自己是否加载过此类。若加载过,直接返回class对象即可。否则委托给Extension/Platform ClassLoader。
- Extension/Platform ClassLoader收到后,查询是否加载过此类,若加载过,直接返回class对象,否则,委托给Bootstrap ClassLoader。
- Bootstrap ClassLoader收到任务后,查询是否加载过此类,若加载过,返回class对象。否则,开始尝试加载,若自己的域中无此类的字节码文件,则向下尝试加载。若有,则加载后,返回class对象。
- Extension/Platform ClassLoader开始尝试加载,若自己的域中无此类字节码文件,则继续向下尝试加载。若有,则加载后返回class对象。
- Application ClassLoader开始尝试加载,若自己域中无此类字节码文件,则抛出
ClassNotFoundException异常。若有,则加载后,返回class对象。
以上步骤中,1、2、3步骤为向上查找是否加载过的步骤;3、4、5步骤为向下尝试加载的步骤。
2. 为什么使用双亲委派?
主要是为了保证类加载的安全性,避免重复加载。即,防止程序代码会对JDK造成影响。如下案例:
定义一个包名为sun.util.calendar的名为ZoneInfo的类。如下:
1 2 3 4 5 6 7 8 9 10 11
| package sun.util.calendar;
public class ZoneInfo { private int id = 666; }
|
此时,使用Application ClassLoader来加载此类。
1 2 3 4 5 6 7 8
| public static void main(String[] args) throws Exception{ ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); Class<?> aClass = systemClassLoader.loadClass("sun.util.calendar.ZoneInfo"); System.out.println(aClass); System.out.println(aClass.getClassLoader()); }
|
输出如下:
1 2
| class sun.util.calendar.ZoneInfo null
|
可见,ZoneInfo类已经被成功加载。获取其类加载器显示null,即启动类加载器。那么就说明,加载的依然是$JAVA_HOME/jre/lib下的字节码文件,而非classpath下的字节码文件。避免了用户自己定义的类破坏JDK核心类的情况。
3. 如何做到双亲委派?
不难推断出,双亲委派机制为类加载器中实现的。所有的类加载器都继承了 ClassLoader 类(Bootstrap ClassLoader除外),以下是ClassLoader中几个核心的方法。先看总览:
1 2 3 4 5 6 7 8 9 10 11
| public Class<?> loadClass(String name)
protected Class<?> findClass(String name)
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
protected final void resolveClass(Class<?> c)
|
3.1 loadClass(String name)的实现
要让指定类加载器加载指定的类,次方法为入口。双亲委派机制在次方法中实现,以下是部分代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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 { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { long t1 = System.nanoTime(); c = findClass(name); PerfCounter.getParentDelegationTime().addTime(t1 - t0); PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
|
3.2 findClass()实现
ClassLoader的findClass实现如下,什么都没有。 通常由子类去实现,根据名称或位置加载.class字节码,然后使用defineClass()方法去解析class字节流,返回class对象实例))将字节码转换为Class返回。
1 2 3
| protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
|
那我们就挑一个相对比较简洁易懂的子类来阅读其 findClass()方法的源码。Hutool的ResourceClassLoader:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Override protected Class<?> findClass(String name) throws ClassNotFoundException { final Class<?> clazz = cacheClassMap.computeIfAbsent(name, this::defineByName); if (clazz == null) { return super.findClass(name); } return clazz; }
private Class<?> defineByName(String name) { final Resource resource = resourceMap.get(name); if (null != resource) { final byte[] bytes = resource.readBytes(); return defineClass(name, bytes, 0, bytes.length); } return null; }
|
先查看缓存中是否有指定类名的class类,若有责返回,没有就直接去尝试获取此类的二进制流,并调用defineClass()方法将类存入内存。具体流程在此代码中已经非常清晰。
3.3 defineClass()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError { protectionDomain = preDefineClass(name, protectionDomain); String source = defineClassSourceLocation(protectionDomain); Class<?> c = defineClass1(this, name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain); return c; }
|
可见,defineClass()又调用了一个defineClass1方法,将类字节码文件加载到内存中,此方法为本地方法,非Java实现。
3.4 resolveClass()实现
此方法做用就是对指定的类做连接操作,调用的依然是本地方法。
1 2 3 4 5
| protected final void resolveClass(Class<?> c) { resolveClass0(c); } private native void resolveClass0(Class<?> c);
|
由上可知,[[类的生命周期及类加载器]]一节中的自定义类加载器,就是重写了findClass()方法。
4. 双亲委派可以打破吗?
先给出答案,可以。
根据以上的分析,双亲委派在 loadClass()中实现,那么,我们重写loadClass()方法即可覆盖掉双亲委派的代码逻辑,实现打破双亲委派。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class MyCLassLoader extends ClassLoader{ public static final String MY_CLASS_HOME = "/Users/yang/"; public static final String CLASS_SUFFIX = ".class"; @Override public Class<?> loadClass(String name) throws ClassNotFoundException { if (!name.startsWith("com.littley")) { return super.loadClass(name); } String path = MY_CLASS_HOME + name + CLASS_SUFFIX; byte[] bytes = FileUtil.readBytes(path); return defineClass(name, bytes, 0, bytes.length); } public static void main(String[] args) throws Exception { MyCLassLoader myCLassLoader = new MyCLassLoader(); Class<?> aClass = myCLassLoader.loadClass("com.yang.Student"); System.out.println(aClass); System.out.println(aClass.getClassLoader()); } }
|
输出为:
1 2
| class com.yang.Student com.yang.MyCLassLoader@4dcbadb4
|
相关的思考
再回忆一下双亲委派机制的作用,不就是为了保证JDK的安全性吗?既然可以打破的话,那么不是可以用相同的方案加载一个自己写的 java.lang.String?
事实上是无法做到的,JDK对对于java包下的类做了比较强的保护。在defineClass()方法中,有一个前置的安全检查方法 preDefineClass(),代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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; }
|
是直接写死在代码中,如果全限定名为java.开头,则直接抛出异常。
通过打破双亲委派可以实现很多功能,后续单独讨论。