双亲委派机制

双亲委派机制指的是当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载。

由于Java虚拟机中有多个类加载器,双亲委派机制的核心是解决一个类到底由谁加载的问题。

从描述上的自底向上查找,由顶向下加载,我们意识到类加载器是有层级结构的,为什么是层级结构排序呢?

因为类加载器之间有父子关系。

向上查找的过程

假如有一个A类要去进行加载,首先类加载器需要检查A类是否被加载过,若应用程序类加载器没有查看到加载过,则把任务委派给自己的父类,也就是扩展类加载器检查是否被加载过,若还没有,则给启动类加载器检查。若在检查过程中发现该类已经被加载过了,则对应加载器直接返回目标class对象。

若所有类加载器都没加载过,在A类需要进行加载时,就会先检查A类是否在启动类加载器的加载路径中,若不在,则无法加载,启动类加载器将任务委派给子类加载器扩展类加载器进行检查加载,这样一直到应用程序类加载器。

这两个加载机制的结合,能防止类加载器重复加载一个类。

相关问题

如果一个类重复出现在三个类加载器的加载位置,应该由谁来加载?

启动类加载器加载,根据双亲委派机制可以知道,该类会先在启动类加载器进行加载。

在自己的项目去创建一个java.lang.string类,会被加载吗?

不能,会返回启动类加载器加载在rt.jar包中的String类。

如何使用代码的方式主动加载一个类呢?

1.使用class.forName方法,使用当前类的类加载器去加载指定的类。

ClassLoader classLoader = DEMO1.class.getClassLoader();

System.out.println(classLoader);

2.获取类加载器,通过类加载器的loadClass方法指定某一个类加载器的加载

Class clazz = classLoader.loadClass("com.my.A");

System.out.println(clazz.getClassLoader());

特别注意:加载器父类并不是extends关系,而是在实现类加载器中保存了一个成员变量parent。

为什么扩展类加载器的parent为null呢?

因为启动类加载器一般加载重要的必须类,为了保障安全性,不提供获取方法。

相关面试题

说说类的双亲委派机制。

  1.当一个类加载器去加载某个类时,它就会自底向上查找是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载。

  2.应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器。

  3.双亲委派机制的好处。

    3.1.避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。

    3.2.避免一个类重复性地被加载,提升加载的性能。

打破双亲委派机制

1.自定义类加载器

自定义类加载器并且重写loadClass方法,就可以将双亲委派机制的代码去除。Tomcat通过这种方式实现应用之间的类隔离。

一个TomCat程序中是可以运行多个web应用的,如果这两个应用中出现了相同限定名的类,比如Servlet类,Tomcat要保证这两个类都能加载并且它们是不同的类。

如果不打破双亲委派机制,当应用类加载器加载Web应用1中的MyServlet之后,web应用2中相同限定名的MyServlet类就无法被加载了。

所以,如果不打破双亲委派机制的话,Tomcat就无法加载多应用的相同名类。

Tomcat解决方法:底层为每一个应用都生成了一个单独的类加载器,这些加载器都有特点:加载时不再走向上委派,向下加载。各个应用的类加载器都只加载自己应用,实现应用之间类的隔离。

那么我们如何实现自定义类加载器呢?

首先我们需要分析类加载器的实现

ClassLoader包括4个核心类

而双亲委派机制的核心代码就位于loadClass中

查看loadClass源码:

protected Class loadClass(String name,boolean resolve) throws ClassNotFoundException

{ //在多线程下防止类重复加载

synchronized (getClassLoadingLock(name))

{ //根据权限命名查看该类是否被加载过,有就直接返回class

Class c = findLoadingClass(name);

if (c == null)

{

long t0 = System.nanoTime();

try{

//判断它的父类加载器是否为空

if(parent != null)

{ //如果不为空,则由父类调用loadClass方法,起到向上委派作用

c = parent.loadClass(name,false);

}else {

//如果parent为空,说明到了拓展类加载器,它底层调用native本地方法,也就是C++代码实现

c = findBootstrapClassOrNull(name);

}

} catch(ClassNotFountException e)

{

}

}

// 到了这一步,如果c还是null,说明父类加载器都没有加载成功,需要用到当前类加载器进行加载

if (c == null)

{

long t1 = System.nanoTime();

c = findClass(name);

}

}

}

