聊聊ClassLoader
1、什么是类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚 机外部实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的模块称为“类加载器”。
周志明. 深入理解Java虚拟机:JVM高级特性与最佳实践(第2版) 机械工业出版社.
2、需要注意的点
两个类是“相等”(包括equals、isAssignableFrom、isInstanceOf)的前提条件是这两个类的类加载器相等。
public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {
URL jar = new URL("file:\\G:\\code\\demo\\demo-0.0.1-SNAPSHOT.jar");
URL[] urls = new URL[]{jar};
//类加载器1
URLClassLoader classLoader1 = new URLClassLoader(urls,null);
Class userClass1 = classLoader1.loadClass("com.demo.User");
//类加载器2
URLClassLoader classLoader2 = new URLClassLoader(urls,null);
Class userClass2 = classLoader2.loadClass("com.demo.User");
//输出false,原因:userClass来自不同的类加载器
System.out.println(userClass1.equals(userClass2));
}
3、类加载器的分类
- 启动类加载器(BootstrapClassLoader):前面已经介绍过,这个类将器负责将存放在<JAVA_HOME>\lib目录中的,或 者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。
- 扩展类加载器(ExtensionClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(ApplicationClassLoader):这个类加载器由sun.misc.Launcher$App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
周志明. 深入理解Java虚拟机:JVM高级特性与最佳实践(第2版) 机械工业出版社
4、类加载器的双亲委托加载
ClassLoader的结构中有一个重要的成员变量parent,也就是我们所说的ClassLoader的双亲。
// java.lang.ClassLoader
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
...
委派ClassLoader进行类加载的过程应该是:
- 首先判断类是否已经加载,如果已经加载直接返回已加载的类
- 如果没有加载交给parent进行加载,如果加载成功返回类
- 如果parent加载失败,自己尝试加载
JDK中loadClass的过程如下:
// java.lang.ClassLoader
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 {
// 如果没有加载交给parent进行加载,如果加载成功返回类
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果parent=null时,认为parent=启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果parent加载失败,自己尝试加载
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;
}
检查与加载过程如图所示:
5、双亲委托模式的弊端
要说明弊端,必须引入SPI。
###什么是SPI
SPI ,全称为 Service Provider Interface,是一种服务发现机制。JAVA中定义的SPI一般是要第三方进行实现,我们比较常见的如:java.sql.Driver,JDK中只定义了Driver接口,并没有去实现,Driver的实现由数据库厂商来实现。
oralce数据库驱动的实现如下:(来自:ojdbc6-11.2.0.4.0.jar)
public class OracleDriver implements Driver {
...
}
同时第三方jar必须增加配置文件:
java.sql.Driver文件内容:oracle.jdbc.OracleDriver
java虚拟机通过扫描jar包下的配置文件信息加载对应接口的实现类。
SPI小示例
定义SayHello接口
package com.demo;
public interface SayHello {
void hello();
}
实现SayHello接口
package com.demo;
public class SayHelloImpl implements SayHello {
@Override
public void hello() {
System.out.println("hello");
}
}
在META-INF/services目录下增加com.demo.SayHello文件,文件内容为:com.demo.SayHelloImpl
主函数
public class ClassLoaderApplication {
public static void main(String[] args) {
ServiceLoader<SayHello> sayHellos = ServiceLoader.load(SayHello.class);
for (SayHello s : sayHellos) {
s.hello();
}
}
}
SPI引入给双亲委托模式带来的冲击
以java.sql.Driver为例,java.sql.Driver接口定义在rt.jar中,而rt.jar由BootstrapClassLoader负责加载,Driver最终由同在rt.jar包中的DriverManager类所使用,代码如下:
// class : DriverManager
private static void loadInitialDrivers() {
...
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
...
由于DriverManager类在rt.jar中,所以可以认定DriverManager类最终由BootstrapClassLoader加载器负责加载,而我们的Driver实现类(OracleDriver)一般都是由应用程序类加载器(ApplicationClassLoader)或自定义类加载器负责加载,所以Driver的实现对BootstrapClassLoader是不可见的,这样必定会导致DriverManager的loadInitialDrivers失败。
解决方案
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(ThreadContextClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
看下ServiceLoader的相关源码:
//class : ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
//ServiceLoader就是通过Thread.currentThread().getContextClassLoader()获取类加载器的
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
所以解决DriverManager类中可以加载OracleDriver的问题,可以通过将应用程序类加载器(ApplicationClassLoader)设置到java.lang.Thread类的setContextClassLoaser()方法来解决。
其实这一过程我们基本不用自己来敲代码实现,因为我们用的容器都已经帮我们考虑到了。以tomcat(9.0.24)的源码为例:
//class : WebappLoader
@Override
public void backgroundProcess() {
if (reloadable && modified()) {
try {
Thread.currentThread().setContextClassLoader
(WebappLoader.class.getClassLoader());
if (context != null) {
context.reload();
}
} finally {
if (context != null && context.getLoader() != null) {
Thread.currentThread().setContextClassLoader
(context.getLoader().getClassLoader());
}
}
}
}
6、再来聊聊Spring中的ClassLoader
我们定义的JavaBean在spring的getBean方法的创建过程其实与DriverManager创建Driver实例的过程是一样的。我们的JavaBean是一般都是由应用程序类加载器(ApplicationClassLoader)或自定义类加载器负责加载,而Spring做为一款开源框架可能是有更高层类加载器负责加载,所以Spring获取JavaBean的Class时第一优先级是通过Thread.currentThread().getContextClassLoader()来获取JavaBean的Class的类加载器。如代码所示:
//org.springframework.util.ClassUtils
public static ClassLoader getDefaultClassLoader() {
ClassLoader cl = null;
try {
cl = Thread.currentThread().getContextClassLoader();
} catch (Throwable var3) {
}
if (cl == null) {
cl = ClassUtils.class.getClassLoader();
if (cl == null) {
try {
cl = ClassLoader.getSystemClassLoader();
} catch (Throwable var2) {
}
}
}
return cl;
}
更多spring源码相关知识点击
《超哥spring源码解析之核心容器篇》免费视频学习
也可以关注超哥微信公众号: