ClassLoader笔记

大致介绍

我们都知道Java代码会被Java编译器编译成class文件,在class文件中描述了该类的各种信息,class类最终需要被加载到JVM中才能运行和使用(由Java解释器执行)。

类的生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。

加载的来源有以下部分:
1、本地磁盘
2、网络下载的.class文件
3、war、jar下加载.class文件
4、从专门的数据库中读取.class文件(少见)
5、将java源文件动态编译成class文件,典型的就是动态代理,通过运行时生成class文件

加载的过程就是通过类加载器 ClassLoader 实现的。ClassLoader 是Java的核心组件,所有的Class都是由 ClassLoader 进行加载的,ClassLoader 负责将class文件中的二进制数据流读入JVM内部,转换为一个与目标类对应的 java.lang.Class 对象实例。然后交给JVM进行后续的生命周期操作。

我们在代码中使用Class.forName(name)时就是使用了类加载器。

3个类加载器

系统定义的类加载器有3个,按下面顺序前一个是后一个的父加载器:

一、启动类加载器(底层使用C++实现)
它用来加载Java的核心库(JVM系统参数sun.boot.class.path 路径下的内容),用于提供JVM自身需要的类,只加载包名为java、javax、sun等开头的类
用户未自己主动设置该属性时,用代码System.getProperties("sun.boot.class.path")可以得到它的默认值:

1
2
3
4
5
6
7
8
9
D:\Development\Java\jdk1.8.0_131/jre/lib/resources.jar
D:\Development\Java\jdk1.8.0_131/jre/lib/rt.jar
D:\Development\Java\jdk1.8.0_131/jre/lib/sunrsasign.jar
D:\Development\Java\jdk1.8.0_131/jre/lib/jsse.jar
D:\Development\Java\jdk1.8.0_131/jre/lib/jce.jar
D:\Development\Java\jdk1.8.0_131/jre/lib/charsets.jar
D:\Development\Java\jdk1.8.0_131/jre/lib/jfr.jar
D:\Development\Java\jdk1.8.0_131/jre/classes
-- "D:\Development\Java\jdk1.8.0_131"其实就是我们设置的JAVA_HOME环境变量

二、扩展类加载器(底层使用java实现,是ClassLoader的子类,位置sun.misc.Launcher$ExtClassLoader)
从 JVM系统参数java.ext.dirs 所指定的目录中加载类库,或从JDK的安装目录的 jre/lib/ext 下加载类库。如果用户创建的JAR放在此目录下,也会自动由该类加载器加载

三、应用程序类加载器(底层使用java实现,是ClassLoader的子类,位置sun.misc.Launcher$AppClassLoader)
它负责加载 环境变量classpath 或 JVM系统参数java.class.path 指定路径下的类库。该类加载器默认是“系统类加载器”,通过ClassLoader.getSystemClassLoader()方法可以获取到该类加载器。它也是用户自定义类加载器的默认父加载器

参考博客:根据配置 CLASSPATH 彻底弄懂 AppCLassLoader(应用程序类加载器)的加载路径问题: https://blog.csdn.net/romantic_jie/article/details/107859901

在 sun.misc.Launcher 类中有初始化扩展类加载器和应用程序类加载器的源码。博客:https://www.pudn.com/news/6286507cb305d84a4f6e8596.html 搜索关键字Launcher

类加载器的其他概念

双亲委派模型,就是指一个类接收到类加载请求后,会把这个请求依次传递给父类加载器(如果还有的话),如果顶层的父类加载器可以加载,就成功返回,如果无法加载,再依次给子加载器去加载。

线程上下文的类加载器,用Thread.currentThread().getContextClassLoader()方法获得,在应用程序类加载器的初始化过程中,将该应用程序类加载器设置为线程上下文的类加载器。

线程上下文的类加载器的作用:
双亲委派模型并不是绝对的,spi机制就可以打破双亲委派模型。

