1.2 双亲委派机制及其原理

时间:2022-07-25
本文章向大家介绍1.2 双亲委派机制及其原理,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

1. 类加载的过程

  1.1 类加载器初始化的过程

  1.2 类加载的过程

  1.3 类的懒加载

2. jvm核心类加载器

参考博客: https://www.cnblogs.com/ITPower/p/13197220.html

一. 双亲委派机制

1.1 什么是双亲委派机制

我们先来看一个案例:

package com.lxl.jvm;
import sun.misc.Launcher;
import java.net.URL;

public class TestJDKClassLoader {
    public static void main(String[] args) {
        System.out.println();
        System.out.println("bootstrap Loader加载一下文件:");
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i<urls.length; i++) {
            System.out.println(urls[i]);
        }

        System.out.println();
        System.out.println("extClassLoader加载以下文件");
        System.out.println(System.getProperty("java.ext.dirs"));

        System.out.println();
        System.out.println("appClassLoader加载以下文件");
        System.out.println(System.getProperty("java.class.path"));
    }
}

这是打印引导类加载器, 扩展类加载器, 应用程序类加载器加载的目录.

我们来看一下:

引导类加载器加载的文件是:Launcher.getBootstrapClassPath().getURLs()下的文件

扩展类加载器加载的文件是: java.ext.dirs , java扩展类目录

应用程学类加载器, 加载的是: java.class.path , java home路径下的所有类

我们来看一下打印结果

bootstrap Loader加载一下文件:
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/classes

extClassLoader加载以下文件
/Users/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java

appClassLoader加载以下文件

通过观察,我们发现

引导类加载器,确实只加载了java home下的/jre/lib目录下面类

扩展类加载器加载了java扩展目录里面的类

但是, 应用程序类加载器, 加载的类包含了java home下/jre/lib目录, java home扩展目录下的类, 还有responsitory仓库下的类, 还有idea的类, 还有就是我们的类路径下target的类.

问题来了, 为什么AppClassLoader加载器加载了引导类加载器和扩展类加载器要加载的类呢? 这样加载不是重复了么?

其实, 不会重复加载, appClassLoader主要加载的类就是target目录下的类, 其他目录下的类基本上不会加载. 为什么呢? 这就是下面要说的双亲委派机制.

上面这个图就是双亲委派机制的图. 什么意思呢?

比如: 我现在有一个自定义的java.lxl.jvm.Math类. 首先是由应用程序类加载器去加载java.lxl.jvm.Math类, 他要去看他已经加载的类中是否有这个类, 如果有, 就直接返回回来, 如果没有, 就委托扩展类加载器去加载. 扩展类加载器去查看已经加载的类是否有java.lxl.jvm.Math, 如果有就返回,如果没有就继续委托它的父类引导类加载器去加载. 这时候, 我们都知道, Math类是我自己定义的, 引导类加载器中不可能有, 所以, 他就会让扩展类加载器去加载, 扩展类加载器中有没有呢? 当然也没有, 于是委托应用程序类加载器, ok,应用程序类加载器是有的, 于是就可以加载, 然后返回了.

那么, 这里有一个问题, 那就是, 由应用程序类加载器首先加载, 然后最后又回到了应用程序类加载器. 绕了一圈又回来了, 这样是不是有些多此一举呢, 循环了两次? 为什么一定要从应用程序类加载器加载呢? 直接从引导类加载器加载不好么?只循环一次啊....

其实, 对于我们的项目来说, 95%的类都是我们自己写的, 因此, 而我们自己写的类是有应用程序类加载器加载. 其实,应用程序类加载器只有在第一次的时候, 才会加载两次. 以后, 当再次使用到这个类的时候, 直接去问应用程序类加载器, 有这个类么? 已经有了, 就直接返回了.

1.2 源码分析双亲委派机制

我们来看一下类加载器.类加载器主要调用的是classLoader.loadClass("com.lxl.Math") 这个方法来实现双亲委派机制的. 根据上面的分析, 我们知道, 在Launcher类初始化的时候, loadClass是AppClassLoader, 那么也就是说, 双亲委派机制的起点是AppClassLoader.