分析findClass方法:

我们看到,findClass方法其实只是抛出一个异常。

protect Class findClass(String name) throws ClassNotFoundException{

throw new ClassNotFoundException(name);

}

因为当前类ClassLoader是抽象类,这个findClass由子类实现的。

例如URLClassLoader,这个子类就是重写了findClass方法,核心就是获取某一个目录下的class字节码文件,把它的文件对象获取,这样就可以获取相应的二进制数据。

最后他会调用defineClass方法,将二进制数据传进来,作调用工作并调用底层方法,将信息保存到方法区和堆上:

findClass源码:

defineClass源码:

介绍这几个类的原因是,双亲委派机制的核心代码就是它们。

所以打破双亲委派机制,就是把上面的代码重新实现,例如:

且自己实现时要注意,在return defineClass在方法区和堆区创建对象时,会首先在ClassLoader进行校验,要是加载的类名以Java.开头,会抛出安全性异常,它认为java.前缀的文件必须以启动类加载器进行加载,不能由Java中的类加载器加载。这是安全性保护。

而若是不用java.前缀的,用自己的文件,它会报系统找不到Object.class类错误。

解决方法:

可以把对应的Object类拖动,也可以在自己的代码中假如判断,如果类的权限命名以Java.开头,那一块就交给父类来加载,这样Object类的加载就交给父类加载器进行加载,我们就不用处理Object.class了。

之后我们运行就看到,类使用我们自定义的类加载器进行加载,我们打破了双亲委派机制。

派生问题:

自定义类加载器的父类加载器是谁呢?

我们在创建自定义类加载器时并没有指定父类加载器,而我们在getParent()时会获取应用程序类加载器,为什么呢?

这时我们看向源码:

以jdk8为例,ClassLoader类中提供了构造方法设置parent的内容。

这个构造方法由另一个构造方法调用,其中父类加载器由getSystemClassLoader方法设置,该方法返回的是AppClassLoader。

两个自定义类加载器加载相同限定名的类,会不会冲突?

不会冲突,在同一个Java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类。

下面代码展示两个不同类加载器加载相同的类限定名字节码文件,发现两个Class对象是不相同的。

如果我们只是实现一个自定义加载器,并拓展加载渠道,其实是不应该打破双亲委派机制的,正确做法是重写findClass方法。

在前面学习中我们知道,loadClass是实现双亲委派机制的方法,而findClass才是拓展加载渠道的方法。

例如我们要在数据库中加载字节码文件,在findClass中就要获取数据库中的数据,写到内存中,变成一个二进制字节数组,再把它传入到defineClass方法中,这样就完成了类的加载。

2.线程上下文类加载器

此技术被大量用在Java自己的技术上,比如JDBC和JNDI等。

JDBC目的是在Java中操作数据库,但它的核心思想是不希望有任何数据库语言,以此来提高泛用性。

设计思路:维护了一个DriverManager驱动类,用来管理jar包数据库的驱动。

例如我们需要用mysql,DriverManager会把这个jar包引入,这样就可以连接mysql数据库,其他数据库也一样。

这样让用户自行引入数据库依赖,JDBC实现在Java中对数据库的管理。

而在DriverManager里加载数据库jar包时,就打破了双亲委派机制。

我们发现,DriverManager就是JDK自己提供的,位于rt.jar包中,由启动类加载器加载。

而数据库驱动对应的类就是应用类加载器加载:

而从流程上看,DriverManager属于rt.jar是启动类加载器加载的,但用户jar包的驱动是由启动类加载器委派应用程序类加载器进行加载。这就打破了双亲委派机制。

