前言

ClassLoader是Java中很重要一个功能,我们都知道Java是个跨平台的语言,这个跨平台的语言是通过JVM去实现,Java通过编译器成class文件,在使用的时候通过ClassLoader去加载。而且很多框架喜欢使用ClassLoader来实现一些特定功能。比如热更新(通过替换新的ClassLoader装载新的class文件实现代码更新),Tomcat中防止重复jar的加载(Tomcat内置好ClassLoader加载器,指定了不同地方的jar包,防止一些类重复加载比如servlet的jar)等等功能。

初识ClassLoader

ClassLoader:类加载器,Java是动态类加载(java 不是一次把所有类都加载进去所有类会很占内存,使用的时候加载,不用的时候卸载,用于把类加载到JVM当中,让JVM使用执行的。所以一般Java类加载器都会指定一些目录,比如我们在window系统中指定环境变量目录CLASSPATH=.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar

三种类加载器

BootStrapClassLoadeExtentionClassLoader ApplicationClassLoader这三种类加载器是java最常见的。

BootStrapClassLoader

启动类加载器,用于加载JVM启动时候需要的类加载器。是Java最顶级的加载器,一般用于加载Java最基本的类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等,主要是rt.jar。rt.jar包含了很多Java的基础类,例如java.lang.*的类都是来自rt.jar中。。在windows上目录是\Java\jdk1.8.0_121\jre\lib。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变BootstrapClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。

启动类加载器是由C++(HotSpot)编写的,针对不同的平台可能是不一样的。所以在Java的代码中无法找到对应的类。

ExtentionClassLoader

扩展类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。windows上的目录\Java\jdk1.8.0_121\jre\lib\ext。他是Java语言编写的类加载器,它是Lanuncher的一个内部类。sun.misc.Launcher.ExtClassLoader

ApplicationClassLoader

应用类加载器,用于加载用户classpath下的jar包,这个类加载器可以直接调用。如果不声明新的类加载器,系统的默认类加载器。ClassLoader.getSystemClassLoader()获取的都是应用类加载器。(一般情况下)

上面这三个类加载器不是单独工作的,而是相互协作完成的Java的类加载。这三个类加载器相互协作有一个工作模式叫双亲委派。(双亲委派是代理模式的一种体现)

双亲委派模型

启动类加载器双亲委派模型只的是Java在加载类时候,不是自己去加载而是先用自己的父加载器去加载,以应用类加载器为例,类加载先去扩展类加载器加载,如果扩展类加载器没加载到,再去启动类加载器去加载,如果启动类加载器加载不到,在使用应用类加载器,如果应用类加载器也加载不到,就会抛出ClassNotFound的异常。

这个双亲委派的机制实现就是通过loadClass方法递归调用。后面我们会在源码上去说这个。

两个重要的方法

loadClass方法
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
64
65
66
67
68
69
//最常用的加载类的方法。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//核心的类加载方法,这里面有递归实现双亲委派
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) {//如果父类加载器不为空,让父类加载器去加载这个类
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); //调用findClass方法去加载类

// 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();
}
}
//是否调用resolveClass方法。默认是不调用的。这个我们后面来介绍
if (resolve) {
resolveClass(c);
}
return c;
}
}

/**
* 返回一个启动类加载器,如果没有返回null
*/
private Class<?> findBootstrapClassOrNull(String name){
//校验类的名字,如果校验失败,直接返回null
if (!checkName(name)) return null;
//调用findBootstrapClass方法返回启动类加载器
return findBootstrapClass(name);
}


// return null if not found,本地方法,如果找到返回,否者返回null
private native Class<?> findBootstrapClass(String name);


// true if the name is null or has the potential to be a valid binary name
private boolean checkName(String name) { //校验类名字 如果ture返回null
if ((name == null) || (name.length() == 0)) //判断明是是不是null,或者是空,如果是直接返回,然后返回null
return true;
//判断是不是二进制文件,结尾是-1(EOF标志),是否允许数组的语法同时是字符串形式的数组即["aaa","bbb"]。他的第一个字符是不是[,如果是说明校验通过。VM.allowArraySyntax是JVM的一个配置属性。一般是ture或false
if ((name.indexOf('/') != -1)|| (!VM.allowArraySyntax() && (name.charAt(0) == '[')))
return false;
return true;
}

看了上面的代码注释,估计也能能明白类加载的过程,它使用递归的方式一直使用父类加载器去加载,如果父类加载器已经加载过就直接使用,否则一直递归到父类是启动类加载器。如果启动类加载器也加载不到类,就调用findClass方法。

