双亲委派机制

参考文章

oracle 官方文档
Class Loaders In Java - Baeldung
Demystifying class loading problems
浅谈双亲委派模型
tomcat classloader violates delegating policy
What is JAR Hell?
Spring Boot Classloader and Class Overriding

致命的翻译(双亲委派模型)

JVM 类加载机制也是一道常见的暖场题, 令人感到厌烦的是, 这个类加载机制的翻译就和“套接字”一样令人感到窒息。

大部分的计算机英文术语在命名时, 都会尽可能做到直白易懂, 体现技术概念的本质。 但是中文翻译中往往因为翻译者水平有限,导致这种信息的丢失, 使得原本直白的概念变得晦涩难懂,容易误解。

双亲委派模型 就是一个典型的例子。

大部分程序员第一眼看到这个术语, 脑子中必定会浮现这样一种画面:
在这里插入图片描述
上面这个第一印象, 再加上百度 “双亲委派模型” 最常见的如下配图, 基本上就足以误导 80 80%80% 80\% 8080% 的读者

  • 误解1: 双亲是指最下面的那两个UserClassLoader。
  • 误解2: 双亲是指两层父结点。
    在这里插入图片描述

正确的翻译(委派模型 或 父委派模型)

oracle 官方文档关于 jvm 类加载机制所用的描述是:

The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a "parent" class loader. When loading a class, a class loader first "delegates" the search for the class to its parent class loader before attempting to find the class itself.

翻译过来就是:

java 平台使用 委派模型来加载类。 基本思想就是, 每一个类加载器都有一个父加载器, 当需要加载一个 class
时, 首先把该 class 的查询和加载优先委派给父加载器进行, 如果父加载器无法加载该 class, 再去尝试自行加载这个 class

在这里插入图片描述

委派模型的代码实现

下面是 jdk 中 java.lang.ClassLoaderloadClass 方法具体代码逻辑, 较为清晰的展现了父加载模型的逻辑。

父委派模型
找到
未找到
不为空
为空
未成功加载
查找是父引用 parent 是否为空
parent.loadClass(name)

用 parent 去加载
调用 findBootstrapClassOrNull(name)

其实就是用 BootStrapClassLoader 去加载
调用 findClass( myClassName )

寻找待加载的 class 物理文件,并解析加载为 class 予以返回
loadClass( myClassName )
loadClass( myClassName,false)
findLoadedClass( myClassName)

寻找是否已经加载过该类
直接返回
该名称的类

下面是源码摘录 java.lang.ClassLoader.java( jdk1.8.0_101)