下面我们来看一下源码, 我们采用断点的方式来分析

首先, 我们在Launcher的AppClassLoader的loadClass(String var1, boolean var2) 这个方法添加一个断点, 并将其赋值为我们的com.lxl.jvm.Math类

然后运行Math的main方法,我们来看一下这个类到底是如何被加载的

启动debug调试模式, 首先进入了Launch.AppClassLoader.loadClass(....)方法

我们来具体看看这个方法的实现

上面都是在做权限校验, 我们看重点代码. 重点代码是调用了super.loadClass(var1,var2), 而这个super是谁呢? 我们来看看AppClassLoader的集成关系

在mac上按option+command+u查看集成关系图

我们看到AppClassLoader继承自URLClassLoader, 而URLClassLoader又继承了上面四个类,最终有继承一个叫做ClassLoader的类, 所有的类加载器, 最终都要继承这个ClassLoader类.

而这里调用的是super.loadClass(),我们来看看URLClassLoader中是否有loadClass()类, 看过之后发现,他没有, 最终这个super.loadClass()是继承了ClassLoader类的loadClass(....)方法

正是这个类实现了双亲委派机制, 下面我们就来看看, 他到底是怎么实现的?

当前的类加载器是AppClassLoader类加载器, 首先第一步是查找AppClassLoader中已经加载的类中,有没有这个类,

通过调用findLoadedClass(name)方法来查询已经加载的类中, 有没有com.lxl.jvm.Math类. 那么findLoadedClass(name)里面做了什么呢? 我们进去看看

我们看到, findLoaderClass(name)方法调用了自己的一个方法findLoadedClass0, 这个方法是native的, 也就是是本地方法, 使用c++实现的, 我们不能看到底部的具体实现细节了. 但是大致的逻辑就是在已经加载的类中查找有没有com.lxl.jvm.Math这个类, 如果有就返回Class类信息.

debug看到,显然是没有的, 接下来就是走到if(c == null)里面了, 这里做了什么事呢?

他判断了,当前这个类加载器的parent是否是null. 我们知道当前这个类加载是AppClassLoader, 他的parent是ExtClassLoader, 自然不是null, 所以, 就会执行里面的parent.loadClass(name, false);

也就是执行扩展类加载器的loadClass(...)方法. 我们来看看扩展类ExtClassLoader

我们发现ExtClassLoader类里面没有loadClass(...)方法, 那他没有, 肯定就是在父类里定义的了, 通过查找, 最后我们发现这个方法还是ClassLoader里的loadClass(...)方法. 于是,我们继续debug.肯定会再次走到loadClass(...)这个方法里来. 而此时, loadClass是ExtClassloader的loadClass(...)方法

果然, 又走到这个方法里面来了

继续往下执行, 首先查找ExtClassLoader中已经加载的类中,是否有java.lxl.jvm.Math类, 过程和上面是一样的. 最后调用的是本地方法.

我们知道, 这肯定是没有的了. 然后继续判断, ExtClassLoader的parent是否为空. 很显然, 他就是空啊, 因为ExtClassLoader的父类加载器是引导类加载器BootStrapClassLoader, 而引导类加载器是c++写的,所以,这里的parent为空. parent为空执行的是else中的代码

这个方法就是去引导类加载器BootstrapClassLoad中查找, 是否有这个类, 我们来看看引导类加载器里面的具体实现

我们发现, 最后具体的逻辑也是一个本地方法实现的. 我们还是猜测一下, 这就是去查找引导类加载器已经加载的类中有没有com.lxl.jvm.Math, 如果有就返回这个类, 如果没有就返回null.

很显然, 是没有的. c == null. 我们继续来看下面的代码

到此为止, 我们第一次向上查找的过程就完完事了. 用图表示就是这样