什么是spi?spi(Service Provider Interface)是一种服务发现机制,Java在核心库中定义了许多接口,并且针对这些接口给出调用逻辑,但是并未给出具体的实现。开发者要做的就是定制一个实现类,在 META-INF/services 中注册实现类信息,以供核心类库使用。

最典型的例子就是 JDBC 的实现方式。Java 提供了一个 Driver 接口用于驱动各个厂商的数据库连接,Driver 接口位于 JAVA_HOME 中 jre/lib/rt.jar 中,应该由启动类加载器进行加载。根据类加载机制,当被加载的类引用了另外一个类的时候,虚拟机就会使用加载该类的类加载器加载被引用的类,因此如果其他数据库厂商定制了 Driver 的实现类之后,按理说也得把这个实现类放到启动类加载器加载的目录下,这显然是很不合理的。

于是Java提供了spi机制,即使 Driver 由启动类加载器去加载,但是他可以让线程上下文类加载器(默认是应用程序类加载器)去请求加载子类的类加载器去完成加载。但是这确实破坏了双亲委派模型。

classLoader.getResources 方法

1
2
3
4
5
6
7
8
9
10
11
12
public Enumeration<URL> getResources(String name) throws IOException {
@SuppressWarnings("unchecked")
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
if (parent != null) {
tmp[0] = parent.getResources(name);
} else {
tmp[0] = getBootstrapResources(name);
}
tmp[1] = findResources(name);

return new CompoundEnumeration<>(tmp);
});

这个方法被用于在 classpath 下查找一个或多个,由方法参数String name指定名字的文件的 URL。该方法返回一个枚举类型 java.util.Enumeration 对象,包含了在 classpath 下所有匹配指定名称的资源的 URL。

例如下面这段代码,正是 Spring 核心类SpringFactoriesLoader查找“spring.factories”文件路径时调用了 classLoader.getResources 方法来查找:

1
2
3
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); // FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"

注意:这个方法返回的CompoundEnumeration对象中,还看不出文件所在的绝对路径,只能看出 ClassLoader 准备在哪些路径下查找目标文件。

CompoundEnumeration类的特点:
CompoundEnumeration 是一个用于处理多个 Enumeration 对象的类。CompoundEnumeration 的构造器接受一个 Enumeration 数组,并且将它们合并成一个新的 Enumeration。在 nextElement() 方法中依次返回每个 Enumeration 对象中的每个枚举元素,直到所有Enumeration对象以及它们中的元素遍历结束。
在调用 nextElement() 方法时,如果当前 Enumeration 还有元素,则返回该元素。如果当前 Enumeration 没有元素,则将指针移到下一个 Enumeration,并返回该 Enumeration 的第一个元素。
PS: 其实可以用 List 代替 Enumeration[] ,因为 List 也能遍历到每个元素。

那么是哪些路径呢?从这个方法可以看出,tmp[0]是当前加载器的父加载器返回的,tmp[1]才是当前加载器返回的,也就是说这个方法也是按照“双亲委派模型”编写的,返回的路径中既包含父加载器的路径,也包含当前加载器的路径。
当前加载器在一般情况下就是应用程序类加载器,所以它的这个方法返回的路径,就是 启动类加载器+扩展类加载器+应用程序类加载器 3个类加载器负责的路径的并集,即 “JVM系统参数sun.boot.class.path” + “JVM系统参数java.ext.dirs” + “环境变量classpath” 包含的路径的并集!

在这个方法中,涉及到几个内部类对象的创建:

一、ClassLoader.getBootstrapResources方法返回的Enumeration<URL>内部类对象,即启动类加载器返回的路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static Enumeration<URL> getBootstrapResources(String name)
throws IOException
{
final Enumeration<Resource> e =
getBootstrapClassPath().getResources(name); // 另一个内部类对象
return new Enumeration<URL> () {
public URL nextElement() {
return e.nextElement().getURL();
}
public boolean hasMoreElements() {
return e.hasMoreElements();
}
};
}

这是个 static 方法,这个内部类对象不依赖于外部类对象,它还使用到了上面一行代码创建的另一个内部类对象e(见第二点)

