共计 5009 个字符,预计需要花费 13 分钟才能阅读完成。
前言
明天大头菜打算讲双亲委派模型,重点关注:如何毁坏双亲委派模型,你看完后,肯定会获益匪浅哈哈哈。
广告工夫:先点赞,先珍藏,转粉不转路。
问题
大家思考一下这些问题:
- 为什么不能定义 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 为例谈双亲委派模型的毁坏
絮叨
非常感谢你能看到这里,如果感觉文章写得不错 求关注 求点赞 求分享 (对我十分十分有用)。
如果你感觉文章有待进步,我非常期待你对我的倡议,求留言。
如果你心愿看到什么内容,我非常期待你的留言。
各位的捧场和反对,是我创作的最大能源!