Java中双亲委派相关知识梳理

classloader介绍

在java中每当需要加载一个类的时候,就需要用到类加载器(classloader),它存在的意义在于把class文件中字节码变成可以被java使用的class对象。java中原生自带3个classloader分别是Bootstrap ClassLoader、Ext ClassLoader、App ClassLoader另外用户也可以自定义Custom ClassLoader。那实际加载类的时候是用哪一个加载器?java为了安全着想规定了classloader之间有一套等级机制。具体来说就是:当从子加载器出发,但子加载器加载class的时候会先委派给由父加载器先加载,如果父加载器无法加载再由子加载器加载,而这套先后顺序就是平时所说的双亲委派

image.png

  • BootstrapClassLoader,启动类加载器/根加载器,负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.. 都在里面。这个 ClassLoader 比较特殊,它其实不是一个ClassLoader实例对象,而是由C代码实现。用户在实现自定义类加载器时,如果需要把加载请求委派给启动类加载器,那可以直接传入null作为 BootstrapClassLoader。

  • ExtClassLoader,扩展类加载器,负责加载 JVM 扩展类,扩展 jar 包位于 JAVA_HOME/lib/ext/*.jar 中,库名通常以 javax 开头。

  • AppClassLoader,应用类加载器/系统类加载器,直接提供给用户使用的ClassLoader,它会加载 ClASSPATH 环境变量或者 java.class.path 属性里定义的路径中的 jar 包和目录,负责加载包括开发者代码中、第三方库中的类。

容易误会的一点是Classloader之间并不是继承关系而是组合。
image.png

AppClassLoader和ExtClassLoader都继承自URLClasLoader。
image.png

URLClassLoader继承自ClassLoader。
image.png

而一般自定义加载器是直接继承ClassLoader并根据需求重写findClass或者loadClass方法(之后会提到)。
image.png

在java代码我们可以调用getParent的方法获取该加载器的父加载器。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class JavaClassLoader {
public static void main(String[] args) {
ClassLoader appClassloader = ClassLoader.getSystemClassLoader();
ClassLoader extensionClassloader = appClassloader.getParent();
System.out.println("AppClassLoader is " + appClassloader);
System.out.println("The parent of AppClassLoader is " + extensionClassloader);
System.out.println("The parent of ExtensionClassLoader is " + extensionClassloader.getParent());
}
}
//console ouput:
////AppClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
////The parent of AppClassLoader is sun.misc.Launcher$ExtClassLoader@61bbe9ba
////The parent of ExtensionClassLoader is null

ps: 因为BootstrapClassLoader是内嵌到jvm中c代码,开发者无法直接获取,因此ExtensionClassLoader.getParent()会返回null。
_
demo代码

1
2
3
4
5
6
7
8
9
10
import java.io.*;
import java.lang.reflect.InvocationTargetException;

public class Main {

public static void main(String[] args) throws IOException, NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException {
Test test = new Test();
}

}

在new 的位置强制进入,可以看到是从确实是从子加载器AppClassLoader开始的。
image.png

步入两步后在第一个红框可以看到加载双亲委派的逻辑。
image.png

如果父类无法加载,那么使用自己的findClass实现来找。这里Test是我自定义的因此不在JAVA_HOME/lib/rt.jar
也不在 JAVA_HOME/lib/ext/*.jar中,所以最后会使用AppClassLoader的findClass实现,而AppClassLoader没有实现自己的findClass,所以最后是由URLClassLoader findClass寻找类。最后通过deineClass把字节码转换为类对象。
image.png

重写ClassLoader

通过上面分析我们可以看到在第一次加载类时是在loadClass进行loader调度(实现双亲委派逻辑),最后由findClass拿到路径Resource,由defineClass加载类。因此,如果我们把自己的class放在不常规的位置,就需要自定义ClassLoader,重写findClass方法。如果我们想突破双亲委派模型(覆盖系统类)就需要重写loadClass。

而实际应用中处于bypass的需要我们经常使用defineClass一个多态实现来从内存中或者文件中执行命令。看下面代码。

从内存中

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
import java.lang.reflect.Method;

/**
* Creator: yz
* Date: 2019/12/17
*/
public class TestClassLoader extends ClassLoader {

// TestHelloWorld类名
private static String testClassName = "com.anbai.sec.classloader.TestHelloWorld";

// TestHelloWorld类字节码
private static byte[] testClassBytes = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 51, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0,
16, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101,
1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108,
97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99,
101, 70, 105, 108, 101, 1, 0, 19, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111,
114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111,
32, 87, 111, 114, 108, 100, 126, 1, 0, 40, 99, 111, 109, 47, 97, 110, 98, 97, 105, 47,
115, 101, 99, 47, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 84, 101, 115,
116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108,
97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1,
0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0,
1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1,
0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 10, 0, 1, 0, 11,
0, 0, 0, 2, 0, 12
};

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// 只处理TestHelloWorld类
if (name.equals(testClassName)) {
// 调用JVM的native方法定义TestHelloWorld类
return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
}

return super.findClass(name);
}

public static void main(String[] args) {
// 创建自定义的类加载器
TestClassLoader loader = new TestClassLoader();

try {
// 使用自定义的类加载器加载TestHelloWorld类
Class testClass = loader.loadClass(testClassName);

// 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
Object testInstance = testClass.newInstance();

// 反射获取hello方法
Method method = testInstance.getClass().getMethod("hello");

// 反射调用hello方法,等价于 String str = t.hello();
String str = (String) method.invoke(testInstance);

System.out.println(str);
} catch (Exception e) {
e.printStackTrace();
}
}

}

从文件中

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
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

public class MyClassLoader extends ClassLoader
{
private String classpath;

public MyClassLoader(String classpath)
{
this.classpath = classpath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException
{
try
{
byte[] classDate = getClassBinaryData(name);
//System.out.println(Arrays.toString(classDate));
if (classDate == null)
{
}
else
{ // defineClass方法将字节码转化为类
return defineClass(name, classDate, 0, classDate.length);
}
}
catch (IOException e)
{
e.printStackTrace();
}

return super.findClass(name);
} // 返回类的字节码

private byte[] getClassBinaryData(String className) throws IOException
{
InputStream in = null;
ByteArrayOutputStream out = null;
String path = classpath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
try
{
in = new FileInputStream(path);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int len = 0;
while ((len = in.read(buffer)) != -1)
{
out.write(buffer, 0, len);
}
return out.toByteArray();
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
finally
{
in.close();
out.close();
}
return null;
}
}

参考

Java动态类加载,当FastJson遇到内网
Idea中那些鲜为人知的调试技巧
Java虚拟机–类加载器源码

Author

李三(cl0und)

Posted on

2020-01-22

Updated on

2020-07-11

Licensed under