二、URLClassPath.getResources方法返回的Enumeration<Resource>内部类对象

上面第一点源码中getBootstrapClassPath返回的是URLClassPath对象,所以getBootstrapClassPath().getResources(name)就是调用 URLClassPath.getResources 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public Enumeration<Resource> getResources(final String var1, final boolean var2) {
return new Enumeration<Resource>() {
private int index = 0;
private int[] cache = URLClassPath.this.getLookupCache(var1);
private Resource res = null;

private boolean next() {
if (this.res != null) {
return true;
} else {
do {
URLClassPath.Loader var1x;
if ((var1x = URLClassPath.this.getNextLoader(this.cache, this.index++)) == null) {
return false;
}

this.res = var1x.getResource(var1, var2);
} while(this.res == null);

return true;
}
}

public boolean hasMoreElements() {
return this.next();
}

public Resource nextElement() {
if (!this.next()) {
throw new NoSuchElementException();
} else {
Resource var1x = this.res;
this.res = null;
return var1x;
}
}
};
}

这是个非 static 方法,所以这个内部类对象的属性之一就是外部类 URLClassPath 对象,在外部类 URLClassPath 对象中的属性 ArrayList path 就保存了启动类加载器负责加载的所有路径。内部类对象可以访问到这些路径。

三、URLClassLoader.findResources方法返回的Enumeration<URL>内部类对象

扩展类加载器 和 应用程序类加载器,它们都继承自URLClassLoader,所以”tmp[1] = findResources(name);”代码就是在调用 URLClassLoader.findResources 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public Enumeration<URL> findResources(final String name)
throws IOException
{
final Enumeration<URL> e = ucp.findResources(name, true);// ucp即URLClassPath,是URLClassLoader成员之一

return new Enumeration<URL>() {
private URL url = null;

private boolean next() {
if (url != null) {
return true;
}
do {
URL u = AccessController.doPrivileged(
new PrivilegedAction<URL>() {
public URL run() {
if (!e.hasMoreElements())
return null;
return e.nextElement();
}
}, acc);
if (u == null)
break;
url = ucp.checkURL(u);
} while (url == null);
return url != null;
}

public URL nextElement() {
if (!next()) {
throw new NoSuchElementException();
}
URL u = url;
url = null;
return u;
}

public boolean hasMoreElements() {
return next();
}
};
}

在这个内部类中,使用到了第一行声明的变量”Enumeration e”,这个变量是调用URLClassPath.findResources方法获取的:
(这个方法和上面第二点的方法的区别仅仅在于返回的是 URL 还是 Resource)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public Enumeration<URL> findResources(final String var1, final boolean var2) {
return new Enumeration<URL>() {
private int index = 0;
private int[] cache = URLClassPath.this.getLookupCache(var1);
private URL url = null;

private boolean next() {
if (this.url != null) {
return true;
} else {
do {
URLClassPath.Loader var1x;
if ((var1x = URLClassPath.this.getNextLoader(this.cache, this.index++)) == null) {
return false;
}

this.url = var1x.findResource(var1, var2);
} while(this.url == null);

return true;
}
}

public boolean hasMoreElements() {
return this.next();
}

public URL nextElement() {
if (!this.next()) {
throw new NoSuchElementException();
} else {
URL var1x = this.url;
this.url = null;
return var1x;
}
}
};
}

所以这个URLClassLoader.findResources方法中其实创建了两个内部类对象,一个是成员 URLClassPath 创建的,一个是 URLClassLoader 自己创建的。在 URLClassPath 自己创建的内部类对象中,使用到了 URLClassPath 创建的内部类对象,所以真正起作用的还是 URLClassPath 创建的内部类对象。

同时,这两个方法都非 static,所以外部类对象(扩展类加载器/应用程序类加载器)自身实例,和 URLClassPath 实例,都是其内部类对象的属性之一。扩展类加载器/应用程序类加载器 负责加载的路径,就保存在其成员 URLClassPath 中的属性 ArrayList path。内部类对象可以访问到这些路径。