Java中的三大系统类加载器

在Java中存在三种原生的类加载器:

  • AppClassloader:它负责在 JVM 启动时,加载来自在命令java中的-classpath或者java.class.path系统属性或者 CLASSPATH 操作系统属性所指定的 JAR 类包和类路径。
  • ExtClassloader:主要负责加载 Java 的扩展类库
  • BootstrapClassloader:主要加载JVM自身工作需要的类:将%JAVA_HOME%\lib路径下或-Xbootclasspath参数指定路径下的、能被虚拟机识别的类库(仅按照文件名识别,如:rt.jar,名字不符合的类库不会被加载)加载至虚拟机内存中。

本文逻辑

  1. 首先我们简单的分析了一下三个类加载器的简单使用,看看他们都是加载什么地方的资源,它的父类都是谁。
  2. 然后分析了一下三个加载器的关系,是谁加载谁,谁是谁的父类。
  3. 然后我们重点看了类加载器的源码,我们认识到了,为什么AppClassloader是加载java.class.path,实际上也不过就是再源码里面写死了一个字符串。ExtClassloader 为什么加载的是java.ext.dirs这个扩展目录下的jar包等等。他们都是怎么设置父类的,ExtClassloader为啥父类就是null
  4. 接着我们讲了委托机制,并看了委托机制这部分的代码,最终知道了,为什么ExtClassloader的父类是null,却用的是BootstrapClassloader加载的(实际上就是一个if 判断)。
  5. 最终我们通过查询源码,非常清楚的知道了三大类加载器的关系。

AppClassloader

这个类加载器可以通过ClassLoader.getSystemClassLoader();来获取,如下代码:

1
2
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);

这样打印出来的结果是:

1
sun.misc.Launcher$AppClassLoader@18b4aac2

很明显可以看出获取到的类加载器是AppClassLoader,这个类加载器的作用是任何用户自定义的加载器都将它设置为父类。这一点我们可以通过java.lang.ClassLoad类的代码看出来。

我们先看这个类的无参构造器

1
2
3
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}

我们发现它将AppClassLoader 传入到另一个有参构造器中,我们再看一下这个有参构造器:

1
2
3
4
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
....
}

我们发现它的第二个参数是parent,所以所有自定义的类加载器的父类都是AppClassLoader

同时我们也注意到了,ClassLoader有一个getParent的方法,我们不妨打印出AppClassLoader的父类,以及父类的父类,如下:

1
2
3
4
ClassLoader systemClassLoaderParent = systemClassLoader.getParent();
System.out.println(systemClassLoaderParent);
ClassLoader systemClassLoaderParentParent = systemClassLoaderParent.getParent();
System.out.println(systemClassLoaderParentParent);

最后得到的结果是:

1
2
sun.misc.Launcher$ExtClassLoader@7cca494b
null

这样我们能看出来AppClassLoader的父类是ExtClassloader,然后ExtClassloader的父类是null

很顺利的我们过渡到ExtClassloader上。

ExtClassloader

扩展类加载器,主要负责加载 Java 的扩展类库,默认加载 JAVA_HOME/jre/lib/ext/ 目录下的所有 Jar 包或者由 java.ext.dirs 系统属性指定的 Jar 包。
那么我们来看一下这个类的扫描路径都有哪些?我们执行代码:

1
System.out.println(System.getProperty("java.ext.dirs"));

发现结果是这样的:

1
C:\Program Files\Java\jdk1.8.0_152\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext

在上面的代码我们也知道了,ExtClassloader的父类是null。

BootstrapClassloader

引导类加载器,又称启动类加载器,是最顶层的类加载器,主要用来加载 Java 核心类,如 rt.jar、resources.jar、charsets.jar 等。

需要注意的是它不是 java.lang.ClassLoader 的子类,而是由 JVM 自身实现的,该类为 C 语言实现,所以严格来说它不属于 Java 类加载器范畴,Java 程序访问不到该加载器。

我们来执行这一段代码,看看他到底是加载哪些东西

1
2
3
4
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
System.out.println(urL);
}

结果是:

1
2
3
4
5
6
7
8
file:/C:/Program%20Files/Java/jdk1.8.0_152/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_152/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_152/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_152/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_152/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_152/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_152/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_152/jre/classes

