简介
本次介绍类加载器,内容较多,包含双亲委派及代码分析,spi等,希望大家学得开心,学得愉快
配套视频讲解:www.bilibili.com/video/BV173… (更新可能会迟到但不会缺席)
初步认识
类加载器作用
我们知道了,java代码运行要先编译成class字节码,再被加载到jvm中,那是被什么”东西“加载的呢?
这个”东西“就是类加载器
类加载器分类
jdk提供的类加载器有
- 启动类加载器(Bootstrap ClassLoader)
- 扩展类加载器(Extension ClassLoader)
- 应用类加载器(Application ClassLoader)
类加载器的分工
分工:其实就是每个类加载器只加载某个路径下的内容
应用程序类加载器:加载的是classpath底下的所有class文件。详细见下图
扩展类加载器: 加载的是jre/lib/ext/
启动类加载器 加载的是/jre/lib/rt.jar
双亲委派
聊到类加载器,就不得不提“双亲委派机制”,双亲委派这个词,听起来有些晦涩难懂,其实很简单。可以理解为,类加载器之间的协作模式,结合图理解以下的文字。
如果现在需要加载某个类(例如代码中写 new Test(),那就要去加载Test类了)
- 应用程序到自己的缓存里找,如果找到了,直接返回(结束),如果没找到就让扩展类加载器找
(假设现在应用程序类加载器没找到,所以就到了扩展类加载器)
- 扩展类加载器同样会到自己的缓存里找,如果能找到直接返回,找不到就让启动类加载器找
(假设现在扩展类加载器没找到,所以就到了启动类加载器)
- 启动类加载器同样会在缓存里找,找到了就返回,找不到,则到自己负责的路径下找(/jre/li/rt.jar),找到了就返回,找不到就又把”锅“甩给扩展类加载器
4.扩展类加载器同样会到自己负责的路径下找(jre/lib/ext/),找不到把锅继续甩回给应用程序类加载器
- 应用类加载器会在classpath下找,如果找不到就抛出ClassNotFound异常
贴士: 类加载器中的父子关系是逻辑上的父子,而不是代码里的extend
为什么要双亲委派
安全,安全,还是安全。
代码千万行,安全第一条。
试想如果你自己定义了一个java.lang.String的类,放到了classpath下,如果直接被应用类加载器加载到内存就是件很危险的事,因为这意味着你可以通过这种方式代替jdk原本的String类。然后从中做点手脚比如,(往你的银行卡转钱)
看看源码
我们通过窥探源码到方式进一步,对双亲委派机制做一个深入了解。接下来我把关键位置代码截出来做解释。请结合上半部分的理论内容理解
c++代码
因为java运行在jvm上,为了接近问题的源头所以看一点c++部分的代码
loandMainCLass:看名字就猜到了,它就是加载我们java代码中main方法所在那个的类
(java.c文件)
进入loandMainCLass后,可以看到调用了GetStaticMethodID方法且传了checkAndLoadMain参数,这是一个分水岭,从这之后就进入到java代码中了
多说一句: GetStaticMethodID获取静态函数main的id
java部分
scloader的类型就是AppClassLoader(应用类加载器)
我们先看一眼AppClassLoader
它继承了UrlClassLoader
UrlClassLoader继承了SecureClassLoader
SecureClassLoader继承了ClassLoader
梳理一下
回到刚刚的loadClass,AppClassLoader,URLClassLoader,SecureClassLoader,都没对loadClass重写,所以此时调用的是 ClassLoader中的loadClass
我们看一眼loadClass
调用了LoadClass(name,false),这个方法被AppClassLoader重写过,所以调的是AppClassLoader重点LoadClass
这个方法最终又调用了父类的LoadClass,也就是ClassLoader。
下面是ClassLoader中的LoadClass方法,我个人认为下面这段代码写的特别的精炼。我们一起来膜拜下大神的思维
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
复制代码
findLoadedClass:查找被加载过的类,因为当前没有加载过,所以这里一定是null
让“父”加载器加载,可以看到父加载器的类型就是ExtClassLoader(扩展类加载器)
我们看一眼ExtClassLoader的代码
它的继承关系跟AppClassLoader很像
回到parent.loadClass,因为ExtClassLoader也继承的是ClassLoader,所以最终又执行到了这个地方,但注意此时的this是ExtClassLoader
很显然,ExtClassLoader的findLoadedClass也是null,所以就会走到findBootstrapClassOrNull
贴士: ExtClassLoader的父加载器是BootStrap写在c++中,从下图中可以看到findBootstrapClassOrNull最终调用了native方法
此时要加载的类是我们自己写的,应由应用类加载器进行加载,所以findBootstrapClassOrNull会抛出ClassNotFoundException的异常
异常捕获后,ExtClassloader尝试在自己负责的目录下加载类,当然也加载不到最终也会抛出异常
应用类加载器捕获异常后同样会尝试自己加载类(从classpath下),因为我们的类在classpath下存在,所以能够被加载,否则抛出ClassNotFoundException异常
到这里,双亲委派的代码基本就差不多了。我们再看一眼 parent是怎么赋值的
回到最初的起点
看一眼scloader定义的地方,
再看getSystemClassLoader
里面有行sun.misc.Launcher.getLauncher();
看构造函数
ExtClassLoader与AppClassLoader就在这创建了
从这就可以看到ExtClassLoader的parent是null
AppClassLoader的parent是ExtClassLoader
到这重要环节的代码就差不多了
spi
绕开双亲委派
聊SPI之前,我们了解一个机制,当我们的类A用到了另一个类B,假设这个类B之前没被加载过,那么此时就要加载类B,加载类B就要用到类加载器,那么用哪一个加载器呢?其实就是用类A的加载器。
那么这就会产生一个问题(以jdbc举例)
driver在rt.jar下由启动类加载,但各个厂商提供的数据库驱动实现在classpath下,启动类加载器加载不到
解决这个问题就可以用spi来绕过双亲委派,我们看一眼源码,其实就是获取了线程上下文类加载器
spi演示
简单的演示下spi怎么用,不做具体解释了(因为不是这篇文章的重点)
随便写一个接口
随便写一个实现类
在这个目录下新建文件,名字是接口的全限定类名,内容是实现类的全限定类名。
随便测试一下
本次就到这,再见~。
近期评论