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 调试的动态图如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t4ptMus9-1634894975454)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0eb0ee85f9bf4a15b1b466810512af83~tplv-k3u1fbpfcp-watermark.image?)]


  • 文字描述:
    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加载,所以可以保证一个类只会由固定的类加载器链只加载一次。

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

  3. 核心类库一致性,不被篡改
    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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值