可以看到一个很常见的rt.jar,他是String 类的jar包,那么我们拿到这个类获取他的类加载器,然后打印出来看看:

1
System.out.println(String.class.getClassLoader());

然后我们打印结果看一下:

1
null

由于 BootstrapClassloader 对 Java 不可见,所以返回了 null。

ps:为什么BootstrapClassloader 是对Java不可见的?
与其用不可见这么专业的词汇,比如说BootstrapClassloader 和jvm一样都不是用java写的,而是c/c++实现的,所以它返回的是null。

三个加载器的关系

Alt text

  • AppClassloader 的父加载器是 ExtClassloader。
  • ExtClassloader 的父加载器为 null,但是要注意的是 ExtClassloader 的父加载器并不是 BootstrapClassloader。

ps:这里为什么不是父类?
这里看下面的代码实现,实际上只是在构造器的部分参数传递了一个写死的null,这作为父类。

ps:三个类加载器的加载顺序是什么?

类加载器的构造

首先我们来看rt.jar里面的sun.misc.Launcher这个类:

总代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Launcher()  
{
ExtClassLoader localExtClassLoader;
try
{ //(1)首先创建了ExtClassLoader
localExtClassLoader = ExtClassLoader.getExtClassLoader();
}
catch (IOException localIOException1)
{
throw new InternalError("Could not create extension class loader");
}
try
{ //(2)然后以ExtClassloader作为父加载器创建了AppClassLoader
this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
}
catch (IOException localIOException2)
{
throw new InternalError("Could not create application class loader");
} //(3)这个是个特殊的加载器后面会讲到,这里只需要知道默认下线程上下文加载器为appclassloader
Thread.currentThread().setContextClassLoader(this.loader);

................
}

根据代码,我们可以知道,首先是先创建ExtClassLoader,然后它作为父加载器创建了AppClassLoader,这样我们也能看出来这两个加载器的顺序。

我们再看看ExtClassLoader是怎么被创建出来的。具体是看getExtClassLoader这个方法:

代码一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static ExtClassLoader getExtClassLoader() throws IOException  {  
File[] arrayOfFile = getExtDirs();
try
{
(ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction() {
public Launcher.ExtClassLoader run() throws IOException {
int i = this.val$dirs.length;
for (int j = 0; j < i; j++) {
MetaIndex.registerDirectory(this.val$dirs[j]);
}
//(5)
return new Launcher.ExtClassLoader(this.val$dirs);
}
});
}
catch (PrivilegedActionException localPrivilegedActionException)
{
throw ((IOException)localPrivilegedActionException.getException());
}
}

这里又有一个加载文件的方法getExtDirs:

代码二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//(6)
private static File[] getExtDirs()
{
String str = System.getProperty("java.ext.dirs");
File[] arrayOfFile;
if (str != null) {
StringTokenizer localStringTokenizer = new StringTokenizer(str, File.pathSeparator);

int i = localStringTokenizer.countTokens();
arrayOfFile = new File[i];
for (int j = 0; j < i; j++) {
arrayOfFile[j] = new File(localStringTokenizer.nextToken());
}
}
else
{
arrayOfFile = new File[0];
}
return arrayOfFile;
}

在第(6)部分(代码中标的注释)代码,我们看到ExtClassLoader 主要是加载java.ext.dirs的目录的,把这个目录下面的文件都读取进来。

然后就是代码(5)(代码中标的注释)的部分的new Launcher.ExtClassLoader(this.val$dirs); 这一句了,这一句具体代码如下:

代码三:

1
2
3
4
5
6
public ExtClassLoader(File[] paramArrayOfFile) throws IOException{
//(7)第一个参数,就是父加载器的设置,这里传递了null。
super(null, Launcher.factory);
SharedSecrets.getJavaNetAccess()
.getURLClassPath(this).initLookupCache(this);
}

我们可以看到,这里ExtClassLoader直接传了一个null,作为父类,所以这也是为什么它的父类是null

所以最后到这里我们再看代码一部分,就比较清楚了,创建一个ExtClassLoader,需要首先读取java.ext.dirs目录下的资源,然后给父类设置为null。这就是代码一部分干的事。

