Java 源码阅读 - 类加载的双亲委派模型
关于 Java 的类加载机制,尽管我看过几篇文章,知道个双亲委派模型,但是从来没钻进源码里看它到底是怎么委派的。
什么双亲?怎么委派?
我刚一开始听到双亲委派,还纳闷咋还双亲?后来才知道,这纯纯就是Parent Delegation这个词的误译。Parent这里指的并不是双亲,而是指父辈。所以看到有人翻译为 “向上委托模型”,我感觉这个翻译更好一点,至于另一个翻译 “啃老模型”…… 倒也没毛病……
至于怎么委托,相信各位都背的滚瓜烂熟了。那就是,当类加载器收到类加载请求的时候,它首先会把这个请求委托给上一层的类加载器去尝试加载,直到委托到启动类加载器;只有当上一层类加载器无法完成这个加载请求的时候,次一级类加载器才会尝试自己加载。

代码上的实现
截图里面的代码就是ClassLoader#loadClass方法的实现,来自Liberica JDK 8。

看得出来,逻辑还是很简单易懂的。一进来先加个锁,防止出现并发问题。然后检查这个类是不是已经被加载了。没被加载的话,就一层层向上委托,直到到达启动类加载器。如果上一层类加载器返回了null或者抛出了ClassNotFoundException异常,就说明它没找到这个类,那么本层类加载器就会尝试加载这个类,如果找不到的话,它就接着把请求交回下一层的类加载器。
虽然上面的图和代码已经可以解释双亲委派的工作机制,但我还是喜欢调试进去看看代码具体是咋走的。所以我写了这么几行,用来调试类加载器。
1 | public class Main { |
断点从classLoader.loadClass("DaemonThreadDemo")这一行进去并停留在if (c == null)之后可以看到,目前类加载的委托请求是交给AppClassLoader,看得出来,这个就是应用类加载器。

继续往下走到c = parent.loadClass(name, false)这一行,然后给parent变量添加一个监视,就可以看到接下来AppClassLoader要把这个类加载请求委托给ExtClassLoader,同理可得,这个就是扩展类加载器。

接着往下走,继续调试ExtClassLoader,这时候可以看到parent是null。没有了parent,这个类加载器就会将这个类加载请求委托给启动类加载器并尝试加载这个类。

逐层点进去,可以看到如下代码:

emmmm…… 走到了一个 native 方法了呢…… 嘛,里面的代码先不管了,看名字能猜得出来,在这里会调用启动类加载器来尝试加载这个类。
因为要加载的DaemonThreadDemo类并不归启动类加载器管,所以findBootstrapClassOrNull返回了null。ExtClassLoader得知启动类加载器加载失败,那么它自己就会再尝试加载。然而这个类也不归扩展类加载器管,所以在ExtClassLoader里面调用findClass方法会抛出ClassNotFoundException异常并返回到AppClassLoader。
这时候,因为DaemonThreadDemo这个类归应用类加载器管,所以这一次调用findClass成功的找到了这个类。

所以代码可以成功走到return c这一行,来完成一个类的加载。
破坏双亲委派模型
说到双亲委派模型,必会谈到怎么破坏它。看完上面的代码就明白了,我们可以自己创建一个自定义类加载器,并重写loadClass方法,不让它向上委派就行了。
番外:尝试理解findBootstrapClass
虽然这部分是 C 和 C++ 的实现,但还是想硬着头皮尝试看一下。到Bellsoft的官网下载虚拟机的源码之后,我找到了FindBootStrapClass函数的实现:
1 | // 这部分代码在 java_md.c |
这个 C 语言…… 确实跟我大学学的 C 语言不一样啊…… 爬了些文,大概理解这里是要找JVM_FindClassFromBootLoader这个函数的实际地址,然后赋给findBootClass指针并执行它的代码。于是我接着挖到了JVM_FindClassFromBootLoader函数的实现。
1 | // 这部分代码在 jvm.cpp |
好吧,更看不懂了。继续爬了文之后,理解这里大致做了三件事:从常量池中拿到类名的信息;查找类的信息并实例化Klass;将Klass对象转换为jclass类型并返回。
算了,虚拟机源码就看到这吧…… 有兴趣的同志可以看 JVM 系列 (四):java 方法的查找过程实现这篇博客。