首页 > 双亲委派模型
头像
大头菜
编辑于 2021-04-21 17:44
+ 关注

双亲委派模型



## 前言
> 今天大头菜打算讲双亲委派模型,重点关注:如何破坏双亲委派模型,你看完后,一定会获益匪浅哈哈哈。
广告时间:先点赞,先收藏,转粉不转路。
## 问题
> 大家思考一下这些问题:
1. 为什么不能定义java.lang.Object的Java文件?
2. 在多线程的情况下,类的加载为什么不会出现重复加载的情况?
3. 以下代码,JVM是怎么初始化注册MySQL的驱动Driver?
```
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "root");
```

## 解答
> 以上这些问题,其实都和双亲委派模型有关,双亲委派模型,在面试中,是非常热的考察点。

首先,我们得知道什么是双亲委派模型?
![图片摘自网络](https://imgkr2.cn-bj.ufileos.com/6e6c936c-0da4-48f3-aa31-500def05cb7e.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=1%252BUdHq0qZRot%252FNXgxoihlgId6z8%253D&Expires=1616060814)
简单点说,所谓的双亲委派模型,就是加载类的时候,先请求其父类加载器去加载,如果父类加载器无法加载类,再尝试自己去加载类。如果都没加载到,就抛出异常。

> 现在让我们回到第一个问题:为什么不能创建java.lang.Object的Java文件?

即使我们已经定义了java.lang.Object的Java文件,但其实也无法加载,因为java.lang.Object已经被启动类加载器加载了。

你可能会问:为什么JVM要使用双亲委派模型来加载类?
> 一,性能,避免重复加载;二,安全性,避免核心类被修改。

第一点,没法好说的。说说第二点安全性吧,你试想一下。假设我现在创建一个java.lang.Object的Java文件,然后在里面植入一些病毒木马,或者写一些死循环在构造方法中。对JVM来说,这是致命的。

接下来,简单介绍一下各种类加载器:

- 启动类加载器:它不是一个Java类,是C++写的。主要负责JDK的核心类库,比如rt.jar,resource.jar等类库。启动类加载器完全是JVM自己控制的,开发人员是无法访问的。
- 扩展类加载器:是一个继承ClassLoader类的Java类,负责加载{JAVA_HOME}/jre/lib/ext/目录下的所有jar包
- 应用程序类加载器:是一个继承ClassLoader类的Java类,负载加载classpath目录下的所有jar和class文件,基本上你写的类文件,都是被应用程序类加载器加载的。

可以用以下代码,打印出三个类加载器的加载的文件。
```
public class TestEnvironment {
public static void main(String[] args) {
//启动类加载器
System.out.println("1"+System.getProperty("sun.boot.class.path"));
//扩展类加载器
System.out.println("2"+System.getProperty("java.ext.dirs"));
//应用类加载器
System.out.println("3"+System.getProperty("java.class.path"));
}
}

```

**补充一下:三个类加载器的关系,不是父子关系,是组合关系。**


接下来我们看看类加载器的加载类的方法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) {
long t0 = System.nanoTime();
try {
//这里通过是否有parent来区分启动类加载器和其他2个类加载器
if (parent != null) {
//先尝试请求父类加载器去加载类,父类加载器加载不到,再去尝试自己加载类
c = parent.loadClass(name, false);
} else {
//启动类加载器加载类,本质是调用c++的方法
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.
long t1 = System.nanoTime();
//加载类
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
```
> 总结一下loadClass方法的大概逻辑:
1. 首先加锁,防止多线程的情况下,重复加载同一个类
2. 当加载类的时候,先请求其父类加载器去加载类,如果父类加载器无法加载类时,才自己尝试去加载类。
![图片摘自网络](https://static01.imgkr.com/temp/154ad50dc8dd4b109ab5e2100c63fe66.png)

上面的源码解析,可以回答问题:在多线程的情况下,类的加载为什么不会出现重复加载的情况?

好,目前我们已经解决2个问题了。

接下来


就要开始破坏双亲委派模型了。首先声明哈,双亲委派模型,JVM并没有强制要求遵守,只是说**推荐**。

**我们来总结一下,双亲委派模型就是子类加载器调用父类加载器去加载类。那如何来破坏呢?可以使得父类加载器调用子类加载器去加载类,这便破坏了双亲委派模型。**

在讲解MySQL的驱动前,先补充一个知识点:
> Class.forName() 与 ClassLoader.loadClass() 两种类的加载方式的区别
### Class.forName()
- 实质是调用原生的forName0()方法

- 保证一个Java类被有效得加载到内存中;

- 类默认会被初始化,即执行内部的静态块代码以及保证静态属性被初始化;

- 默认会使用当前的类加载器来加载对应的类(先记住这个特点,下面会用到)
### ClassLoader.loadClass()
- 实质是启动类加载器进行加载
- 与Class.forName()不同,类不会被初始化,只有显式调用才会进行初始化。
- 类会被加载到内存中
- 提供一种灵活度,可以根据自身的需求继承ClassLoader类实现一个自定义的类加载器实现类的加载。

我们继续讲一下关于MySQL的驱动,我们列举2种情况进行对比理解:
> 第一种:不破坏双亲委派模型
```
自定义的Java类

// 1.加载数据访问驱动
Class.forName("com.mysql.jdbc.Driver");
//2.连接到数据"库"上去
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");
```

分析一下这2行代码:
第一行:进行类加载,还记得上面说过Class.forName()会使用当前的类加载器来加载对应的类。当前的类,就是用户写的Java类,用户写的Java类使用应用程序类加载器加载的。那现在问题就是应用程序类加载器是否能加载com.mysql.jdbc.Driver这个类,答案是可以的。因此这种方式加载类,是不会破坏双亲委派模型的。
第二行:就是通过遍历的方式,来获取MySQL驱动的具体连接。
> 第二种:破坏双亲委派模型
在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就直接这样就可以了:
```
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");
```

这和不破坏双亲委派模型的代码有啥区别:就是少了Class.forName("com.mysql.jdbc.Driver")这一行。
```
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}

static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
```
因为少了Class.forName(),因为就不会触发Driver的静态代码块,进而少了注册的过程。

现在,我们分析下看使用了这种spi服务的模式原本的过程是怎样的:

第一,从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
第二,加载这个类,这里肯定只能用class.forName("com.mysql.jdbc.Driver")来加载。

好了,问题来了,现在这个调用者是DriverManager,加载DriverManager在rt.jar中,rt.jar是被启动类加载器加载的。还记得上面Class.forName()会使用当前的类加载器来加载对应的类。也就是说,启动类加载器会去加载com.mysql.jdbc.Driver,但真的可以加载得到吗?很明显不可以,为什么?因为om.mysql.jdbc.Driver肯定不在<JAVA_HOME>/lib下,所以肯定启动类加载器是无法加载com.mysql.jdbc.Driver这个类。这就是双亲委派模型的局限性:父类加载器无法加载子类加载器路径中的类。

> 问题我们定位出来了,接下来该如何解决?

我们分析一下,列出来:
- 一,可以肯定com.mysql.jdbc.Driver,只能由应用程序类加载器加载。
- 二,我们需要使用启动类加载器去获取应用程序类加载器,进而通过应用程序类加载器去加载com.mysql.jdbc.Driver。

那么问题就变为了:如何让启动类加载器去获取应用程序类加载器?

为了解决上述的问题,我们需要引入一个新概念:线程上下文类加载器
> 线程上下文类加载器(ThreadContextClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。

线程上下文加载器:可以让父类加载器通过调用子类加载器去加载类。

这里得注意一下:我们之前定义的双亲委派模型是:子类加载器调用父类加载器去加载类。现在相反了,换句说,其实已经破坏了双亲委派模型。

如果你看到这里,相信你已经会解答问题3了吧。

今天就到这里结束了!明天见!

## 参考资料
《深入理解JAVA虚拟机》
低情商的大仙——以JDBC为例谈双亲委派模型的破坏

## 絮叨
非常感谢你能看到这里,如果觉得文章写得不错 求关注 求点赞 求分享 (对我非常非常有用)。
如果你觉得文章有待提高,我十分期待你对我的建议,求留言。
如果你希望看到什么内容,我十分期待你的留言。
各位的捧场和支持,是我创作的最大动力!

全部评论

(2) 回帖
加载中...
话题 回帖

推荐话题

相关热帖

近期热帖

热门推荐