【类加载】进阶知识点与思考
2.3进阶知识点与思考
2.3.1 可以被破坏的双亲委派模型
双亲委派模型一种有效的类加载模型,绝大多数类加载器采用这一模型实现,但是仍有一些特殊情况导致模型被破坏。
由于在双亲委派模型实现前,JDK已经提供自定义类加载的实现,导致双亲委派模型存在被破坏的机会。在使用自定义类加载器时,通过在loadClass()中编写加载逻辑实现。为了避免自定义类加载器使用上述方式,JDK在java.lang.ClassLoader中添加了findClass()方法,并要求用户重写这一方法。
双亲委派模型的第二次被破坏是由于其本身的性质。如果基础类在被加载时又要调用回用户的代码,启动类加载器将无法识别这些代码。为了解决这个问题,引入了线程上下文加载器,这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建时还未设置,他将从父线程中继承一个,如果在应用程序的全局范围内都没有设置过,那这个类加载器默认就是应用程序类加载器。这里简单介绍下JDBC中破坏双亲委派机制的相关内容。
为什么JDBC要破坏双亲委派模型?这是因为在一些场景下下父类加载器需要委托子类加载器去加载.class文件。Driver接口(DriverManager)仅仅在JDK中存在定义,而没有具体实现。具体实现由各个数据库厂商自行提供,但是不能把所有厂商提供的驱动代码全放到JDK的lib目录吧。例如如下代码:
Class clz = Class.forName("java.sql.Driver");
Driver d = (Driver) clz.newInstance();
BootstrapClassLoader将尝试加载,但是java.sql.Driver仅仅是一个接口,无法进行实例化,所以执行必然失败。如果一定执行成功,可以将mysql-connector-java.jar丢到JDK的lib目录下,但是这种方式过于硬核,扩展性和优雅性不足。DriverManager的加载可由BootstrapClassLoader实现,但是数据库厂商提供的诸如mysql-connector-.jar无法由BootstrapClassLoader加载。而通过线程上下文类加载器可实现将BootstrapClassLoader无法识别或加载的类转交AppClassLoader进行加载。大家可以自己再执行下面代码观察结果。
public static void main(String[] args){
Connection connection = null;
try {
connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "xxx", "xxxxx");
} catch (SQLException e) {
e.printStackTrace();
}
System.out.println(connection.getClass().getClassLoader());
System.out.println(Thread.currentThread().getContextClassLoader());
System.out.println(Connection.class.getClassLoader());
}
下面我们再关注一下getConnection()的实现
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
// 同步块处理
synchronized(DriverManager.class) {
// callerCL为空说明时启动类加载器,但是启动类加载器不能识别rt.jar之外的类
// 为了完成加载,可以从线程上下文中获取应用类加载器
if (callerCL == null) {
// 获取线程上下文中类加载器
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
// isDriverAllowed中实现mysql-connector-java.jar的加载
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 (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");
}
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;
}
双亲委派模型的第三次被破坏是由于动态性变态追求导致,实现动态性的好处例如代码热替换、模块热部署。动态性固然有许多好处,但是往往会在性能或其他场景上存在一定缺陷。OSGi是Java模块化标准,允许自定义类加载器,并且每一个程序模块都有一个类加载器。在JDK9之前,基础类集中在rt.jar包内。然而过度集中缺违反了单一职责原则,这将导致编译时会将很多无用的类也一并打包。JDK9中引入了模块化的思想,对rt.jar中的模块进行解耦,分为数十个模块单独管理。编译时也只引入用到的模块。下面以一段伪代码对JDK9中的加载过程进行描述:
Class<?> c = findLoadedClass(cn);
if (c == null) {
// 定位类所属模块
LoadedModule loadedModule = findLoadedModule(cn);
if (loadedModule != null) {
// 获取所属模块的类加载器
BuiltinClassLoader loader = loadedModule.loader();
// 执行类加载
c = findClassInModuleOrNull(loadedModule, cn);
} else {
// 降级执行双亲委派
if (parent != null) {
c = parent.loadClassOrNull(cn);
}
}
显然,双亲委派在JDK9中成为了一种后备加载方式。这种模块化方式应用在类加载场景更高效,也可以减少不必要的打包。这里仅简单对OSGi、模块化系统进行介绍,详细内容感兴趣的同学可以自行深入理解,例如深入理解OSGi:Equinox原理、应用与最佳实践。
2.3.2 类加载部分源码简析
本节对类加载涉及的部分源码进行分析,不涉及C/C++实现的Bootstrap层类加载器。
首先通过代码打印出各个类加载器:
public class Application {
public static void main(String[] args) {
Application app = new Application();
ClassLoader loader = app.getClass().getClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
System.out.println(ClassLoader.getSystemClassLoader());
}
}
// 代码输出
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@61bbe9ba
null
sun.misc.Launcher$AppClassLoader@18b4aac2
通过输出我们不难发现,AppClassLoader和ExtClassLoader都是类Launcher类下的静态内部类。这个Launcher类是Java程序执行的入口。启动Java应用时首
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
“挨踢”行业行情日益严峻,企业招聘的门槛也随之越来越高,大厂hc少之又少。 庞大的知识体系下,不知道学什么、怎么学? 面试高频考点是什么、怎么回答才能得到面试官的青睐? 作为后端求职者,在Java的道路上越走越宽。 本专刊则针对Java面试考点上,精讲JVM知识点,为大家的大厂求职路保驾护航! 针对如今校招痛点,深入详解JVM知识考点,列出高频真题并详细解答!探索JVM精髓!
查看8道真题和解析