阅读 132

ClassLoader分析(一):源码详解

一.Class文件是如何运作的

image.png

二.Classloader源码流程

1. 如何加载class

ClassLoader是调用其java.lang.ClassLoader#loadClass(String, boolean)方法来加载class的,loadClass核心代码如下

image.png 注意: 此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  调试的动态图如下
loadClass-1.1演示3高清.gif


  • 文字描述:

    1. UserService先由AppClassLoader重写的loadClass方法,调用父类ClassLoader的loadClass方法加载,

    2. AppClassLoader.parent属性等于ExtClassLoader,所以交由ExtClassLoader尝试加载

    3. ExtClassLoader的parent属性为空,所以交由BootstrapClassLoader加载

    4. BootstrapClassLoader找不到UserService返回为null,所以交由ExtClassLoader.findClass加载

    5. ExtClassLoader.findClass找不到UserService抛出异常ClassNotFoundException,所以交由AppClassLoader.findClass加载

    6. AppClassLoader.findClass加载成功


疑问:

  1. 疑问: 为什么ExtClassLoader.findClass(UserService)返回为null,而AppClassLoader可以找到呢
    我们在loadClass方法的findClass处加上断点,步入看看,其调试动态图如下

loadClass-1.1为什么ext找不到.gif

   结论: 每个classLoader只负责加载特地路径下的class,其具体加载哪个路径下面会讲


  • loadClass流程图:

image.png

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);     ... } 复制代码

代码的数据走向如下 image.png

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 优点:

  1. 保证了class对象的唯一性(同一条ClassLoader链下)

由于优先由parent加载,所以可以保证一个类只会由固定的类加载器链只加载一次。

  1. 保证了class对象的隔离性(同一条ClassLoader链下)

由于优先由parent加载,而parent由当前ClassLoader决定,所以如果两个不同的加载器加载同一个class,会得到两个不同的Class对象。
所以类的唯一标识是ClassLoader.id+全限定类名(PackageName+ClassName),所以如果ClassLoader.id不同,即使两个实例的全限定类名完全相同,这个两个实例也是无法强制转换的,这就是jvm的隔离性。比如: image.png

  1. 核心类库一致性,不被篡改

jdk相关的基础核心类(java.lang包下的等等),已经由父加载器加载过了,所以子加载器不会再次加载。
同时还因为在把class文件的二进制流放到jvm方法区时必须要调用java.lang.ClassLoader#defineClass,其为final方法,其会验证,如果name为java.xxx开头,就会报错 image.png

2.1 缺点:

  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()}//可能被重置过 复制代码

image.png ServiceLoader.next方法最终会调用java.util.ServiceLoader.LazyIterator#nextService方法去初始化META-INF/services路径配置的目标接口的实现类 image.png

  1. 一个类只加载一次,导致同一个class多个不同版本场景(多版本jar包共存问题)无法实现

三.其他疑问

  1. 疑问: 为什么idea导入多个项目,更改后不需要install,也能调试最新的代码

现有项目jvm-clash-dotest引入了项目jvm-utils-v1.0,当前打开的jvm-utils也是1.0版本,我们现在打印java.class.path看看

image.png 然后我们再把当前打开的jvm-utils改成2.0,再ava.class.path看看

image.png

image.png     结论:  引入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

image.png

2. loadClass流程图

image.png


作者:白色小黑
链接:https://juejin.cn/post/7021010821291442190


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