ClassLoader分析(一):源码详解
一.Class文件是如何运作的
二.Classloader源码流程
1. 如何加载class
ClassLoader是调用其java.lang.ClassLoader#loadClass(String, boolean)
方法来加载class的,loadClass核心代码如下
注意: 此parent为ClassLoader的一个成员属性,而非子父类继承关系
总结: ClassLoader加载时,会优先尝试父加载器去加载(如果父加载器为null,则调用BootstrapClassLoader去加载),所有父加载器都尝试失败后才会交由当前 ClassLoader重写的findClass方法去加载
1.1 演示
我们在编写一个测试类UserService,然后另外建一个测试类,测试类的main方法中new UserService()
public static void main(String[] args) { debugLoadClass(); } public static void debugLoadClass() { UserService userService = new UserService(); } 复制代码
然后在loadClass方法的findLoadedClass和findClass处加上断点加上断点,断点条件为name!=null&&name.indexOf("UserService")!=-1
调试的动态图如下
文字描述:
UserService先由AppClassLoader重写的loadClass方法,调用父类ClassLoader的loadClass方法加载,
AppClassLoader.parent属性等于ExtClassLoader,所以交由ExtClassLoader尝试加载
ExtClassLoader的parent属性为空,所以交由BootstrapClassLoader加载
BootstrapClassLoader找不到UserService返回为null,所以交由ExtClassLoader.findClass加载
ExtClassLoader.findClass找不到UserService抛出异常ClassNotFoundException,所以交由AppClassLoader.findClass加载
AppClassLoader.findClass加载成功
疑问:
疑问: 为什么ExtClassLoader.findClass(UserService)返回为null,而AppClassLoader可以找到呢
我们在loadClass方法的findClass处加上断点,步入看看,其调试动态图如下
结论: 每个classLoader只负责加载特地路径下的class,其具体加载哪个路径下面会讲
loadClass流程图:
2. 类加载器如何初始化的
2.1 三个核心类加载器如何初始化的
首先程序启动时,会先加载BootstrapClassLoader,然后再由BootstrapClassLoader加载{@see sun.misc.Launcher}
类。由于BootstrapClassLoader对java不可见,本次只研究Launcher类, 其构造器核心代码如下
public Launcher() { // 创建ExtClassLoader,并将其parent属性置为空 Launcher.ExtClassLoader extClassLoader= Launcher.ExtClassLoader.getExtClassLoader(); // 创建AppClassLoader,并将其parent属性置为extClassLoader this.loader = Launcher.AppClassLoader.getAppClassLoader(extClassLoader); // 设置当前线程上下文的类加载器为AppClassLoader Thread.currentThread().setContextClassLoader(this.loader); ... } 复制代码
代码的数据走向如下
2.1 自定义类加载器如何初始化的
自定义类加载器一般继承了抽象类ClassLoader,所以势必会调用无参构造函数java.lang.ClassLoader#ClassLoader()
,其会将当前classLoader的parent属性置为AppClassLoader
protected ClassLoader() { /** * Launcher对象的loader属性会在调用ClassLoader无参构造函数的时候,赋值当前ClassLoader.parent属性为this.loader * {@link ClassLoader#ClassLoader()} * (this(checkCreateClassLoader(), getSystemClassLoader());) * {@link ClassLoader#getSystemClassLoader()} * (scl = sun.misc.Launcher.getLauncher().getClassLoader();) * (return scl;) */ this(checkCreateClassLoader(), getSystemClassLoader()); } 复制代码
2. 为什么要这样加载class
为什么java的loadClass方法要这么麻烦的递归去加载呢,简单点就用一个加载器不行吗?
2.1 优点:
保证了class对象的唯一性(同一条ClassLoader链下)
由于优先由parent加载,所以可以保证一个类只会由固定的类加载器链只加载一次。
保证了class对象的隔离性(同一条ClassLoader链下)
由于优先由parent加载,而parent由当前ClassLoader决定,所以如果两个不同的加载器加载同一个class,会得到两个不同的Class对象。
所以类的唯一标识是ClassLoader.id+全限定类名(PackageName+ClassName),所以如果ClassLoader.id不同,即使两个实例的全限定类名完全相同,这个两个实例也是无法强制转换的,这就是jvm的隔离性。比如:
核心类库一致性,不被篡改
jdk相关的基础核心类(java.lang包下的等等),已经由父加载器加载过了,所以子加载器不会再次加载。
同时还因为在把class文件的二进制流放到jvm方法区时必须要调用java.lang.ClassLoader#defineClass
,其为final方法,其会验证,如果name为java.xxx开头,就会报错
2.1 缺点:
强双亲委派规则,导致父加载器的实例无法使用仅子加载器才可以加载的实例
要想做到这点就得打破双亲委派机制。比如java.util.ServiceLoader
的SPI机制,BootstrapClassLoader加载了 java.sql.Driver
接口和java.sql.DriverManager
类,其中DriverManager需要获取Driver的具体实现类去做一些操作;而其具体实现类是由不同厂商mysql,orcale等提供的jar包,BootstrapClassLoader的加载路径不包括用户引入的第三方jar,只能由AppClassLoader或其他自定义类加载器加载。
解决方案: 在需要初始化Driver接口实现类的Class对象时,使用AppClassLoader或其他自定义类加载器去初始化Class.forName("Driver接口实现类的全限定类名",false,AppClassLoader或其他自定义类加载器)
AppClassLoader全局默认只有一个静态实例,可通过以下几种方式获取
1. {@link sun.misc.Launcher.getLauncher().loader} 2. {@link java.lang.ClassLoader.getSystemClassLoader()} 3. {@link Thread.currentThread().getContextClassLoader()}//可能被重置过 复制代码
ServiceLoader.next方法最终会调用java.util.ServiceLoader.LazyIterator#nextService
方法去初始化META-INF/services路径配置的目标接口的实现类
一个类只加载一次,导致同一个class多个不同版本场景(多版本jar包共存问题)无法实现
三.其他疑问
疑问: 为什么idea导入多个项目,更改后不需要install,也能调试最新的代码
现有项目jvm-clash-dotest
引入了项目jvm-utils-v1.0
,当前打开的jvm-utils也是1.0版本,我们现在打印java.class.path看看
然后我们再把当前打开的jvm-utils改成2.0,再ava.class.path看看
结论: 引入jar包的路径如果在当前项目中,java.class.path
存的将不是那个jar包在maven库中的位置,而是那个项目的编译路径/target/classes
;而每次启动,idea会重新编译相关的项目,所以无需install也能够运行最新的代码
四.总结
1. 类加载器关系
加载器 | 加载路径 | parent属性 |
---|---|---|
BootstrapClassLoader 启动类加载器 | sun.boot.class.path 核心类库rt.jar等 | 无 |
ExtClassLoader 扩展了加载器 | java.ext.dirs 扩展类库dnsns.jar等 | null 间接等于Bootstrap |
AppClassLoader 应用程序加载器 | java.class.path 当前项目工作路径、引入的jar包路径等 | ExtClassLoader |
重置了parent的加载器 | 重写findClass控制 | 传入的parent |
未重置parent的加载器 | 重写findClass控制 | AppClassLoader |
2. loadClass流程图
作者:白色小黑
链接:https://juejin.cn/post/7021010821291442190