加载中...

双亲委派机制


类加载器

从Java虚拟机的角度上讲,其实只存在两种不同的类加载器,一是启动类加载器(Bootstrap ClassLoader),其为虚拟机的一部分,二是其他所有的类加载器。

但是从开发人员的角度上说,类加载器可以分得更为细致。

一般认为上一层加载器是下一层加载器的父加载器,因而,除了BootstrapClassLoader之外,所有的加载器都是有父加载器的。

启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部。

  • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类

  • 并不继承自ava.lang.ClassLoader,没有父加载器。

  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。

  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

扩展类加载器(Extension ClassLoader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。

  • 派生于ClassLoader类

  • 父类加载器为启动类加载器

  • java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

应用程序类加载器(系统类加载器,AppClassLoader)

  • java语言编写,由sun.misc.LaunchersAppClassLoader实现

  • 派生于ClassLoader类

  • 父类加载器为扩展类加载器

  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库

  • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载

  • 通过ClassLoader#getSystemclassLoader() 方法可以获取到该类加载器

用户自定义类加载器

主要用途:隔离加载类、修改类加载的方式、扩展加载源、防止源码泄漏

补充:获取ClassLoader的途径

  1. 获取当前ClassLoader

    clazz.getClassLoader()
  2. 获取当前线程上下文的ClassLoader

    Thread.currentThread().getContextClassLoader()
  3. 获取系统的ClassLoader

    ClassLoader.getSystemClassLoader()
  4. 获取调用者的ClassLoader

    DriverManager.getCallerClassLoader()

双亲委派机制

定义:

当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。

好处

1、因为类加载器之间有严格的层次关系,那么Java的类也随之具备了一种带优先级的层次关系

2、通过双亲委派的方式,还保证了安全性(因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Object,那么这个类是不会被随意替换的,可以避免有人自定义一个有破坏功能的java.lang.Object被加载,也就是沙箱安全机制

“父子加载器”之间的关系是继承吗?

不是!!

类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码的

双亲委派是怎么实现的?

在java.lang.ClassLoader的loadClass()方法的短短数十行之中有很清晰地描述

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
        {

            synchronized (getClassLoadingLock(name)) {

                // First, check if the class has already been loaded

                Class<?> c = findLoadedClass(name);

                if (c == null) {

                    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

                    }



                    if (c == null) {

                        // If still not found, then invoke findClass in order

                        // to find the class.

                        c = findClass(name);
                    }

                }

                if (resolve) {

                    resolveClass(c);

                }

                return c;

            }

        }

具体过程从代码不难看出:

1、先检查类是否已经被加载过

2、若没有加载则调用父加载器的loadClass()方法进行加载

3、若父加载器为空则默认使用启动类加载器作为父加载器。

4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自定义的findClass()方法进行加载。

如果我们自定义的类加载器不希望破坏双亲委派机制,那么只需要重写 ClassLoaderfindClass 方法即可,在这个方法中,我们可以自定义类的查找顺序,根据某种规则查找类。

破坏双亲委派机制

从上面可以看到,因为他的双亲委派过程都是在loadClass()方法中实现的,那么想要破坏这种机制,那么就自定义一个类加载器,重写其中的loadClass()方法,使其不进行双亲委派即可

双亲委派被破坏的例子

1、双亲委派出现之前。

由于双亲委派模型是在JDK1.2之后才被引入的,而在这之前已经有用户自定义类加载器在用了。所以,这些是没有遵守双亲委派原则的。

2、JNDI、JDBC等需要加载SPI接口实现类的情况。

这种情况是基础类型想要调用回用户的代码(而根据双亲委派机制,越基础的类由越上层的加载器进行加载) => 引入线程上下文类加载器(Thread Context ClassLoader)

3、为了实现热插拔热部署工具。

为了让代码动态生效而无需重启,实现方式时把模块连同类加载器一起换掉就实现了代码的热替换。

4、Tomcat等web容器的出现。

5、OSGI、Jigsaw等模块化技术的应用。

下面以JNDI、JDBC、Tomcat展开解释为什么要破坏双亲委派机制

JNDI,JDBC破坏双亲委派

我们日常开发中,大多数时候会通过API的方式调用Java提供的那些基础类,这些基础类时被Bootstrap加载的。但是,调用方式除了API之外,还有一种SPI的方式。

例如典型的JDBC,需要以以下的方式创建数据库链接:

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234");

原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的(即上述的DriverManager类是被Bootstrap ClassLoader加载的),原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,如mysql的mysql-connector-.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载

于是,就在JDBC中通过引入ThreadContextClassLoader(线程上下文加载器,默认情况下是AppClassLoader)的方式破坏了双亲委派原则。

Tomcat破坏双亲委派

由于Tomcat是web容器,那么一个web容器可能需要部署多个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。因而,如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。

所以,Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。

所以,为了实现隔离性,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给父类加载器加载,这和双亲委派刚好相反

参考

  1. 《深入理解Java虚拟机》周志明
  2. 我竟然被“双亲委派”给虐了 - 知乎

文章作者: DestiNation
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 DestiNation !
  目录