委派模型与类加载器

最近在读许令波的深入分析Java Web技术内幕一书,对于学习Java以来一直有的几个疑惑得到了解答,遂记录下来.

我的疑问

  1. 双亲委派模型(实际上是一个翻译错误,英文为parent delegation,只是一个父委托模型)是什么?如何实现?为什么这样实现?
  2. 热加载的技术原理是什么?
  3. ClassLoader如何实现动态加载jar,实现插件模式系统?
    下面跟着教程来寻找这些答案.

ClassLoader与委派模型

ClassLoader顾名思义是类加载器,负责将Class加载到JVM中,其所使用的加载策略叫做双亲委派模型.
JVM平台提供三个ClassLoader:

  • Bootstrap ClassLoader,由C++实现的类加载器,其主要负责加载JVM自身工作所需要的类,该Loader由JVM自身控制,别人是无法访问的,其也不再双亲委派模型中承担角色.
  • ExtClassLoader,该类加载器主要加载System.getProperty("java.ext.dirs")所对应的目录下class文件.一般为JVM平台扩展工具.
  • AppClassLoader,该类加载器主要加载 System.getProperty("java.class.path")所对应的目录下的class文件,其委托父类为ExtClassLoader(后面会解释)

对于ExtClassLoaderAppClassLoader有着一个统一的父类ClassLoader,该类的结构如下,其拥有一个委托父类对象,同样的设计在Mybatis的Exector中也有体现,感兴趣的同学可以看我之前关于Mybatis分析的文章.

1
2
3
4
5
public abstract class ClassLoader {
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve);
......
}

接下来重点看loadClass()方法,该方法为加载class二进制文件的核心方法.

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
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//当父加载器不存在的时候会尝试使用BootStrapClassLoader作为父类
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//c为null则证明父加载器没有加载到,进而使用子类本身的加载策略`findClass()`方法
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

那么开始回答问题
1. 双亲委派模型是什么?
上述加载流程是 使用parent加载器加载类 -> parent不存在使用BootStrapClassLoader加载 -> 加载不到则使用子类的加载策略,这里要注意BootStrapClassLoader是由C++实现的JVM内部的加载工具,其没有对应的Java对象,因此不在这个委派体系中,只是相当于名义上的加载器父类. 那么所谓的双亲我认为是parent委托对象与BootStrapClassLoader最顶端的加载器,两者都是属于被委托的对象,那么这就是所谓的双亲委派模型.

那么双亲是什么? 看ClassLoader的注释就能发现这只是个翻译错误,害得我脑补半天,明明是单亲委派,更通俗来说就是一个委托模式,当parent为null的时候,其parent为名义上的BootStrapClassLoader

1
Each instance of <tt>ClassLoader</tt> has an associated parent class loader

2. 委派模型如何实现?
实现如上述代码所示,其类本身有private final ClassLoader parent;这一委托父对象,另外其还有虚拟机实现的BootStrapClassLoader这个名义上的父加载器,在方法上优先执行委托类的策略.
3. 为什么使用委派模型?
回答这个问题要先了解Java中是如何判定两个类是同一个类状况,如下段官方所说,也就是类名(包括包名)相同并且他们的类加载器相同,那么两个对象才是等价的.

1
2
3
At run time, several reference types with the same binary name may be loaded simultaneously by different class loaders. 
These types may or may not represent the same type declaration.
Even if two such types do represent the same type declaration, they are considered distinct.

对于Object类因为父加载器先加载所以能保证对于所有Object的子类其所对应的Object都是由同一个ClassLoader所加载,也就保证了对象相等. 简单来说委托类优先模式保证了加载器的优先级问题,让优先级高的ClassLoader先加载,然后轮到优先级低的.

热加载的技术原理

热部署对于开发阶段的实用性极高,利用Jrebel等工具可以极大的节省应用调试时间.关于热加载技术可以参考文章http://www.hollischuang.com/archives/606,
对于一个被ClassLoader加载到内存的类来说,再次加载的时候就会被findLoadedClass()方法所拦截,其判断该类已加载,则不会再次加载,那么热加载的技术本质是要替换到已加载的类.

对于Spring Boot devtools的restart技术,其是使用了两个ClassLoader,对于开发者所写的类使用自定义的ClassLoader,对于第三方包则使用默认加载器,那么每当代码有改动需要热加载时,丢弃自定义的ClassLoader所加载的类,然后重新使用其加载,如此做到了热部署.

对于Jrebel使用的貌似是修改类的字节码方式,具体不是很懂也就不讨论了.

对于Tomcat,其热部署技术是每次清理之前的引用,然后创建一个新的ClassLoaderWebClassLoader来重新加载应用,这个加载使得永久代中对象增多,那么清理要求是full GC,这个是不可控的,所以也就导致了Tomcat热部署频繁会触发java.lang.OutOfMemoryErrorPermGen space这个bug.

ClassLoader如何实现动态加载jar,实现插件模式系统?

ClassLoader的委派模型使得很容易扩展自定义的类加载器,那么基本步骤 定义自己的类加载器 -> 加载指定jar -> 创建所需要的应用实例,大概代码如下.

1
2
3
4
5
6
7
8
9
10
String jarPath = "/Users/niuli/workspace/quding-git/quding-study/helloworld/target/hello-world-1.0-SNAPSHOT.jar";
URL jarUrl = new File(jarPath).toURI().toURL();
//加载该jar
URLClassLoader loader = new URLClassLoader(new URL[]{jarUrl},Thread.currentThread().getContextClassLoader());
//获取插件Class对象
Class helloClass = loader.loadClass("com.itoolshub.hello.HelloWorld");
//创建该对象
IHelloWorldService helloWorldService = (IHelloWorldService) helloClass.newInstance();
//调用方法
helloWorldService.sayHello();

另外插件模式的话一般还会有一些配置文件plugin.xml,告诉系统主要对外提供服务的类是什么以及一些默认配置等.不过大概思路都是大同小异.

另外既然有装载也就有卸载,卸载的必要条件是以下三个外,另外类是装载在永久代,那么卸载的触发也就是full GC才会去清理永久代中没有被强引用指向的类.

  1. 该类所有的实例都已经被GC。
  2. 加载该类的ClassLoader实例已经被GC。
  3. 该类的java.lang.Class对象没有在任何地方被引用。
感谢您的阅读,本文由 屈定's Blog 版权所有。如若转载,请注明出处:屈定's Blog(http://mrdear.cn/2017/11/13/java/委派模型与类加载器/
Java中的四种引用
git的回滚操作