2.双亲委派机制详细解析及原理

1,191 阅读16分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

写在前面的话:为什么要研究类加载的过程?为什么要研究双亲委派机制?

研究类加载的过程就是要知道类加载的时候使用了双亲委派机制。但仅仅知道双亲委派机制不是目的,目的是要了解为什么要使用双亲委派机制,他的原理是什么?知道双亲委派机制的逻辑思想,然后这个思想是否可以被我们借鉴,为我所用。这才是学习知识的目的。

比如:双亲委派机制,避免了类的重复加载,避免了核心类库被修改。那么,我们在做框架设计的时候,框架底层的东西是不是应该是不容被串改的,或者不可以被黑客进攻的,那么我们就可以借鉴双亲委派机制了。

再比如:双亲委派机制的实现使用了责任链设计模式,我们借此可以研究一下责任链设计模式,这样就理解了委派的原理。那么哪些场景我们可以使用责任链设计模式呢?多思考,才是学习的目的和精髓所在。

学到的东西,能用在工作中,才是王道。

一、什么是双亲委派机制

我们先来看一个案例: 打印引导类加载器, 扩展类加载器, 应用程序类加载器加载的目录

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加载以下文件 /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home**/jre/lib**/charsets.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/deploy.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/cldrdata.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/dnsns.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/jaccess.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/jfxrt.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/localedata.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/nashorn.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunec.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/zipfs.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home**/jre/lib**/javaws.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jce.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home**/jre/lib**/jfxswt.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jsse.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/management-agent.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/plugin.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/resources.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home**/lib**/ant-javafx.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home**/lib**/dt.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home**/lib**/javafx-mx.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home**/lib**/jconsole.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home**/lib**/packager.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home**/lib**/sa-jdi.jar: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home**/lib**/tools.jar: /Users/Downloads/workspace/project-all/target/classes: /Users/responsitory/org/springframework/boot/spring-boot-starter/2.2.8.RELEASE/spring-boot-starter-2.2.8.RELEASE.jar: **/Users/responsitory/**org/springframework/boot/spring-boot/2.2.8.RELEASE/spring-boot-2.2.8.RELEASE.jar: /Users/responsitory/org/springframework/spring-context/5.2.7.RELEASE/spring-context-5.2.7.RELEASE.jar: **/Users/responsitory/**org/springframework/spring-aop/5.2.7.RELEASE/spring-aop-5.2.7.RELEASE.jar: /Users/responsitory/org/springframework/spring-beans/5.2.7.RELEASE/spring-beans-5.2.7.RELEASE.jar: /Users/responsitory/org/springframework/spring-expression/5.2.7.RELEASE/spring-expression-5.2.7.RELEASE.jar: /Users/responsitory/org/springframework/boot/spring-boot-autoconfigure/2.2.8.RELEASE/spring-boot-autoconfigure-2.2.8.RELEASE.jar: /Users/responsitory/org/springframework/boot/spring-boot-starter-logging/2.2.8.RELEASE/spring-boot-starter-logging-2.2.8.RELEASE.jar: /Users/responsitory/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar: **/Users/responsitory/**ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar: /Users/responsitory/org/apache/logging/log4j/log4j-to-slf4j/2.12.1/log4j-to-slf4j-2.12.1.jar: /Users/responsitory/org/apache/logging/log4j/log4j-api/2.12.1/log4j-api-2.12.1.jar: /Users/responsitory/org/slf4j/jul-to-slf4j/1.7.30/jul-to-slf4j-1.7.30.jar: /Users/responsitory/jakarta/annotation/jakarta.annotation-api/1.3.5/jakarta.annotation-api-1.3.5.jar: /Users/responsitory/org/springframework/spring-core/5.2.7.RELEASE/spring-core-5.2.7.RELEASE.jar: /Users/responsitory/org/springframework/spring-jcl/5.2.7.RELEASE/spring-jcl-5.2.7.RELEASE.jar: /Users/responsitory/org/yaml/snakeyaml/1.25/snakeyaml-1.25.jar: /Users/responsitory/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar:

/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar

通过观察,我们发现

引导类加载器,确实只加载了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类是我自己定义的, 引导类加载器中不可能有, 加载失败,所以, 他就会去加载这个类。回去扫描/lib/jar包中有没有这个类,发现没有,于是让扩展类加载器去加载, 扩展类加载器会去扫描扩展包lib/jar/ext包,里面有没有呢? 当然也没有, 于是委托应用程序类加载器, ok, 应用程序类加载器是有的, 于是就可以加载了, 然后返回这个类。

【通过分析,我们可以得出,双亲委派机制的实现使用的是责任链设计模式。】

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

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

二、 源码分析双亲委派机制

还是从这张图说起,c++语言调用了sun.misc.Launcher.getLauncher()获取了launcher对象,Launcher类初始化的时候其构造器创建了ExtClassLoader和AppClassLoader。然后接下来调用launcher对象的getClassLoader()方法。

public ClassLoader getClassLoader() {
    return this.loader;
}

getClassLoader()返回了this.loader对象。 而loader对象是在Launcher初始化的时候进行了赋值, loadClass是AppClassLoader。

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
          	// loader的值是AppClassLoader
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
  ......
}

类加载器是如何加载类的呢?

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

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

2.1第一次向上查找

1. 从AppClassLoader加载目标类

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

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

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

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

上面都是在做权限校验, 我们看重点代码.

Launcher.AppClassLoader.loadClass(...)
  
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
  int var3 = var1.lastIndexOf(46);
  if (var3 != -1) {
    SecurityManager var4 = System.getSecurityManager();
    if (var4 != null) {
      var4.checkPackageAccess(var1.substring(0, var3));
    }
  }

  // 缓存中是否有目标路径,如果有,说明之前已经加载过,直接调动findLoadedClass()从已经加载的类中查找,找到后直接返回。
  if (this.ucp.knownToNotExist(var1)) {
    Class var5 = this.findLoadedClass(var1);
    if (var5 != null) {
      if (var2) {
        this.resolveClass(var5);
      }

      return var5;
    } else {
      throw new ClassNotFoundException(var1);
    }
  } else {
    // 缓存中没有,则调用loadClass加载类。
    return super.loadClass(var1, var2);
  }
}

看注释部分,我们知道这是双亲委派机制里的第一步,现在AppClassLoader中查找,先从已经加载过的类中查找,如果找到就直接返回, 如果没找到,则加载这个类。我们分两步来看:一部分是findLoaderClass()的源码, 另一部分是super.loadClass(...)的源码

第一步:在已加载的类中查找是否存在

if (this.ucp.knownToNotExist(var1)) {
    Class var5 = this.findLoadedClass(var1);
    if (var5 != null) {
      if (var2) {
        this.resolveClass(var5);
      }

      return var5;
    } else {
      throw new ClassNotFoundException(var1);
    }
  }

调用findLoaderClass(var1)之前先判断this.ucp.knownToNotExist(var1)在缓存中是否存在,如果存在则调用this.findLoadedClass(var1);查找。而findLoadedClass最终调用的是本地方法查找

private native final Class<?> findLoadedClass0(String name);

第二步:之前没有加载过此类,首次加载

else {
    // 缓存中没有,则调用loadClass加载类。
    return super.loadClass(var1, var2);
  }

首次加载调用了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);

2. 从ExtClassLoader中加载目标类

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

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

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

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

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

3.从BootStrapClassLoader中查找

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

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

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

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

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

2.2 类加载器向下委派加载

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

1.启动类加载器加载目标类

引导类加载器中也没有这个类, 返回null, 这里的返回空包含了两个步骤,一个是查找,没找到,二是没找到后去/lib/jar目录下加载这个类,也没有加载到。最后返回null。然后回到ExtClassLoader.loadClass(...).

2.扩展类加载器加载目标类

接下来调用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加载类里面

3.应用程序类加载器加载目标类

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

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

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

类加载的四个步骤

defindClass()这个类执行的就是类加载的过程。 也就是下图中的四个步骤:验证->准备->解析->初始化。如下图红线圈出的部分.

再看看这四个步骤:

private Class<?> defineClass(String name, Resource res) throws IOException {
    long t0 = System.nanoTime();
    int i = name.lastIndexOf('.');
    // 获取classes目录的绝对路径,如:file:/Users/用户名/workspace/demo/target/classes/
    URL url = res.getCodeSourceURL();
    if (i != -1) {
      	// 获取包名
        String pkgname = name.substring(0, i);
        // Check if package already loaded.
        Manifest man = res.getManifest();
        definePackageInternal(pkgname, man, url);
    }
    // Now read the class bytes and define the class
    java.nio.ByteBuffer bb = res.getByteBuffer();
    if (bb != null) {
        // Use (direct) ByteBuffer:
        CodeSigner[] signers = res.getCodeSigners();
        CodeSource cs = new CodeSource(url, signers);
        sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
        return defineClass(name, bb, cs);
    } else {
        byte[] b = res.getBytes();
        // must read certificates AFTER reading bytes.
        CodeSigner[] signers = res.getCodeSigners();
        CodeSource cs = new CodeSource(url, signers);
        sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
        return defineClass(name, b, 0, b.length, cs);
    }
}

这里面的核心逻辑代码都是本地方法。我们能看到的通常是一些基础的校验,比如准备阶段,解析阶段,初始化阶段都是本地方法

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
  			// 预定义类信息
        protectionDomain = preDefineClass(name, protectionDomain);
  			// 定义类源码
        String source = defineClassSourceLocation(protectionDomain);
  			// 初始化类
        Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
        // 类定义后置处理
        postDefineClass(c, protectionDomain);
        return c;
    }

这块代码了解即可。不用深入研究。

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

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

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

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

两个原因: 
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包下的类, 他会加载么? 不会, 他会让上面的类加载器加载, 当上面的类加载器加载以后, 就直接返回了, 避免了重复加载.

第三个作用:全盘委托机制。 比如Math类,里面有定义了private User user;那么user也会由AppClassLoader来加载。除非手动指定使用其他类加载器加载。也就是说,类里面调用的其他的类都会委托当前的类加载器加载。