前言
今天大头菜打算讲双亲委派模型,重点关注:如何破坏双亲委派模型,你看完后,一定会获益匪浅哈哈哈。
广告时间:先点赞,先收藏,转粉不转路。
问题
大家思考一下这些问题:
- 为什么不能定义java.lang.Object的Java文件?
- 在多线程的情况下,类的加载为什么不会出现重复加载的情况?
- 以下代码,JVM是怎么初始化注册MySQL的驱动Driver?
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "root");
解答
以上这些问题,其实都和双亲委派模型有关,双亲委派模型,在面试中,是非常热的考察点。
首先,我们得知道什么是双亲委派模型?
简单点说,所谓的双亲委派模型,就是加载类的时候,先请求其父类加载器去加载,如果父类加载器无法加载类,再尝试自己去加载类。如果都没加载到,就抛出异常。
现在让我们回到第一个问题:为什么不能创建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方法的大概逻辑:
- 首先加锁,防止多线程的情况下,重复加载同一个类
- 当加载类的时候,先请求其父类加载器去加载类,如果父类加载器无法加载类时,才自己尝试去加载类。
上面的源码解析,可以回答问题:在多线程的情况下,类的加载为什么不会出现重复加载的情况?
好,目前我们已经解决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为例谈双亲委派模型的破坏
絮叨
非常感谢你能看到这里,如果觉得文章写得不错 求关注 求点赞 求分享 (对我非常非常有用)。
如果你觉得文章有待提高,我十分期待你对我的建议,求留言。
如果你希望看到什么内容,我十分期待你的留言。
各位的捧场和支持,是我创作的最大动力!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。