然后根据总代码的流程,我们要看看,ExtClassLoader是如何创建一个AppClassLoader的。也就是看看总代码部分的AppClassLoader.getAppClassLoader这个方法具体干了哪些事情:

代码四:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static ClassLoader getAppClassLoader(final ClassLoader paramClassLoader) throws IOException  
{ //(8)
String str = System.getProperty("java.class.path");
final File[] arrayOfFile = str == null ? new File[0] : Launcher.getClassPath(str);

(ClassLoader)AccessController.doPrivileged(new PrivilegedAction()
{
public Launcher.AppClassLoader run()
{
URL[] arrayOfURL = this.val$s == null ? new URL[0] : Launcher.pathToURLs(arrayOfFile);

return new Launcher.AppClassLoader(arrayOfURL, paramClassLoader);
}
});
}

首先一拿到代码我们就看到了java.class.path,这个非常清晰了,AppClassLoader是读取的这个固定路径下的资源的。然后同样的,我们也看一下父类在加载器中设置的样子:

我们看一下Launcher.AppClassLoader的代码:

1
2
3
4
5
6
7
AppClassLoader(URL[] paramArrayOfURL, ClassLoader paramClassLoader)
{
//paramClassLoader就是ExtClassloader
super(paramClassLoader, Launcher.factory);
this.ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
this.ucp.initLookupCache(this);
}

可见它将第二个参数设置为父类,第二个参数的父类格式就是ExtClassloader。如此便很清楚了。

类加载器的原理

Java类加载器使用的是一种委托机制,也就是一个类加载器在加载一个类的时候会首先尝试让父类加载器来加载。

那么这种加载器加载的方式有什么好处呢?

  1. 当父类加载器加载该类的时候,就没有必要使用子ClassLoader 再加载一次了。
  2. 考虑到的是一种安全的因素,如果不用这种机制,那么我们随便可以使用一个自己定义的类来代替Java核心API中类的实现,比如我们也可以自己写个String类,来替代rt.jar中的String。使用双亲委托则,当 JVM 加载 String 类的时候, AppClassLoader 会委托父类加载器 ExtClassLoader 来加载,ExtClassLoader 又会委托给 Bootstrcp ClassLoader 来加载,这样就不会加载自定义的 String 类了。

ps:这里是不是也可以说Bootstrcp ClassLoader还是ExtClassLoader的父类。这个问题可以看代码里具体的实现

我们从代码来看一下这种委托机制是怎么实现的。

委托机制的代码实现

代码一:

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
protected Class<?> loadClass(Stringname,boolean resolve) throws ClassNotFoundException  
{
synchronized (getClassLoadingLock(name)) {
// 首先从jvm缓存查找该类
Class c = findLoadedClass(name); // (1)
if (c ==null) {
longt0 = System.nanoTime();
try { //然后委托给父类加载器进行加载
if (parent !=null) {
c = parent.loadClass(name,false); (2)
} else { //如果父类加载器为null,则委托给BootStrap加载器加载
c = findBootstrapClassOrNull(name); (3)
}
} catch (ClassNotFoundExceptione) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c ==null) {
// 若仍然没有找到则调用findclass查找
// to find the class.
longt1 = System.nanoTime();
c = findClass(name); (4)

// 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); //(5)
}
returnc;
}
}

代码(1)表示从 JVM 缓存查找该类,如果该类之前被加载过,则直接从 JVM 缓存返回该类。

代码(2)表示如果 JVM 缓存不存在该类,则看当前类加载器是否有父加载器,如果有的话则委托父类加载器进行加载,否者调用(3),委托 BootStrapClassloader 进行加载,如果还是没有找到,则调用当前 Classloader 的 findclass 方法进行查找。

代码(5)则是当字节码加载到内存后进行链接操作,对文件格式和字节码验证,并为 static 字段分配空间并初始化,符号引用转为直接引用,访问控制,方法覆盖等,本文对这些不进入深入探讨。

总结

看到这里我们最终能够得到这三类加载器的关系了:

  • AppClassloader 父类是ExtClassloader,按照委托机制也是父类加载的
  • ExtClassloader 父类是null,按照委托机制,如果父类是null,使用的是BootstrapClassloader加载的。