public Class<?> loadClass(String name) throws ClassNotFoundException {
     return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loadCed
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) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

                <span class="token keyword">if</span> <span class="token punctuation">(</span>c <span class="token operator">==</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>
                    <span class="token comment">// If still not found, then invoke findClass in order</span>
                    <span class="token comment">// to find the class.</span>
                    <span class="token keyword">long</span> t1 <span class="token operator">=</span> System<span class="token punctuation">.</span><span class="token function">nanoTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
                    c <span class="token operator">=</span> <span class="token function">findClass</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>

                    <span class="token comment">// this is the defining class loader; record the stats</span>
                    sun<span class="token punctuation">.</span>misc<span class="token punctuation">.</span>PerfCounter<span class="token punctuation">.</span><span class="token function">getParentDelegationTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addTime</span><span class="token punctuation">(</span>t1 <span class="token operator">-</span> t0<span class="token punctuation">)</span><span class="token punctuation">;</span>
                    sun<span class="token punctuation">.</span>misc<span class="token punctuation">.</span>PerfCounter<span class="token punctuation">.</span><span class="token function">getFindClassTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addElapsedTimeFrom</span><span class="token punctuation">(</span>t1<span class="token punctuation">)</span><span class="token punctuation">;</span>
                    sun<span class="token punctuation">.</span>misc<span class="token punctuation">.</span>PerfCounter<span class="token punctuation">.</span><span class="token function">getFindClasses</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">increment</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
                <span class="token punctuation">}</span>
            <span class="token punctuation">}</span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span>resolve<span class="token punctuation">)</span> <span class="token punctuation">{</span>
                <span class="token function">resolveClass</span><span class="token punctuation">(</span>c<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span>
            <span class="token keyword">return</span> c<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
  • 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
  • 39
  • 40
  • 41

类加载过程的细分流程

细心的同学应该会有疑问, 加载一个类时, 发现该类还继承其他的类,或者方法定义中用到了别的类作为参数或者返回值, 会发生什么。

这就涉及到了类的具体加载过程, 如下图, 类的加载过程被从左到右划分为 3 大阶段:

  • 装载 (Loading)
    • 该阶段负责找到待加载类的二进制 class 文件, 并把它以 bytecode 的形式装载到虚拟机。 在这个过程中, JVM 会给这个类分配一个基本的内存结构, 但是方法, 变量域, 和它引用到的其他类在这个阶段都还没有处理, 也就是说, 这个类在目前阶段还不可用
  • 链接 (Linking)
    • 这个步骤又可细分为3个阶段
    • 字节码验证
      • 验证字节码是否是一个正确,符合规范的类字节码
    • 类准备
      • 为这个类定义好必须的数据结构以表示成员变量域, 方法, 以及实现的接口等等
    • 解析
      • 把这个类锁引用的其他类全部加载进来 , 引用的方式有如下几种:
        • 继承
        • 实现接口
        • 域变量
        • 方法定义
        • 方法中定义的本地变量
  • 初始化(Initializing)
    • 执行类中定义的静态代码块, 初始化静态变量为默认值

在这里插入图片描述

隐式加载 vs 显示加载

从上文类加载的详细过程可以看出, 类有两种方式被加载进来

  • 显式加载
    • 程序主动调用下列类型的方法去主动加载一个类
      • classloader.loadClass( className)
      • Class.forName( className)
  • 隐式加载
    • 被显式加载的类对其他类可能存在如下引用:
      • 继承
      • 实现接口
      • 域变量
      • 方法定义
      • 方法中定义的本地变量
    • 被引用的类会被动地一并加载至虚拟机, 这种加载方式属于隐式加载

不要把父加载器误解为父类

看过 ClassLoader 的源码以后, 会意识到所谓的父加载器, 只是一个简单的成员变量引用 parent, 该引用在构造 ClassLoader 时, 由外部传递

正如先前所展示的, jdk 默认提供了三类内建的类加载器。

在这里插入图片描述
下面代码的输出了不同类的加载器

public void printClassLoaders() throws ClassNotFoundException {
System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"Classloader of this class:"</span>
    <span class="token operator">+</span> PrintClassLoader<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">.</span><span class="token function">getClassLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"Classloader of Logging:"</span>
    <span class="token operator">+</span> Logging<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">.</span><span class="token function">getClassLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"Classloader of ArrayList:"</span>
    <span class="token operator">+</span> ArrayList<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">.</span><span class="token function">getClassLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

输出内容:

Class loader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Class loader of Logging:sun.misc.Launcher$ExtClassLoader@3caeaf62
Class loader of ArrayList:null

 
 
  • 1
  • 2
  • 3

上面输出了三种结果 AppClassLoader, ExtClassLoader, null(其实是 BootStrapClassLoader)

  • AppClassLoader 负责加载 classpath 下的文件
  • ExtClassLoader 负责加载 java 核心类的扩展类, 通常是搜索 $JAVA_HOME/lib/ext 中的文件或是任意定义在 java.ext.dirs 属性中的文件夹下的文件予以加载
  • BootStrapClassLoader 负责加载 java 核心类, 例如 ArrayList.

但是, 我们看到, ArrayList 加载类的输出内容为 null , 这是因为 BootStrapClassLoader 是用平台原生语言( 可能是 C,C++ 或其他平台相关语言) , 而 getClassLoader() 返回的是 java 类, 所以这项输出只能为空

如何看出三种内置加载器的父子关系

AppClassLoader, ExtClassLoader 是由 sun.misc.Launcher 初始化的, 查看源码中的构造方法可以发现

源码为 IDE 反编译获得, 所以变量名可读性较弱, 但不影响理解

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
        	// var1 是 ExtClassLoader 引用变量
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
    <span class="token keyword">try</span> <span class="token punctuation">{</span>
    	<span class="token comment">// var1 作为入参, 传入了 getAppClassLoader </span>
        <span class="token keyword">this</span><span class="token punctuation">.</span>loader <span class="token operator">=</span> Launcher<span class="token punctuation">.</span>AppClassLoader<span class="token punctuation">.</span><span class="token function">getAppClassLoader</span><span class="token punctuation">(</span>var1<span class="token punctuation">)</span><span class="token punctuation">;</span>  
    <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">IOException</span> var9<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">InternalError</span><span class="token punctuation">(</span><span class="token string">"Could not create application class loader"</span><span class="token punctuation">,</span> var9<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    Thread<span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">setContextClassLoader</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>loader<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">// ... 省略部分代码</span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

沿着 getAppClassLoader() 方法, 最后可以追踪到 ClassLoader 的构造方法中, 可以看到 getAppClassLoader(var1) 中传入的参数 var1 最终被保存在 parent 成员变量中

private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        // 省略部分代码
    }

 
 
  • 1
  • 2
  • 3
  • 4

综上, 可以看到 AppClassLoaderExtClassLoader 的父子关系由 Launcher 保证

BootStrapClassLoader 是如何成为 ExtClassLoader 的父加载器呢?

其实上文提到过的 ClassLoader 的源码逻辑提供了答案

父委派模型
不为空
为空
未成功加载
查找是父引用 parent 是否为空
parent.loadClass(name)

用 parent 去加载
调用 findBootstrapClassOrNull(name)

其实就是用 BootStrapClassLoader 去加载

public class Main {
public Main() {
}

<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">main</span><span class="token punctuation">(</span>String<span class="token punctuation">[</span><span class="token punctuation">]</span> args<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"this sun.applet.Main class defined by an ignorant programmer"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

得到输出

用法: appletviewer <options> url

其中, <options> 包括:
-debug 在 Java 调试器中启动小应用程序查看器
-encoding <encoding> 指定 HTML 文件使用的字符编码
-J<runtime flag> 将参数传递到 java 解释器

-J 选项是非标准选项, 如有更改, 恕不另行通知。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这说明 jvm 并没有加载运行我们自行定义的 sun.applet.Main , 这也是父委派模型的好处, 当 AppClassLoader 试图加载我们自行定义的 sun.applet.Main 时, 最终将这个请求委派给了 Bootstrap Class Loader, 并执行了 jdk 中所定义的 sun.applet.Main 类的 main 方法。

原因二: 类可见性

使用了父委派模型的另一个影响是, 一个类加载器只能看到由他自己或是由其父辈加载的类, 它自己是看不到更低层级加载器所负责加载的类。

例如, 如果父加载器(ExtClassLoader)需要加载的类 $JAVA_HOME/jre/ext/xx.jar#Class A 引用了存在于更低层级加载器AppClassLoader负责范围($class_path)中才存在的类, 那么在加载过程就会报错。

当这种需求出现的时候, 可以使用 JDK 提供的另一种类加载器 ContextClassLoader 予以解决, 这里不做展开描述, 有兴趣的同学请自行查阅资料

如何自定义符合父委派模型的类加载器

  • 首先, 自定义 CustomClassLoader 继承自 ClassLoader
public class CustomClassLoader extends ClassLoader {
<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> Class <span class="token class-name">findClass</span><span class="token punctuation">(</span>String name<span class="token punctuation">)</span> <span class="token keyword">throws</span> ClassNotFoundException <span class="token punctuation">{</span>
    <span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span> b <span class="token operator">=</span> <span class="token function">loadClassFromFile</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> <span class="token function">defineClass</span><span class="token punctuation">(</span>name<span class="token punctuation">,</span> b<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> b<span class="token punctuation">.</span>length<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">private</span> <span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token function">loadClassFromFile</span><span class="token punctuation">(</span>String fileName<span class="token punctuation">)</span>  <span class="token punctuation">{</span>
    InputStream inputStream <span class="token operator">=</span> <span class="token function">getClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getClassLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getResourceAsStream</span><span class="token punctuation">(</span>
            fileName<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token string">'.'</span><span class="token punctuation">,</span> File<span class="token punctuation">.</span>separatorChar<span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">".class"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span> buffer<span class="token punctuation">;</span>
    ByteArrayOutputStream byteStream <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ByteArrayOutputStream</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">int</span> nextValue <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
    <span class="token keyword">try</span> <span class="token punctuation">{</span>
        <span class="token keyword">while</span> <span class="token punctuation">(</span> <span class="token punctuation">(</span>nextValue <span class="token operator">=</span> inputStream<span class="token punctuation">.</span><span class="token function">read</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">!=</span> <span class="token operator">-</span><span class="token number">1</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span>
            byteStream<span class="token punctuation">.</span><span class="token function">write</span><span class="token punctuation">(</span>nextValue<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">IOException</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        e<span class="token punctuation">.</span><span class="token function">printStackTrace</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    buffer <span class="token operator">=</span> byteStream<span class="token punctuation">.</span><span class="token function">toByteArray</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> buffer<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

  • 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

注意到我们重写了 ClassLoader 中的 findClass(String name) 方法, 里面自行实现了读取 class 文件为 byte 数组, 调用 defineClass 方法将 byte 数组解析加载为类。

由于我们并未重写 loadClass(String name) 方法 , 所以 CustomClassLoader 依旧会遵从 loadClass(String name) 中定义的父委派模型加载方法。

如何自定义一个违背父加载模型的类加载器

以为我们之前自行定义的 sun.applet.Main 为例, 如果我们就是想让这个自定义的类加载到 JVM 中, 并得以执行自定义 main 方法, 该如何自定义一个类加载器完成该操作?

package sun.applet;

public class Main {
public Main() {
}

<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">main</span><span class="token punctuation">(</span>String<span class="token punctuation">[</span><span class="token punctuation">]</span> args<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"this sun.applet.Main class defined by an ignorant programmer"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

把 ide 编译出的 Main.class 文件放到 ./out/production/classes/sun/applet/ 目录下

然后自定义类加载器如下

public class UnDelegationClassLoader extends ClassLoader {
    private String classpath;
<span class="token keyword">public</span> <span class="token function">UnDelegationClassLoader</span><span class="token punctuation">(</span>String classpath<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">super</span><span class="token punctuation">(</span>null<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">this</span><span class="token punctuation">.</span>classpath <span class="token operator">=</span> classpath<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> Class<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">&gt;</span> <span class="token function">loadClass</span><span class="token punctuation">(</span>String name<span class="token punctuation">)</span> <span class="token keyword">throws</span> ClassNotFoundException <span class="token punctuation">{</span>
    Class<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">&gt;</span> clz <span class="token operator">=</span> <span class="token function">findLoadedClass</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>clz <span class="token operator">!=</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token keyword">return</span> clz<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>    
    <span class="token comment">// jdk 目前对"java."开头的包增加了权限保护,这些包我们仍然交给 jdk 加载</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>name<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">"java."</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token keyword">return</span> ClassLoader<span class="token punctuation">.</span><span class="token function">getSystemClassLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">loadClass</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">return</span> <span class="token function">findClass</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token annotation punctuation">@Override</span>
<span class="token keyword">protected</span> Class<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">&gt;</span> <span class="token function">findClass</span><span class="token punctuation">(</span>String name<span class="token punctuation">)</span> <span class="token keyword">throws</span> ClassNotFoundException <span class="token punctuation">{</span>
    InputStream is <span class="token operator">=</span> null<span class="token punctuation">;</span>
    <span class="token keyword">try</span> <span class="token punctuation">{</span>
        String classFilePath <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>classpath <span class="token operator">+</span> name<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token string">"."</span><span class="token punctuation">,</span> <span class="token string">"/"</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">".class"</span><span class="token punctuation">;</span>
        is <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">FileInputStream</span><span class="token punctuation">(</span>classFilePath<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span> buf <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">byte</span><span class="token punctuation">[</span>is<span class="token punctuation">.</span><span class="token function">available</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
        is<span class="token punctuation">.</span><span class="token function">read</span><span class="token punctuation">(</span>buf<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">return</span> <span class="token function">defineClass</span><span class="token punctuation">(</span>name<span class="token punctuation">,</span> buf<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> buf<span class="token punctuation">.</span>length<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">IOException</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">ClassNotFoundException</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>is <span class="token operator">!=</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span>
            <span class="token keyword">try</span> <span class="token punctuation">{</span>
                is<span class="token punctuation">.</span><span class="token function">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">IOException</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span>
                <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">IOError</span><span class="token punctuation">(</span>e<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">main</span><span class="token punctuation">(</span>String<span class="token punctuation">[</span><span class="token punctuation">]</span> args<span class="token punctuation">)</span>
        <span class="token keyword">throws</span> ClassNotFoundException<span class="token punctuation">,</span> IllegalAccessException<span class="token punctuation">,</span> InstantiationException<span class="token punctuation">,</span>
        MalformedURLException<span class="token punctuation">,</span> FileNotFoundException<span class="token punctuation">,</span> NoSuchMethodException<span class="token punctuation">,</span> InvocationTargetException <span class="token punctuation">{</span>
        
    sun<span class="token punctuation">.</span>applet<span class="token punctuation">.</span>Main main1 <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">sun<span class="token punctuation">.</span>applet<span class="token punctuation">.</span>Main</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    FileInputStream file <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">FileInputStream</span><span class="token punctuation">(</span><span class="token string">"./out/production/classes/sun/applet/Main.class"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    UnDelegationClassLoader cl <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">UnDelegationClassLoader</span><span class="token punctuation">(</span><span class="token string">"./out/production/classes/"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    String name <span class="token operator">=</span> <span class="token string">"sun.applet.Main"</span><span class="token punctuation">;</span>
    Class<span class="token operator">&lt;</span><span class="token operator">?</span><span class="token operator">&gt;</span> clz <span class="token operator">=</span> cl<span class="token punctuation">.</span><span class="token function">loadClass</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>
    Object main2 <span class="token operator">=</span> clz<span class="token punctuation">.</span><span class="token function">newInstance</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    Method mainMehthod <span class="token operator">=</span> clz<span class="token punctuation">.</span><span class="token function">getMethod</span><span class="token punctuation">(</span><span class="token string">"main"</span><span class="token punctuation">,</span>String<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    String params<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">=</span> null<span class="token punctuation">;</span>
    mainMehthod<span class="token punctuation">.</span><span class="token function">invoke</span><span class="token punctuation">(</span>null<span class="token punctuation">,</span><span class="token punctuation">(</span>Object<span class="token punctuation">)</span>params<span class="token punctuation">)</span><span class="token punctuation">;</span>

    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"main1 class: "</span> <span class="token operator">+</span> main1<span class="token punctuation">.</span><span class="token function">getClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"main2 class: "</span> <span class="token operator">+</span> main2<span class="token punctuation">.</span><span class="token function">getClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"main1 classloader: "</span> <span class="token operator">+</span> main1<span class="token punctuation">.</span><span class="token function">getClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getClassLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"main2 classloader: "</span> <span class="token operator">+</span> main2<span class="token punctuation">.</span><span class="token function">getClass</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getClassLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token punctuation">}</span>

}

  • 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
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

输出:

this sun.applet.Main class defined by an ignorant programmer
main1 class: class sun.applet.Main
main2 class: class sun.applet.Main
main1 classloader: null
main2 classloader: sun.applet.UnDelegationClassLoader@36baf30c

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

注意到为了打破父委派模型, 我们重写 loadClass(String name) 方法, 在该方法中, java. 开头的类, 我们还是调用 jdk 提供的加载器去加载。因为这些核心类 jdk 做了权限保护, 如果直接尝试加载一个自定义的 java. 开头的核心类, 例如 java.lang.Object 的话, 在执行 defineClass 时会得到如下报错

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
	at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
	at sun.applet.UnDelegationClassLoader.findClass(UnDelegationClassLoader.java:36)
	at sun.applet.UnDelegationClassLoader.loadClass(UnDelegationClassLoader.java:24)
	at sun.applet.UnDelegationClassLoader.main(UnDelegationClassLoader.java:58)

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这是 jdk 对于 java. 开头的类加载的一种权限保护, 确保用户没办法错误或恶意的加载自定义的核心 java 类 。

哪些场景下需要违背父委派模型

目前有不少框架都会自行实现 classLoader 满足一些特定需求, 其中就有一些框架会在一定程度上违背父加载模型, 例如 Tomcat, JNDI、OSGi .

这里分析一下 Tomcat 什么如何违背父委派模型, 以及为什么违背

首先 tomcat 文档中描述了其自定义的类加载器层级关系:

When Tomcat is started, it creates a set of class loaders that are organized into the following parent-child relationships, where the parent class loader is above the child class loader:

 	 			Bootstrap ( $JAVA_HOME/jre/lib/ ;  $JAVA_HOME/jre/lib/ext )
     				 |
     			  System   ( $CATALINA_HOME/bin/bootstrap.jar ;$CATALINA_BASE/bin/tomcat-juli.jar or $CATALINA_HOME/bin/tomcat-juli.jar ; $CATALINA_BASE/bin/tomcat-juli.jar ;CATALINA_HOME/bin/commons-daemon.jar
     			 |
		      Common    (deafult: $CATALINA_BASE/lib)
         	 /     \
        webapp1     webapp2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Tomcat 作为一个服务器容器, 需要有能力同时运行多个 war 包, 而每个 war 包中都拥有各自的依赖 lib 库(WEB-INF/lib) 以及各自的项目代码(WEB-INF/classes), 为了保证每个 web 项目可以共同运行, 互不干扰, Tomcat 为每个项目都创建一个单独 webapp classloader, 它会负责加载对应的 web 项目下 WEB-INF/classes 的 class 文件和资源以及 WEB-INF/lib 下的jar 包中所包含的 class 文件和资源文件, 使得这些被加载的内容仅对该 web 项目可见, 对其他 web 项目不可见。

webapp class loader 违背父加载模型

在上述过程中, 每一个 webapp classloader 在加载类时, 会优先在 WEB-INF/classesWEB-INF/lib 中搜索并尝试加载, 而不是优先委托给父加载器尝试加载,

这样做的好处是它允许不同的 web 项目去重载 Tomcat 提供的 lib 包(如$CATALINA_HOME/lib/ 中的 jar 包)。

这极大程度上保证了不同 web 项目的独立性和自由度。

理解类加载机制有什么用

应该很多人会疑问, 作为普通程序员, 为什么有必要理解类的加载过程 ? 我平时又没有需求要开发自己的类加载器

这里简单举几个用处

好处一: 理解 JAR Hell / Classpath Hell 是什么

JAR Hell 是一个术语, 用于描述由 java 类加载机制特性而引发的一系列问题。

  • 问题一: jar 包对于自身依赖的表达能力缺失
    • 一个 jar 包并没有途径向 JVM 表达它自己依赖哪些其他的 jar 包。 必须有一个外部的实体负责主动的把相互依赖的 jar 包都添加到类路径下, 让 jvm 统一予以加载。 在没有构建工具的时候, 程序员需要人工根据文档, 找到相互依赖的 jar 包, 将其下载好, 并把他们添加到项目中
    • JVM 运行时, 并不会检查 jar 包中缺失的依赖, 只有当那些依赖需要被访问时, 才会直接抛出 NoClassDefFoundError
  • 问题二: 传递依赖 ( Transitive Dependency )
    • jar A 依赖 jar B , jar B 又依赖 jar C , 这种延伸可以指数级地进行, 导致项目的依赖变得庞大且难以管理
  • 问题三: 同名类的相互遮蔽(Shadowing
    • 回顾前文提到的类的父委派加载机制, 可以发现一个重要的特点, 当一个类加载器被要求加载一个类时, 它首先会查询这个类是否已经被加载过findLoadedClass( myClassName), 如果加载过, 就会直接返回
    • 这一特性的好处时提升了效率,避免重复加载。 坏处是当 classpath 中不同的 jar 包含有相同的类全局限定名时, 只有一个类文件会得到加载。 例如: 当一个项目中意外引入了多个版本的同一个类库以后, JVM 中具体加载哪一个类就取决于类加载器会优先访问到哪一个 jar 包。 这显然会导致难以排查的异常, 因为 IDE 和 生产环境的类加载顺序很有可能产生不一致。
  • 问题四: 日趋复杂的类加载模型
    • 由于程序员可以自行实现各种类加载器, 框架也可以自定义类加载器, 当一个项目中引入了很多会自行创建类加载器的框架以后, 整个项目的类加载就会混乱而难以管理

好处二: 利用类加载机制, 实现对第三方库的低侵入式 bug fix

上文提到, 全局限定名相同的类只会被同一个类加载器加载一次。 这容易引发问题, 但也可以用来实现对第三方类库的修改。

想象你引用了一个第三方 jar 包, 但是发现有一点小问题, 你希望简单地修改这个 jar 包中的某一个类。 但是这个 jar 包其他项目也在引用, 你无权或不便修改。 但你的项目又确实需要进行这种修改。

此时最为便捷的方式, 是把 jar 包中的类拷贝到你的项目中,包路径及类名和 jar 中的完全相同, 然后直接进行修改,如果类加载能直接优先加载项目源码中, 你所定义的 class 文件,而不再使用 jar 包中的那个类文件, 不是极好的吗?

以笔者使用的 gradle 构建的 spring boot 单体 jar 包为例, 由于 gradle 构建出的 spring boot 单体 jar 包中,在 BOOT-INF 文件夹下, 将项目文件 classes 目录放置与 lib 目录之前, 而 spring boot 应用启动时, 又会按照 BOOT-INF 中文件夹组织顺序去加载类文件, 这就确保了笔者可以方便的对 lib 中所引用的第三方 jar 包进行类的替换 --》 想替换或修改哪个类, 就在项目下面自定义一个包路径相同的同名类, 自由修改。 jvm 运行时, 只会加载这个我们自定义的类, 忽略 jar 包中原始的那个类。
在这里插入图片描述
在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值