双亲委派剖析

[TOC]

定义

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器

双亲委派模型的工作过程是:

  • 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
  • 每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。
  • 只有当父加载器反馈自己无法完成这个加载请求时(搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。

作用

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。因此,使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处:类随着它的类加载器一起具备了一种带有优先级的层次关系

例如类java.lang.Object,它由启动类加载器加载。双亲委派模型保证任何类加载器收到的对java.lang.Object的加载请求,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类

相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并用自定义的类加载器加载,那系统中将会出现多个不同的Object类,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
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
public class TestStaticBlock {
static{
System.out.println("static block init");
}

@Test
public void test(){
//new TestStaticBlock();

Class<?> class0 = TestStaticBlock.class;
try {
System.out.println(class0.getClassLoader() instanceof MyClassLoader);
Class<?> class1 = class0.getClassLoader().loadClass("classloader.TestStaticBlock");
ClassLoader classLoader = new MyClassLoader();
Class<?> class2 = classLoader.loadClass("TestStaticBlock");

System.out.println(class1.hashCode());
System.out.println(class2.hashCode());
System.out.println(class1.equals(class2));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

//自定义一个类加载器从指定磁盘目录加载类
public class MyClassLoader extends ClassLoader {
//不破坏双亲委派模型
@Override
protected Class<?> findClass(String name) {
String myPath = "D:/myeclipseworkspace/class/" + name.replace(".","/") + ".class";
System.out.println(myPath);
byte[] classBytes = null;
FileInputStream in = null;

try {
File file = new File(myPath);
in = new FileInputStream(file);
classBytes = new byte[(int) file.length()];
in.read(classBytes);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}

Class<?> clazz = defineClass(name, classBytes, 0, classBytes.length);
return clazz;
}


}

}

System.out.println(class1.equals(class2));猜猜输出的结果是什么?答案居然是true!这不是违背了我们平时的认知吗?被不同的类加载器加载的类不应该是不同的类吗?机智的博主很快想到了可能是双亲委派模型在作祟,先让我们看一看ClassLoader中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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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);

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

看看打印的结果

1
2
3
4
System.out.println(classLoader.getParent());
System.out.println(class0.getClassLoader());12
sun.misc.Launcher$AppClassLoader@6d06d69c
sun.misc.Launcher$AppClassLoader@6d06d69c12

当当当当,发现我们虽然重写了ClassLoader的findClass()方法,但是并没有打破双亲委派模型。使用自定义类加载器加载TestStaticBlock最后还是被转发到了父类加载器,而从输出结果可以看出这个父类加载器就是class0.getClassLoader()。当然加载出来的类也会是同一个类。


打破双亲委派模型
那么就没有办法打破双亲委派模型吗?结果当然是false。只需要重写ClassLoader类的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
//破坏双亲委派模型
@Override
public Class<?> loadClass(String name)
throws ClassNotFoundException {
String myPath = "D:/myeclipseworkspace/class/" + name.replace(".","/") + ".class";
System.out.println(myPath);
byte[] classBytes = null;
FileInputStream in = null;

try {
File file = new File(myPath);
in = new FileInputStream(file);
classBytes = new byte[(int) file.length()];
in.read(classBytes);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}

System.out.println();
Class<?> clazz = defineClass(name, classBytes, 0, classBytes.length);
return clazz;
}

以为这就结束了吗?naive。让我们看看运行结果:

1
java.io.FileNotFoundException: D:\myeclipseworkspace\class\java\lang\Object.class (系统找不到指定的路径。)1

由于我们打破了双亲委派模型,所以父类的加载(Object)也会交由我们自自定义的类加载器加载。而很明显在我们自定义的加载目录下是不会有Object.class这个文件的。