Java ClassLoader原理及源码分析

讲道理我们日常开发中基本涉及不到类加载器,大多数人更不必要去自定义类加载器,但我本着知其然必知其所以然的想法,还是对java中的类加载器是什么?如何工作?它的源码?)做尽量详细的分析。研究的过程毕竟是好玩的。

本文适合对于ClassLoader有初步了解的人阅读。

Java类加载器理论

什么是类加载器

Java中的类加载器(ClassLoader)用来加载class文件的。它就是JVM将编译好的.class文件加载到虚拟机中运行的工具,这个工具有JVM为我们提供的,我们也可以自己去实现。

类加载器的分类

JVM自带的有3个类加载器:

  • Bootstrap ClassLoader:启动类加载器,加载$JRE_HOME/lib下的rt.jar、resources.jar、charsets.jar和class文件等;
  • Extention ClassLoader:扩展的类加载器,加载$JRE_HOME/lib/ext下的jar包和class文件;
  • App ClassLoader:加载当前应用的classpath的所有类。

除此之外,我们也可以根据自己的需求,自定义类加载器,自定义的类加载器是java.lang.ClassLoader的子类,可以定制类的加载方式。

双亲委派机制

好多人对于这个双亲委派机制云里雾里,其实我也不喜欢“正经”地学习技术,那么在看这个概念之前,我希望我们一起默念下面这个口诀:我爸是李刚,有事找我爹 —– 往上捅

classLoader

加载一个类时,若父加载器可以加载,优先使用父加载器。
这样做避免自己写的类(如自己写了一个String.java)污染Java的源代码,提高程序安全性。

核心:

当一个类收到了类加载请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到BootstrapClassLoader中,只有当父类加载器反馈自己无法完成这个请求时(在它自己的加载路径下没有找到所需的class),子类加载器才会尝试自己去加载。

采用双亲委派的一个好处是,比如加载位于rt.jar的类java.lang.Object,不管哪个加载器加载这个类,最后都是委托给顶层的bootstrap类加载器进行加载,这样保证了使用不同的类加载器最终都获得同一个object对象。

loadClass()源码

在双亲委派机制中,有一个非常重要的loadClass(String name, boolean resolve)方法,我们先抽象的来看一下这个方法的执行步骤:

  1. 调用findLoadedClass(String)来检查传进来的name对应的class是不是已经加载过了;
  2. 如果没有,调用父加载器的loadClass()方法。如果父加载器是null,则使用jvm内置的加载器,也就是BootstrapClassLoader。这也就是为什么ExtClassLoader的parent为null,但仍然说BootstrapClassLoader是它的父加载器,后面我们会具体说;
  3. 如果向上委托父加载器没有加载成功,则通过自己的findClass(String)来加载。

Show me the code,下面这段代码详细解释了双亲委派模型:

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检测是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
// 若没有被加载
long t0 = System.nanoTime();
try {
if (parent != null) {
// 父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
// 父加载器为空则调用BootstrapClassLoader
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;
}
}

这里要注意的是如果要编写一个classLoader的子类,也就是自定义一个classLoader,建议覆盖findClass()方法,而不要直接改写loadClass()方法

父加载器是父类吗?

父加载器不是父类,ExtClassLoader、AppClassLoader以及自定义的加载器的继承关系都是-> URLClassLoader -> SecureClassLoader -> ClassLoader。但实际上我们调用getParent()方法时,得到的并不是它的父类,而是父加载器:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public abstract class ClassLoader {
// 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;
// The class loader for the system
// @GuardedBy("ClassLoader.class")
private static ClassLoader scl;

private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
...
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
public final ClassLoader getParent() {
if (parent == null)
return null;
return parent;
}
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
return scl;
}

private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
// 通过Launcher获取ClassLoader
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
}

所以实际上,getParent()返回的就是一个ClassLoader对象parent,而parent又有2种情况:

  1. 自定义类(也就是我们平时自己写的class)创建ClassLoader时直接指定一个ClassLoader为parent;
  2. 由l.getClassLoader()获取,也就是Launcher发现如果没有指定parent,会默认指定ClassLoader为AppClassLoader。

