参考文章:
1.双亲委派模型的破坏(JDBC例子)
https://blog.csdn.net/awake_lqh/article/details/106171219
2.面试官:说说双亲委派模型?
https://baijiahao.baidu.com/s?id=1633056679004596814&wfr=spider&for=pc
3.【JVM】浅谈双亲委派和破坏双亲委派
https://www.cnblogs.com/joemsu/p/9310226.html
在我们的面试过程中,免不了会被问到 JVM 相关的知识。其中双亲委派模型 就是一个较为经常被考察的点。下面对这个点做一个整理。
类从加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括, 加载,验证,准备,解析,初始化,使用 和 卸载 7个步奏。
加载 -》 验证 -》准备 -》解析 -》初始化 -》使用 -》卸载
加载 -》验证 -》准备 -》 初始化 -》卸载
加载 -》 验证 -》准备 -》解析 -》初始化
其中 验证-》准备-》解析 又被统称为连接,所以 类加载又可以称为 加载,连接,初始化 3个阶段。
验证阶段主要完成以下4个阶段的检验动作
1.文件格式验证
2.元数据验证
3.字节码验证
4.符号引用验证
准备阶段正式为类变量分配内存,并设置类变量的初始值的阶段,这些变量所使用的内存都将是在方法去中进行分配的。
Tips:
1.这时候进行内存分配的仅包括类变量(被 static 修饰的变量)
2.这里说的初始值 通常情况 下是 数据类型的零值,例如 public static int value = 123; 准备后的初始值为0,而不是123
有且只有 以下5种情况, 必须立即对类进行 “初始化”。(而加载,验证,准备自然需要在此次之前开始)
1.遇到 new, getstatic , putstatic 或 invokestatic 这 4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
2.使用 java.lang.reflect 包的方法对类 进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4.当虚拟机启动时,用户需要指定一个要执行的主类 (包含 main() 方法的哪个类),虚拟机会先初始化这个主类。
5.当使用 JDK 1.7 的动态语言支持时,如果一个 Java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic , REF_putStatic, REF_invoke 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
对 HotSpot 虚拟机,可以通过 -XX:+TraceClassLoading 参数观察此操作是否导致子类的加载。
在Java中任意一个类都是由 这个类本身 和 加载这个类的类加载器来确定 这个类在JVM中的唯一性。也就是你用你A类加载器加载的com.aa.ClassA 和 你B 类加载器加载的com.aa.ClassA它们是不同的,也就是用instanceof这种对比都是不同的。所以即使都来自于同一个class文件但是由不同类加载器加载的那就是两个独立的类。
Java 提供了 3种类加载器 , 启动类加载器,扩展类加载器 和 应用程序类加载器
启动类加载器 (Bootstrap ClassLoader ): 它是属于虚拟机自身的一部分,用C++实现的。 这个类加载器类 负责将存放在
扩展类加载器(Extension ClassLoader): Java 实现的,独立于虚拟机,主要负责加载
应用程序类加载器(Application ClassLoader)它是Java实现的,独立于虚拟机。主要负责加载用户类路径(classPath)上的类库,如果我们没有实现自定义的类加载器, 那 它 Application ClassLoader 就是我们程序中的默认加载器。
那么有那么多的类加载器,它们之前的层级关系是怎样的呢。它们之间是使用的双亲委派模型,如下图
如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。
这里有几个流程要注意一下:
双亲委派对保证Java 程序运行的稳定性很重要,实现却很简单,实现代码都在 java.lang.ClassLoader 的 loadClass() 方法中
/**
* Loads the class with the specified binary name. The
* default implementation of this method searches for classes in the
* following order:
*
*
*
* Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded.
*
* Invoke the {@link #loadClass(String) loadClass} method
* on the parent class loader. If the parent is null the class
* loader built-in to the virtual machine is used, instead.
*
* Invoke the {@link #findClass(String)} method to find the
* class.
*
*
*
* If the class was found using the above steps, and the
* resolve flag is true, this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting Class object.
*
*
Subclasses of ClassLoader are encouraged to override {@link
* #findClass(String)}, rather than this method.
*
* Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock getClassLoadingLock} method
* during the entire class loading process.
*
* @param name
* The binary name of the class
*
* @param resolve
* If true then resolve the class
*
* @return The resulting Class object
*
* @throws ClassNotFoundException
* If the class could not be found
*/
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 {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
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;
}
}
双亲委派模型有一系列的优势,还是需要去破坏双亲委派模型。比如 :基础类去调用回用户的代码。
具体的例子:以Driver 接口为例,由于Driver 接口定义在JDK 当中,其实现由各个数据库的服务商来提供,比如 mysql 的就写了 MySQL Connector , 那么为题来了,DriverManager(也由JDK 提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,
这里是通过 :线程上下文类加载器 Thread Context ClassLoader 实现的。这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoaser() 方法进行设置,如果 创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有进行设置过的话,那这个类加载器默认就是应用程序类加载器。
Java 中涉及SPI 的加载动作都采用这种方式,例如JNDI, JDBC,JCE,JAXB,JBI 等
双亲委派第三次被破坏是基于程序动态性导致的,如代码热替换(HotSwap), 模块热部署 (Hot Deployment)
目前OSGi 已经成了业界 事实上的 Java 模块化标准。其实先模块化热部署的关键是自定的类加载机制。每一个程序模块 (OSGi 称为 Bundle)都有一个自己的类加载器,当需要更换一个Bundle 时,就把 Bundle 连同类加载器一起替换掉以实现代码的热替换。
package thread.classLoad;
import java.sql.Connection;
/**
* Created by szh on 2020/6/15.
*/
public class TestJDBC {
public static void main(String[] args) throws Exception{
String url = "jdbc:mysql://localhost:3306/testdb";
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root");
}
}
追踪 getConnection 方法
@CallerSensitive
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
return (getConnection(url, info, Reflection.getCallerClass()));
}
追踪 getConnection 方法
// Worker method called by the public getConnection() methods.
private static Connection getConnection(
String url, java.util.Properties info, Class> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
获取线程上下为类加载器
callerCL = Thread.currentThread().getContextClassLoader();
isDriverAllowed对于mysql连接jar进行加载
isDriverAllowed(aDriver.driver, callerCL))
isDriverAllowed将传入的Thread.currentThread().getContextClassLoader();拿到的应用类加载器用去Class.forName加载我们mysql连接jar,这样子就可以加载到我们自己的mtsql连接jar
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}
DriverManager::getConnection 方法需要根据参数传进来的 url 从所有已经加载过的 Drivers 里找到一个合适的 Driver 实现类去连接数据库.
Driver 实现类在第三方 jar 里, 要用 AppClassLoader 加载. 而 DriverManager 是 rt.jar 里的类, 被 BootstrapClassLoader 加载, DriverManager 没法用 BootstrapClassLoader 去加载 Driver 实现类(不再lib下), 所以只能破坏双亲委派模型, 用它下级的 AppClassLoader 去加载 Driver.