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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值