那么接下来的一个问题:在测试过程中遇到了这样一个现象,下面代码会输出什么?

1
2
3
System.out.println(Test.class.getClassLoader());
System.out.println(Test.class.getClassLoader().getParent());
System.out.println(Test.class.getClassLoader().getParent().getParent());

我们可以先想一想,Test.class是我们自己实现的一个类,那么它的ClassLoader应该是AppClassLoader。那么它的parent()应该是ExtClassLoader,再parent()就应该是BootstrapClassLoader了吧,但事实是这样吗?

输出:

1
2
3
[email protected]
[email protected]
null

前两个猜对了,但第三个为啥是null?

这是因为BootstrapClassLoader是C++编写的,它本身也是JVM的一部分,所以严格意义来讲,它并不是一个java类,也就无法在java代码中获取它的引用。

类加载器源码

上面的“双亲委派机制”分析了ClassLoader加载类时的理论模型,下面我们来根据源码更好的理解一下(为了阅读体验,精简了部分源码)。

Launcher和BootstrapClassLoader

sun.misc.Launcher,它是一个java虚拟机的入口类:

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
43
44
45
46
47
48
49
50
51
public class Launcher {
private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");

public static Launcher getLauncher() {
return launcher;
}

private ClassLoader loader;

public Launcher() {
// Create the extension class loader
// 创建ExtClassLoader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}

// Now create the class loader to use to launch the application
try {
// 通过ExtClassLoader创建AppClassLoader
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}

//设置AppClassLoader为线程上下文类加载器
Thread.currentThread().setContextClassLoader(loader);
}

/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {}

/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {}

从上面的代码中,我们不禁会有一个问题,上面你说有3个ClassLoader,为啥这就变成了2个了?BootstrapClassLoader呢?

在前几行我们看到了一个bootClassPath的String,这个其实就是BootstrapClassLoader加载包的路径,我们单独看一下这个bootClassPath是什么:

1
System.out.println(System.getProperty("sun.boot.class.path"));

得到的运行结果是:

1
2
3
4
5
6
7
8
$JRE_HOME/lib/resources.jar:
$JRE_HOME/lib/rt.jar:
$JRE_HOME/lib/sunrsasign.jar:
$JRE_HOME/lib/jsse.jar:
$JRE_HOME/lib/jce.jar:
$JRE_HOME/lib/charsets.jar:
$JRE_HOME/lib/jfr.jar:
$JRE_HOME/classes

由此可见,这些都是jre目录下面的jar包和class文件,这其实也是jvm的提供的加载器BootstrapClassLoader加载的文件。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {

static {
ClassLoader.registerAsParallelCapable();
}

/**
* create an ExtClassLoader. The ExtClassLoader is created
* within a context that limits which files it can read
*/
public static ExtClassLoader getExtClassLoader() throws IOException
{
// 获取ext目录下的所有文件
final File[] dirs = getExtDirs();

try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().

return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}

private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
}

还是看一下这个getExtDirs()返回什么:

1
System.out.println(System.getProperty("java.ext.dirs"));
1
2
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext:
/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java

AppClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {


public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);


return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
}

这个System.getProperty("java.class.path");输出的是当前java工程目录bin,就不贴了,里面存放的是编译生成的class文件。

自定义ClassLoader

根据上面我们对于源码的分析,其他的ClassLoader主要加载的是本地指定目录下的jar或者class。但如果想动态加载一些诸如:本地其他目录下的class文件,或者网络上下载的class,就需要自定义ClassLoader了。

步骤:

  1. 编写一个类继承ClassLoader;
  2. 复写它的findClass()方法;
  3. 在findClass()方法中调用defineClass()方法转换为Class对象(这个defineClass是rt.jar提供的)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DiskClassLoader extends ClassLoader {

private String path;

public DiskClassLoader(String path) {
this.path = path;
}

@Override
protected Class<?> findClass(String name) {
byte[] data;
// 读取class文件到byte[] data中
defineClass(name, data, 0, data.length);
}
}

总结

没啥总结的,读读源码一切都很清晰了。