双亲委派模型



## 前言
> 今天大头菜打算讲双亲委派模型,重点关注:如何破坏双亲委派模型,你看完后,一定会获益匪浅哈哈哈。
广告时间:先点赞,先收藏,转粉不转路。
## 问题
> 大家思考一下这些问题:
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为例谈双亲委派模型的破坏

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

#学习路径#
全部评论
今天面试就问到了怎么破坏,可惜没来得及看
点赞 回复 分享
发布于 2021-03-31 13:36

相关推荐

06-03 19:56
门头沟学院 Java
建信融通有没有约一面的?到底是不是半结构化面试?附一篇拼多多面经1.使用Redis解决集群模式下的session共享问题,是把session存在Redis里了吗?我说存的是用户信息,不是session2.那你请求传过来的是什么?key是UUID+用户id,3.那你怎么知道传过来什么呢?我说登录后返回一个token,放在请求头的authorization里4.前端是你写的吗?不是5.那你怎么通过redis&nbsp;template获取数据?就是通过redis直接去呀,根据uuid+用户id6.为什么要用随机值?就是加一个校验机制二、分布式锁1.一人一单集群下分布式锁是怎么用的?Key为用户id&nbsp;+&nbsp;业务名,自定义分布式锁,或者用的是Redisson2.怎么实现的自定义锁,自定义和Redisson有什么区别Setnx,看门狗机制、重入比较难实现,用他封装好的3.看门狗机制解决什么问题?超时释放4.反问能解决超时释放吗?能,说到了判断锁是否被持有5.如何判断锁是否被持有不知道6.都要用&nbsp;用户id吗?不是,根据业务需求来,如果是库存超卖,那应该是商品id+业务三、Rabbitmq1.我看你第二个项目说用到了rabbitmq,你对几个消息队列的中间件有什么了解,他们有什么区别?说了rabbitmq&nbsp;和&nbsp;rocketmq,说了rocket可能更加可靠2.消息队列可靠是什么意思&nbsp;?保证消息被消费,消息不丢失3.什么情景&nbsp;rocketmq能做到,rabbitmq不能做不知道四、Zset1.为什么要用zset,不用其他的数据结构我说压缩列表和跳表2.什么情况下是跳表什么情况下是压缩列表设置&nbsp;&nbsp;长度&nbsp;&nbsp;1283,为什么要从压缩列表换成跳表增删的性能4.增删性能好的数据结构很多,为什么用跳表我说相比于链表,跳表可以实现范围查询5.实现范围查询,为什么不用B+树?B+树空间太大五、MySQL1.mysql熟悉吧?还可以2.Mysql都用到了什么锁表级锁、行级锁3.什么情况用表级锁、什么情况用行级锁表结构变化才用表级锁,一般情况只用行级锁4.行级锁又会锁那几行,举例一下不知道5.事务了解吧,都有哪几种事务?开始吟唱6.它们的实现有什么不同?锁和MVCC机制,开始吟唱7.不可重复读是什么问题?开始吟唱8.在开发中,经常用读已提交是为什么?你知道吗?不太依赖事务追求性能六、JVM1.G1&nbsp;回收器知道吗?2.你了解哪些回收机制?七、计算机网络1.滑动窗口是如何进行拥塞控制的?拥塞窗口:1.慢启动,拥塞窗口从1个报文段开始,每收到一个ACK,指数增长(*2)直到达到慢启动阈值或者发生丢包(超时/重复ack)2.拥塞避免,当拥塞窗口大小大于等于&nbsp;ssthresh(慢启动阈值),转为线性增长,避免窗口过大导致网络拥塞3.拥塞处理,丢包A.超时,严重拥塞,ssthresh置为&nbsp;cwnd/2,&nbsp;cwnd(拥塞窗口)置为1,重新慢启动B.重复ack,轻微拥塞,触发快速重传/快速恢复,ssthresh置为cwnd/2,cwnd也减半后线性增长接收窗口:由接收方通过TCP头部通告,表示其剩余缓冲区大小发送窗口&nbsp;=&nbsp;min(接收窗口,拥塞窗口),发送方在任意时刻可以连续发送但尚未收到确认的数据量,由接收窗口和接收窗口共同决定,确保数据发送既不会导致网络拥塞,也不会超过接收方的处理能力。2.HTTPS对比HTTP为什么是安全的?HTTPS&nbsp;=&nbsp;HTTP+加密+身份认证+完整性保护·加密传输(防窃听),HTTP以明文传输,攻击者可以直接截获通信内容;HHTPS使用SSL/TLS协议对数据进行加密(AES、RSA算法),即使被截获也无法解密·身份验证,HTTP无法验证服务器身份,攻击者可以伪造虚假网站;HTTPS通过数字证书(CA)验证网站的真实性,浏览器会显示锁图标,点击可查看证书信息,若证书无效,会提示警告·数据完整行,HTTP数据在传输中可能被修改(如插入广告或者恶意代码),而HTTPS使用消息认证码(MAC)或者哈希校验,确保数据未被修改。&nbsp;&nbsp;原理:TLS协议会为数据生成唯一指纹,接收方校验指纹是否匹配。手撕算法1.求链表的公共节点2.合并两个有序链表
查看4道真题和解析
点赞 评论 收藏
分享
评论
3
19
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务