findClass方法
1
2
3
4
5
//ClassLoader类的抽象类方法,不同类去实现,这个方法实现各个框架可以去直接调用加载类。自定义ClassLoader,这个类在loadClass方法后执行。默认抛出ClassNotFoundException异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

一般自定义类加载器都是自己重新findClass方法,来实现自定义类加载,可以在findClass的顺序。这样可以不使用上面的类加载器。我们就找一些已经实现了的findClass方法来简单讲一下,然后我们在自定义类加载器。

我们下面分析一个javassist的一个类加载器的实现。

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
protected Class findClass(String name) throws ClassNotFoundException {
byte[] classfile;
try {
if (this.source != null) {//如果源类池不为空
if (this.translator != null) {
//加载转义器
this.translator.onLoad(this.source, name);
}

try {
//获取源文件的字节码信息
classfile = this.source.get(name).toBytecode();
} catch (NotFoundException var7) {
return null;
}
} else {//如果类池类没有信息
//获取jar名称
String jarname = "/" + name.replace('.', '/') + ".class";
//获取jar的输入流信息
InputStream in = this.getClass().getResourceAsStream(jarname);
if (in == null) {
return null;
}
//获取jar里所有类信息,放到类池
classfile = ClassPoolTail.readStream(in);
}
} catch (Exception var8) {
throw new ClassNotFoundException("caught an exception while obtaining a class file for " + name, var8);
}
//切割类名
int i = name.lastIndexOf(46);
if (i != -1) {//获取包名
String pname = name.substring(0, i);
if (this.getPackage(pname) == null) {
try {//定义报名信息
this.definePackage(pname, (String)null, (String)null, (String)null, (String)null, (String)null, (String)null, (URL)null);
} catch (IllegalArgumentException var6) {
}
}
}
//根据获取信息生成类对象。
return this.domain == null ? this.defineClass(name, classfile, 0, classfile.length) : this.defineClass(name, classfile, 0, classfile.length, this.domain);
}

我们简单的描述一下,主要就是获取字节码信息,如果没有字节码信息就获取文件流信息。然后根据字节码信息和文件流信息去定义包信息,类信息等等,最后返回生成返回类对象。详细的defineClass方法就是针对流信息,创建对应的类对象,后面我们自己写一个ClassLoader。

自定义ClassLoader

使用双亲委派这种加载模式要继承ClassLoader,主要是复写findClass方法。然后递归调用结束后,使用findClass去实现自定义加载Class。

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
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class TestClassLoader extends ClassLoader {

/**
* 重写父类方法,返回一个Class对象
* ClassLoader中对于这个方法的注释是:
* This method should be overridden by class loader implementations
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {

Class clazz = null;
String classFilename = name + ".class";
System.out.println(classFilename);
File classFile = new File(classFilename);
if (classFile.exists()) {
try (FileChannel fileChannel = new FileInputStream(classFile)
.getChannel();) {
MappedByteBuffer mappedByteBuffer = fileChannel
.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
byte[] b = mappedByteBuffer.array();
clazz = defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
}
}
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}


public static void main(String[] args) throws Exception {
TestClassLoader myClassLoader = new TestClassLoader();
Class clazz = myClassLoader.loadClass("Hello");
Method sayHello = clazz.getMethod("sayHello");
sayHello.invoke(null, null);
}
}

被加载的类。

1
2
3
4
5
6
7
8
9
10
11
public class Hello {

public static void sayHello(){
System.out.println("Hello,I am ....");
}

public static void main(String[] args) {
sayHello();
}
}

结果打印

1
2
3
4
5

Hello,I am ....

Process finished with exit code 0

自定义加载器可以做什么,简单的说一些,比如说做一些加密处理,把class加密后使用特殊ClassLoader才能加载使用。指定特殊位置的类,指定文件夹。(类似BootStarp类加载器加载指定的包的class)

总结

ClassLoader是向JVM钟加载类的一个工具,他的加载过程是复杂的,需要验证,加载等等操作。我这边都没有详细的描述,我这边只是对ClassLoader这个类进行源码分析,对Java的双亲委派和委派是如果实现做了一个简单的解读。同时对Java常见的三种类加载器和ClassLoader这个类的两个重要的方法进行介绍。

后面展示javassist类加载器的实现和自己怎么实现一个简单的类加载器做了个demo。实际上真正的类加载器远比这复杂。需要我们自己再去实现更多的内容。

最后在提一个小的经验,如果判断是不是两个相同类,对类加载器很关键。(尽管两个类是同一个类,但是不通的加载器加载出来,可能是不一样的)判断一个类的唯一性 全类名+类加载器