浅析java双亲委派机制及其作用及代码示例

  首先我们了解下什么是双亲委派机制?当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

  Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且,加载某个类的class文件时,Java虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式

  其工作原理:

(1)如果一个类加载器收到了类加载请求,它并不会自己先加载,而是把这个请求委托给父类的加载器去执行

(2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的引导类加载器;

(3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制

(4)父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常

一、知识准备:类加载器的类别

  在介绍双亲委派机制的时候,不得不提ClassLoader(类加载器)。说ClassLoader之前,我们得先了解下Java的基本知识。

  Java是运行在Java的虚拟机(JVM)中的,但是它是如何运行在JVM中了呢?我们在IDE中编写的Java源代码被编译器编译成.class的字节码文件。然后由我们的ClassLoader负责将这些class文件给加载到JVM中去执行。 

(1)我们定义的类,如果我们要在编码中用到这个类,首先就是要先把“*.java”这个文件编译成class文件,然后由对应的“类加载器”加载到JVM中,我们才能够使用这个“类对象”。
(2)一般的场景下,类的加载是在我们程序启动的时候由jvm来完成,但是有些场景可能需要我们手动去指定加载某个类或找到某个类,这时候就要用到 Class.forName(String className) 加载/找到 这个className对应的类。
(3)如果比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这个两个类就必定不相等。

  JVM中提供了三层的ClassLoader:

1、BootstrapClassLoader(启动类加载器)

  Bootstrap classLoader主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。JDK自带的一款类加载器,用于加载JDK内部的类。Bootstrap类加载器用于加载JDK中$JAVA_HOME/jre/lib下面的那些类,比如rt.jar包里面的类。

  c++编写,加载java核心库 java.*,构造ExtClassLoaderAppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

2、ExtClassLoader (标准扩展类加载器)

  ExtClassLoader主要负责加载 jre/lib/ext 目录下的一些扩展的jar。主要用于加载JDK扩展包里的类。一般$JAVA_HOME/lib/ext下面的包都是通过这个类加载器加载的,这个包下面的类基本上是以javax开头的。

  java编写,加载扩展库,如classpath中的jrejavax.*或者java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。

3、AppClassLoader(系统类加载器)

  AppClassLoader:主要负责加载应用程序的主函数类。用来加载开发人员自己平时写的应用代码的类的,加载存放在classpath路径下的那些应用程序级别的类的。

  java编写,加载程序所在的目录,如user.dir所在的位置的class

  其实还有一种类加载器,就是下面第4种。

4、CustomClassLoader(用户自定义类加载器)

  java编写,用户自定义的类加载器,可加载指定路径的class文件

5、为什么应该叫做“父委派模型”,而不是“双亲委派机制”

  这是个很蛋疼的翻译问题,实际上在oracle官方文档上,人家是这样描述的:

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平台通过委派模型去加载类。每个类加载器都有一个父加载器。当需要加载类时,会优先委派当前所在的类的加载器的父加载器去加载这个类。如果父加载器无法加载到这个类时,再尝试在当前所在的类的加载器中加载这个类。(所以,java的类加载机制应该叫做“父委派模型”,不应该叫做“双亲委派机制”,“双亲委派机制”这个名字太具有误导性了。)

二、双亲委派机制

  如果有一个我们写的Hello.java编译成的Hello.class文件,它是如何被加载到JVM中的呢?别着急,请继续往下看。

1、源码分析

  打开AndroidStudio,搜索“ClassLoader”,然后打开“java.lang”包下的ClassLoader类,然后将代码翻到loadClass方法:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查这个classsh是否已经加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // c==null表示没有加载,如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //如果父类的加载器为空 则说明递归到bootStrapClassloader了
                        //bootStrapClassloader比较特殊无法通过get获取
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {}
                if (c == null) {
                    //如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

  其实这段代码已经很好的解释了双亲委派机制,下面这张图来描述一下上面这段代码的流程:

  从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。

1、不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。

2、父类中同理也会先检查自己是否已经加载过,如果没有再往上。

  注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。

3、直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层。

4、如果没有任何加载器能加载,就会抛出ClassNotFoundException

5、“父委派模型”是怎么工作的?

  举个例子,当前有个Test.class,需要加载rt.jar中的java.lang.String,那么加载的流程如下图所示,整体的加载流程是向上委托父加载器完成的。

  如果整个链路中,父加载器都没有加载这个类,且无法加载这个类时,才会由Test.class所在的加载器去加载某个类(例如希望加载开发人员自定义的类 Test2.class)。

三、为什么要设计这种机制

  这种设计有个好处是,如果有人想替换系统级别的类:String.java,篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。

“父委派模型”保证了系统级别的类的安全性,使一些基础类不会受到开发人员“定制化”的破坏。

如果没有使用父委派模型,而是由各个类加载器自行加载的话,如果开发人员自己编写了一个称为java.lang.String的类,并放在程序的ClassPath中,那系统将会出现多个不同的String类, Java类型体系中最基础的行为就无法保证。应用程序也将会变得一片混乱。

  双亲委派机制的作用:

1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

  有人总结了一张脑图如下:

四、代码示例

1、举例 1 :我自己建立一个 java.lang.String 类,写上 static 代码块

package java.lang;

public class String {
    static{
        System.out.println("我是自定义的String类的静态代码块");
    }
}

  在另外的程序中加载 String 类,看看加载的 String 类是 JDK 自带的 String 类,还是我们自己编写的 String 类

public class StringTest {

    public static void main(String[] args) {
        java.lang.String str = new java.lang.String();
        System.out.println("hello,atguigu.com");

        StringTest test = new StringTest();
        System.out.println(test.getClass().getClassLoader());
    }
}

  程序并没有输出我们静态代码块中的内容,可见仍然加载的是 JDK 自带的 String 类

  为什么呢?由于我们定义的String类本应用系统类加载器,但它并不会自己先加载,而是把这个请求委托给父类的加载器去执行,到了扩展类加载器发现String类不归自己管,再委托给父类加载器(引导类加载器),这时发现是java.lang包,这事就归引导类加载器管,所以加载的是 JDK 自带的 String 类

2、举例 2 :

  在我们自己的 String 类中整个 main() 方法

package java.lang;

public class String {
    static{
        System.out.println("我是自定义的String类的静态代码块");
    }
    //错误: 在类 java.lang.String 中找不到 main 方法
    public static void main(String[] args) {
        System.out.println("hello,String");
    }
}

  由于双亲委派机制找到的是 JDK 自带的 String 类,但在引导类加载器的核心类库API里的 String 类中并没有 main() 方法

3、举例 3:

  在 java.lang 包下整个 ShkStart 类 (自定义类名)

package java.lang;

public class ShkStart {
    public static void main(String[] args) {
        System.out.println("hello!");
    }
}

  会报错,出于保护机制,java.lang 包下不允许我们自定义类

  通过上面的例子,我们可以知道,双亲机制可以

(1)避免类的重复加载

(2)保护程序安全,防止核心API被随意篡改

  • 自定义类:java.lang.String (没用)
  • 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)

参考文档:

Java类加载机制:双亲委派机制,还是应该叫做“父委派模型”?

posted @ 2021-02-23 23:01  古兰精  阅读(3175)  评论(0编辑  收藏  举报