类加载器ClassLoader
2022-03-18 10:30:30 # Java安全

类加载器

ClassLoader即常说的类加载器,其功能是用于从Class文件加载所需的类,主要场景用于热部署、代码热替换等场景。 系统提供了3种类加载器:Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader。

  • Bootstrap ClassLoader 最顶层的加载器-启动类加载器,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中,Java程序无法直接引用该类加载器。
  • Extention ClassLoader 扩展类加载器,由Java实现,独立于虚拟机的外部,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件,还可以加载-D java.ext.dirs选项指定的目录。开发者可直接使用扩展类加载器。 该加载器是由sun.misc.Launcher$ExtClassLoader实现。
  • Appclass Loader应用程序类加载器,该加载器是由sun.misc.Launcher$AppClassLoader实现,该类加载器负责加载用户类路径上所指定的类库。开发者可通过ClassLoader.getSystemClassLoader()方法直接获取,故又称为系统类加载器。当应用程序没有自定义类加载器时,默认采用该类加载器。

加载顺序

  1. Bootstrap CLassloder
  2. Extention ClassLoader
  3. AppClassLoader

Launcher分析

我们查看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
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
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 {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}

//设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
Thread.currentThread().setContextClassLoader(loader);
}

根据上面部分源码我们可以知道:

  1. Launcher在构造方法中初始化了ExtClassLoaderAppClassLoader
  2. Launcher定义了一个静态变量static String bootClassPath =System.getProperty("sun.boot.class.path");,这个字符串”sun.boot.class.path”应该和BootstrapClassLoader有关,猜测是BootstrapClassLoader加载Jar包的路径。

我们可以输出一下这个值

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

得到以下结果,可以看到,这些全是jre目录下的jar包或者是class文件

1
2
3
4
5
6
7
8
C:\Program Files\Java\jdk1.8.0_321\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_321\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_321\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_321\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_321\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_321\jre\classes

ExtClassLoader分析

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
56
/*
* 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
{
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;
}

......
}

在先前的内容有说过,可以指定-D java.ext.dirs参数来添加和改变ExtClassLoader的加载路径。这里我们编写测试代码。

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

得到以下结果

1
2
C:\Program Files\Java\jdk1.8.0_321\jre\lib\ext;
C:\Windows\Sun\Java\lib\ext

源码中通过ExtClassLoader$getExtDirs获取到了”java.ext.dirs”的路径值,然后在

ExtClassLoader$getExtClassLoader中被调用用来加载。

AppClassLoader分析

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);
}
});
}

......
}

在源码中我们看到AppClassLoader加载的就是java.class.path下的路径。

加载顺序分析

前面了解到了BootstrapClassLoaderExtClassLoaderAppClassLoader实际是获取了相应的环境属性sun.boot.class.pathjava.ext.dirsjava.class.path来加载资源文件的。

获取Test.class文件的类加载器:

1
2
ClassLoader cl = Test.class.getClassLoader();
System.out.println("ClassLoader is:"+cl.toString());

输出结果如下

image-20220318115118410

结果说明Test.class文件是由AppClassLoader加载的,这个Test类是我们自己编写的,那我们再测试下一些基础类是由哪个加载器加载的

1
2
3
4
5
6
ClassLoader cl = Test.class.getClassLoader();
System.out.println("ClassLoader is:"+cl.toString());
cl = String.class.getClassLoader();
System.out.println("ClassLoader is:"+cl.toString());
cl = int.class.getClassLoader();
System.out.println("ClassLoader is:"+cl.toString());

运行报错,提示空指针

image-20220318115536600

实际上int.class等基础类是由Bootstrap ClassLoader加载的

每个类加载器都有一个父加载器,比如加载Test.class是由AppClassLoader完成,那么AppClassLoader也有一个父加载器,通过getParent方法。代码如下:

1
2
3
ClassLoader cl = Test.class.getClassLoader();
System.out.println("ClassLoader is:"+cl.toString());
System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString());

结果如下:

image-20220318115932982

根据结果我们可以知道,AppClassLoader的父加载器是ExtClassLoader,因此我们可以在获取下ExtClassLoader的父加载器

1
2
3
System.out.println("ClassLoader is:"+cl.toString());
System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString());
System.out.println("ClassLoader\'s grand father is:"+cl.getParent().getParent().toString());

运行结果如下:

image-20220318120148595

一样报的空指针异常,这表明ExtClassLoader没有父加载器?往下分析

ExtClassLoaderAppClassLoader继承同一个父类URLClassLoader

1
2
static class ExtClassLoader extends URLClassLoader {}
static class AppClassLoader extends URLClassLoader {}

调用AppClassLoadergetParent()代码为什么会得到ExtClassLoader的实例呢

先了解下URLClassLoader的类继承图

image-20220318120605826

ClassLoader.java中发现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
64
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对象

1
private final ClassLoader parent;

parent的赋值是在ClassLoader对象的构造方法中,根据上面的源码,我们可以发现

它可以通过两种方式赋值:

  1. 由外部类创建ClassLoader时直接指定一个ClassLoader为parent。

  2. getSystemClassLoader()方法生成,也就是在sun.misc.Laucher通过getClassLoader()获取,也就是AppClassLoader。也就是说,一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader

现在探究下ExtClassLoaderAppClassLoader的parent的来源

image-20220318123332729

从代码中可以看到AppClassLoader的parent是一个ExtClassLoader实例。ExtClassLoader并没有直接找到对parent的赋值。它调用了它的父类也就是URLClassLoder的构造方法并传递了3个参数。

image-20220318130002200

在父类的构造方法中,我们可以看到传递的parent为null

image-20220318130031339

综上,AppClassLoader的parent是ExtClassLoaderExtClassLoader的parent是null。

继续往下,BootstrapClassLoader是如何创建的?

Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。然后呢,我们前面已经分析了,JVM初始化sun.misc.Launcher并创建Extension ClassLoaderAppClassLoader实例。并将ExtClassLoader设置为AppClassLoader的父加载器。Bootstrap没有父加载器,但是它却可以作用一个ClassLoader的父加载器。比如ExtClassLoader。这也可以解释之前通过ExtClassLoader的getParent方法获取为Null的现象。

双亲委托

一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。

流程图:

image-20220318140231962

这个更具体一些

image-20220318140827686

描述:

  1. 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
  2. 递归,重复第1部的操作。
  3. 如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。
  4. Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
  5. ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。

在了解加载过程时,需要了解几个个重要的方法loadClass()、findLoadedClass()、findClass()、defineClass()。

  1. loadClass():加载具有指定二进制名称的类。此方法搜索类的方式与loadClass(String, boolean)方法相同,Java虚拟机调用它来解析类引用。
  2. findClass():根据名称或位置加载.class字节码,然后使用defineClass,通常由子类去实现。
  3. defineClass():把字节码转化为Class。
  4. findLoadedClass():判断该类是否已经加载过,加载过就返回Class对象,未加载过就返回null。
1
2
3
protected Class<?> loadClass(String name,
boolean resolve)
throws ClassNotFoundException

下面为loadClass(String, boolean)方法的源代码

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
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 {
//父加载器为空则调用Bootstrap Classloader
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();
//父加载器没有找到,则调用findclass
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()
resolveClass(c);
}
return c;
}
}

大致的流程是:

  1. 执行findLoadedClass(String)去检测这个class是不是已经加载过了。
  2. 执行父加载器的loadClass方法。如果父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader。这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。
  3. 如果向上委托父加载器没有加载成功,则通过findClass(String)查找。

image-20220318143032440

如果class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()又会调用resolveClass(Class)这个方法来生成最终的Class对象

image-20220318143111427

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

image-20220318142957798

自定义ClassLoader

不管是Bootstrap ClassLoader还是ExtClassLoader等,这些类加载器都只是加载指定的目录下的jar包或者资源。如果从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载,这样的话,需要自定义一个ClassLoader。

步骤分为如下几步:

  1. 编写一个类继承自ClassLoader抽象类。
  2. 复写它的findClass()方法。
  3. findClass()方法中调用defineClass()

注:如果自定义一个ClassLoader,默认的parent父加载器是AppClassLoader,因为这样就能够保证它能访问系统内置加载器成功加载class文件。

测试:自定义一个ClassLoader,默认加载路径为D:\code\lib下的jar包和资源。

Say.java

1
2
3
4
5
public class Say {
public void say(){
System.out.println("Say Hello");
}
}

编译成class文件后放到D:\code\lib路径下。

DiskClassLoader.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
import java.io.*;

public class DiskClassLoader extends ClassLoader{
private String mLibPath;

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

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = getFileName(name);
File file = new File(mLibPath, fileName);
try {
FileInputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
try {
while ((len = is.read()) != -1) {
bos.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
is.close();
bos.close();

return defineClass(name,data,0,data.length);

} catch (IOException e) {
e.printStackTrace();
}

return super.findClass(name);
}

//获取要加载的class文件名
private String getFileName(String name) {
// TODO Auto-generated method stub
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}

ClassLoaderTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ClassLoaderTest {
public static void main(String[] args) {
//创建自定义classloader对象。
DiskClassLoader diskLoader = new DiskClassLoader("D:\\code\\lib");
try {
Class clazz = diskLoader.loadClass("Say");

if(clazz!=null){
Object o = clazz.newInstance();
Method say = clazz.getDeclaredMethod("say", null);
say.invoke(o, null);
}
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}

}
}

运行结果如下:

image-20220318151211530

Context ClassLoader 线程上下文类加载器

ContextClassLoader其实只是一个概念,查看Thread.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
public class Thread implements Runnable {

/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;

public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}

public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(contextClassLoader,
Reflection.getCallerClass());
}
return contextClassLoader;
}
}

contextClassLoader只是一个成员变量,通过setContextClassLoader()方法设置,通过getContextClassLoader()返回。

编写测试代码来加深理解,编写一个接口类ISpeak

1
2
3
4
public interface ISpeak {
public void speak();

}

再编写2个SpeakTest.java文件实现ISpeak接口,一个源码是:

1
2
3
4
5
6
7
8
public class SpeakTest implements ISpeak {

@Override
public void speak() {
// TODO Auto-generated method stub
System.out.println("Test");
}
}

它生成的SpeakTest.class文件放置在D:\\code\\lib\\test目录下。

然后,另一个SpeakTest.java

1
2
3
4
5
6
7
8
public class SpeakTest implements ISpeak {

@Override
public void speak() {
// TODO Auto-generated method stub
System.out.println("I am boy");
}
}

它生成的SpeakTest.class文件放置在D:\\code\\lib目录下。

再编写DiskClassLoader1DiskClassLoader2,代码和DiskClassLoader基本一致,修改下加载路径和类路径即可。

DiskClassLoader1.java的main方法

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
public static void main(String[] args) {
DiskClassLoader1 diskLoader1 = new DiskClassLoader1("D:\\code\\lib\\test");
Class cls1 = null;
try {
//加载class文件
cls1 = diskLoader1.loadClass("SpeakTest");
System.out.println(cls1.getClassLoader().toString());
if(cls1 != null){
try {
Object obj = cls1.newInstance();
//SpeakTest1 speak = (SpeakTest1) obj;
//speak.speak();
Method method = cls1.getDeclaredMethod("speak",null);
//通过反射调用Test类的speak方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

运行结果如下:

image-20220318154942350

DiskClassLoader2.java的main方法

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 static void main(String[] args) {
DiskClassLoader2 diskLoader2 = new DiskClassLoader2("D:\\code\\lib");
System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());
new Thread(new Runnable() {

@Override
public void run() {
System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());

// TODO Auto-generated method stub
try {
//加载class文件
// Thread.currentThread().setContextClassLoader(diskLoader);
//Class c = diskLoader.loadClass("SpeakTest");
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class c = cl.loadClass("SpeakTest");
// Class c = Class.forName("SpeakTest");
System.out.println(c.getClassLoader().toString());
if(c != null){
try {
Object obj = c.newInstance();
//SpeakTest1 speak = (SpeakTest1) obj;
//speak.speak();
Method method = c.getDeclaredMethod("speak",null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
}

运行结果如下:

image-20220318155041635

根据结果我们可以得出:

  1. DiskClassLoader1成功加载了SpeakTest.class文件。
  2. 主线程和子线程的ContextClassLoaderAppClassLoader
  3. AppClassLoader加载不了父线程当中已经加载的SpeakTest.class内容。

修改代码,再子线程开头加上

1
Thread.currentThread().setContextClassLoader(diskLoader2);

结果如下:

image-20220318160726100

可以看到子线程的ContextClassLoader变成了DiskClassLoader

修改diskLoader2为diskLoader1,结果如下:

image-20220318160547669

可以看到DiskClassLoader2DiskClassLoader1分别加载了自己路径下的SpeakTest.class文件。

参考

https://blog.csdn.net/briblue/article/details/54973413

http://gityuan.com/2016/01/24/java-classloader/