但这里有个问题:DriverManager在底层rt.jar,被启动类加载器加载,它是怎么知道用户引入的jar包中要加载的驱动在哪里的呢?

这个技术使用到SPI机制:是JDK内置的一种服务提供发现机制。SPI可以快速找到接口的实现类对象,这让我们想到了spring的依赖注入。

SPI工作原理:

首先,mysql下的jar包需要暴露驱动给DriverManager使用。这时候需要在固定层级文件夹上被发现。所以,引出SPI工作原理的第一点:

1.在ClassPath路径下的MRTA-INF/services文件夹中,以接口的全限定名来命名文件名,对应的文件里面写该接口的实现。

以mysql为例子,该包在com.mysql.cj.jdbc,同时它也实现了Java.sql.Driver,在静态代码块里,它把自己的类注册进了DriverManager里。

所以只要该类被注册加载了,就完成了注册驱动的过程。

之后在文件上就需要写我们需要暴露出去的接口的实现类,例如现版本mysql驱动是com.mysql.cj.jdbc。

之后,引出第二点:2.使用ServiceLoader加载实现类。

在驱动jar包中,我们需要暴露需要加载的类:在现版本com.mysql.cj.jdbc,把类名写在java.sql.Driver.文件中,把文件放在META-INF/services/java.sql.Driver固定位置等待扫描。之后再DriverManager代码中就使用ServiceLoader。

ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);进行加载Driver,这就是基于mysql的Driver加载。

接下来我们在代码层面进行分析:

Connection conn  = DriverManager.getConnection(URL,User,Password);

使用到的DriverManager:jvm会加载该类,加载完之后发现需要调用它的静态方法,就需要进行初始化。下面为静态代码。

该方法会加载jar包中的所有的驱动,查看源码:

我们拿到Driver的加载器之后,他就会遍历所有jar包满足条件的类名。

只要迭代器能找到相应的类名,迭代器就会返回该类。

打断点就可以查看到该类名。

我们发现,该对象还存了个loader,里面就写了该Driver类是appClassLoader加载,也就是应用程序类加载器。

引出新问题:SPI是如何获取到这个应用程序类加载器的呢?

spi使用了线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器。

我们进行验证:一个线程的默认类加载器是什么?

我们new一个线程并打印它当前的类加载器,发现是应用程序类加载器。

说明线程上下文中保存的类加载器默认是应用程序类加载器,我们以后想要去获取相关加载器时可以使用这种方法进行获取。

也可以设置自定义类加载器。

总结:

但这样真的是打破了双亲委派机制吗?

我们在上面的学习中可以看到,这个例子中有两个类被加载了,一个是DriverManager,一个是jar包中的mysql驱动。

首先看DriverManager,它在rt.jar,是启动类加载器进行加载的:自顶向上加载,满足双亲委派机制。

在jar包中mysql驱动位于classPath,在应用程序类加载器进行加载,这也是符合双亲委派机制的,因为它先向上一层层查找,再向下委派,由于驱动还未被任何类加载器加载过,所以会自顶向下先用启动类加载器加载,发现目录不匹配,再用扩展类加载器也加载不了,最后在应用程序类记载其被加载,也符合双亲委派机制。且不管是什么类加载,它都使用了jdk自带的类加载器,而这些类加载器都没有重写classLoader方法,也就是没有打破双亲委派机制。

为什么说它打破了双亲委派机制?

在周志明《深入理解Java虚拟机》中写道,jdbc这种从启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制。

也就是:在启动类加载器加载类时,使用线程上下文获取应用程序类加载器,委派它加载驱动类。

作者个人见解

认为打破了双亲委派机制的更倾向根据实际过程进行分析,得出打破了双亲委派机制的结果。

而认为没有打破双亲委派机制的人更倾向于源码理论,认为没有重写classLoader,也就没有打破双亲委派机制的说法。

本文基于作者自身的学习总结。如有错误,恳请指出。 如果对您有帮助的话,请给我点个赞吧。作者在后面也会分享文章,要是感兴趣也可以给我点个关注。

推荐链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: