一文看懂双亲委派
一文带你看懂双亲委派机制
类的加载
首先需要了解一下类的加载过程,类的加载过程可以分为加载、连接、初始化三步走。其中加载就是将java字节码转化为JVM中的java.lang.Class类的对象,而转化的”工具“就是类加载器(ClassLoader)。类加载器可以分为:
- 启动类加载器 Bootstrap ClassLoader
- 扩展类加载器 Extention ClassLoader
- 应用类加载器 Application ClassLoader
- 自定义类加载器 User ClassLoader
这四种类加载器的层级关系如下图所示:
上层的加载器为下层的父加载器,每一种类加载器都是”各司其职“的:
- 启动类加载器负责加载Java的核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
- 扩展类加载器负责加载%JRE_HOME%\lib\ext目录下的jar包和class文件
- 应用类加载器负责加载当前应用classpath下的所有类 AppClassLoader 是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。可以由 ClassLoader 类提供的静态方法 getSystemClassLoader() 得到.当我们的 main 方法执行的时候,这第一个用户类的加载器就是 AppClassLoader。
- 自定义类加载器可以加载指定路径的class文件
可以看到不同的类加载器负责加载指定的class文件,因此这里也就可以引出另外一个知识点:类在JVM中的唯一性是由该类本身和加载该类的类加载器确定的。
这里我们重新理解一下 ClassLoader 的意义,它相当于类的命名空间,起到了类隔离的作用。位于同一个 ClassLoader 里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类。ClassLoader 是类名称的容器,是类的沙箱。
不同的 ClassLoader 之间也会有合作,它们之间的合作是通过 parent 属性和双亲委派机制来完成的。parent 具有更高的加载优先级。除此之外,parent 还表达了一种共享关系,当多个子 ClassLoader 共享同一个 parent 时,那么这个 parent 里面包含的类可以认为是所有子 ClassLoader 共享的。这也是为什么 BootstrapClassLoader 被所有的类加载器视为祖先加载器,JVM 核心类库自然应该被共享。
什么是”双亲委派”
了解了类加载器后,我们来看看什么是双亲委派,双亲委派的英文原文为 “parents delegate” 由于翻译的原因将 parents 翻译成了“双亲”,但其实 “双亲“只是“父母这一辈”的意思,因此双亲委派的解释为:
如果一个类加载器接收到加载某个类的请求,该类加载器比较”懒”,它自己不会马上去加载该类,而是委派给其父类加载器进行加载。每一层的类加载器都是如此,因而加载该类的请求会被传到最顶层的启动类加载器,如果自底向上都发现该类没有被加载,则再尝试自顶向下使用子类加载器加载该类。
简而言之双亲委派就是加载类的一种“算法”,我们来看看源代码中双亲委派是如何实现的:
首先看看java.lang.ClassLoader中父类加载器的定义:
1 | public abstract class ClassLoader { |
可以看到当前类加载器的父加载器是一组合的形式进行关联的,因此类加载器之间的父子关系不是通过继承实现的。这里还需要指明一点,启动类加载器的parent成员变量为null
双亲委派的实现是在ClassLoader#loadClass方法中,下面是该方法的源码:
1 | protected Class<?> loadClass(String name, boolean resolve) |
从源码可以看出加载类的逻辑可以分为以下几个步骤:
自底向上检查该类是否已经被加载,如果已经加载过了就不用再加载了
如果加载命令传递到启动类加载器时,该类没有被加载则尝试使用启动类加载器进行加载。如果启动类加载器没有加载成功则调用findClass方法抛出ClassNotFoundException异常, 子类捕获到父类加载器抛出的异常后说明父类加载器没有加载成功,子类加载器再调用自己实现的findClass方法进行加载,也就是自顶向下进行加载。
如果最后都没有加载成功则会抛出 ClassNotFoundException 异常。
上述过程可以用下图表示:
双亲委派的优点
那么双亲委派有什么优点呢?
首先,自底向上检查类是否已经被加载可以防止类的重复加载,父类加载过某个类时该类就不会再被加载
其次,通过委派的方式可以保证安全性。举个例子:通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器发现该类已被加载,就会直接返回已加载过的Integer.class,并不会重新加载网络传递的过来的java.lang.Integer,这样便可以防止核心API库被随意篡改。
打破双亲委派机制
通过之前的源码分析,可以知道双亲委派机制是在loadClass方法中实现的,因此我们只需要继承ClassLoader类,通过重写loadClass方法即可自定义类加载机制。
自定义类加载器涉及到loadClass、findClass、defineClass三个方法。其中loadClass用于实现类的加载方法,findClass用于根据名字或路径加载字节码文件,而defineClass用于将字节码转化为Class对象。
如果不想破坏双亲委派机制,那么只需要重写findClass方法,下面是自定义类加载器的一个例子:
1 | import java.io.File; |
打破双亲委派机制的例子
很多容器框架都会打破这种机制来实现一些功能
JDBC破坏双亲委派
JDBC的Driver接口定义在JDK中,其实现由各个数据库的服务商来提供。在DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager位于JAVA_HOME中jre/lib/rt.jar 包是由启动类加载器加载的。根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。也就是说启动类加载器还要去加载jar包中的Driver接口的实现类。我们知道,启动类加载器默认只负责加载 JAVA_HOME中jre/lib/rt.jar 里所有的class,所以需要由子类加载器去加载Driver实现,这就破坏了双亲委派模型。
这个子类加载器是通过 Thread.currentThread().getContextClassLoader() 得到的线程上下文加载器。
在 sun.misc.Launcher 初始化的时候,会获取AppClassLoader,然后将其设置为上下文类加载器,所以线程上下文类加载器默认情况下就是应用类加载器。
Tomcat破坏双亲委派
tomcat是一个应用容器,不同的应用可能会依赖相同类库的不同版本。不同版本的类库中某个类的全路径名可能是一样的,如果采用双亲委派则不能重复加载,所以Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。
JDK9中破坏双亲委派
在JDK9之前,JVM的基础类以前都是在rt.jar这个包里,这个包也是JRE运行的基石。
这不仅是违反了单一职责原则,同样程序在编译的时候会将很多无用的类也一并打包,造成臃肿。
在JDK9中,整个JDK都基于模块化进行构建,以前的rt.jar, tool.jar被拆分成数十个模块,编译的时候只编译实际用到的模块,同时各个类加载器各司其职,只加载自己负责的模块。
1 | Class<?> c = findLoadedClass(cn); |