首先有应用程序类加载器加载类, 判断应用程序已加载的类中, 是否有这个类, 结果是没有, 没有则调用其父类加载器ExtClassLoader的loadClass()方法, 去扩展类加载器中查找是否有这个类, 也没有. 那么判断其父类是否为空, 确实为空, 则进入到引导类加载器中取查找是否有这个类, 最后引导类加载器中也没有, 返回null

下面来看看类加载器是如何向下委派的?

引导类加载器中也没有这个类, 返回null, 接下来调用findClass(name);查找ExtClassLoader中是否有com.lxl.jvm.Math, 我们来看看具体的实现. 首先这是谁的方法呢?是ExtClassLoader的.

进入到findClass(name)方法中, 首先看看ExtClassLoader类中是否有这个方法, 没有, 这里调用的是父类UrlClassLoader中的findClass()方法

在findClass()里面, 我们看到将路径中的.替换为/,并在后面增加了.class. 这是在干什么呢? 不就是将com.lxl.jvm.Math替换为com/lxl/jvm/Math.class么

然后去resource库中查找是否有这个路径. 没有就返回null, 有就进入到defineClass()方法.

我们想一想, 在ExtClassLoader类路径里面能找到这个类么?显然是找不到的, 因为这个类使我们自己定义的.

他们他一定执行return null.

正如我们分析, debug到了return null; 这是执行的ExtClassLoader的findClass(). 返回null, 回到AppClassLoader加载类里面

c就是null, 然后继续执行findClass(name), 这是还是进入到了URLClassPath类的findClass(name)

如上图, 此时调用的是AppClassLoader的findClass(name), 此时的resource还是空么?当然不是了, 在target目录中就有Math.class类, 找到了, 接下来执行defineClass(name,res)

defindClass这个方法是干什么的呢? 这个方法就是加载类. 类已经找到了, 接下来要做的就是将其加载进来了.

这里执行的就是我们之前说的类加载的几个步骤,如下图红线圈出的部分.

好了,具体的就不说了. 后面有时间在继续分析.

这就是双亲委派机制的源码.

那么当下一次在遇到com.lxl.jvm.Math类的时候, 我们在AppClassLoader中就已经有了, 直接就返回了.

在来看一遍双亲委派机制的流程图

1.3 为什么要有双亲委派机制?

两个原因: 
1. 沙箱安全机制, 自己写的java.lang.String.class类不会被加载, 这样便可以防止核心API库被随意修改
2. 避免类重复加载. 比如之前说的, 在AppClassLoader里面有java/jre/lib包下的类, 他会加载么? 不会, 他会让上面的类加载器加载, 当上面的类加载器加载以后, 就直接返回了, 避免了重复加载.

我们来看下面的案例

加入, 我在本地定义了一个String类, 包名是java.lang.String. 也就是是rt.jar包下的String类的包名是一样的哈.

如上图, 这是我们运行main方法, 会怎么样? 没错, 会报错

下面分析一下, 为什么会报错呢?

还是看双亲委派机制的流程, 首先由AppClassLoader类加载器加载, 看看已经加载的类中有没有java.lang.String这个类, 我们发现, 没有, 找ExtClassLoader加载, 也没有, 然后交给引导类BootStrapClassLoader加载, 结果能不能找到呢? 当然可以了. 但是这个java.lang.String是rt.jar中的类, 不是我们自定义的类, 加载了rt.jar中的java.lang.String类以后, 去找main 方法, 没找到.....结果就跑出了找不到main方法异常.

所以说, 如果我们自己定义的时候, 想要重新定义一个系统加载的类, 比如String.class, 可能么? 不可能, 因为自己定义的类根本不会被加载

这就是双亲委派机制的第一个作用: 沙箱安全机制, 自己写的java.lang.String.class类不会被加载, 这样便可以防止核心API库被随意修改

双亲委派机制还有一个好处: 避免类重复加载. 比如之前说的, 在AppClassLoader里面有java/jre/lib包下的类, 他会加载么? 不会, 他会让上面的类加载器加载, 当上面的类加载器加载以后, 就直接返回了, 避免了重复加载.