目录
1 写在前面
为了更好的理解双亲委派机制,需要先了解类加载与类加载器相关知识。
2 什么是双亲委派机制
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,子加载器才会尝试自己去加载。
总结就是
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

3 类加载器介绍

-
Bootstrap 类加载器是用 C++ 实现的,是虚拟机自身的一部分,主要负责加载核心的类库。如果获取它的对象,将会返回 null;
-
扩展类加载器和应用类加载器是独立于虚拟机外部,为 Java 语言实现的,均继承自抽象类 java.lang.ClassLoader ,开发者可直接使用这两个类加载器。
-
Application 类加载器对象可以由
ClassLoader.getSystemClassLoader()方法的返回,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 -
用户自定义类加载器(Custom ClassLoader):可加载指定路径下的class文件。
4 双亲委派机制工作流程
-
当
Application ClassLoader收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。 -
当
Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。 -
如果
Bootstrap ClassLoader加载失败(在<JAVA_HOME>\lib中未找到所需类),就会让Extension ClassLoader尝试加载。 -
如果
Extension ClassLoader也加载失败,就会使用Application ClassLoader加载。 -
如果
Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载。 -
如果均加载失败,就会抛出
ClassNotFoundException异常。
例子:
当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理会先检查自己是否已经加载过,如果没有再往上。注意这个过程,直到到达Bootstrap classLoader之前,都是没有哪个加载器自己选择加载的。如果父加载器无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
5 双亲委派机制作用
-
避免类的重复加载。当父类加载器加载了该类时,子类加载器就不会再加载一次。
-
防止java核心API被随意替换。
假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常java.lang.SecurityException: Prohibited package name: java.lang
6 双亲委派机制的实现
双亲委派的实现其实并不复杂,其实就是一个递归,我们一起来看一下ClassLoader里的代码:
// 代码摘自《深入理解Java虚拟机》
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先,检查请求的类是否已经被加载过了
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// 在父类加载器无法加载的时候
// 再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
6 破坏双亲委派模型-线程上下文类加载器
在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。
附:面试题:能不能自己写个类叫java.lang.System?
答案:可以。
我们需要System去打印信息,所以这里换成Math类。
package java.lang;
/**
* 自定义的一个Math类
*/
public class Math {
public Math() {
System.out.println("Math class init");
}
}
在Test类中创建一个实例,调用我们自定义的方式时,会报错Error:(36, 21) java: Math()可以在java.lang.Math中访问private
由于双亲委派机制的存在,我们自定义的String类并不会被加载,所以自然也找不到我们定义的方法。我们这里定义的构造函数是公有的,而原生库中是私有的,所以是非法访问。
如果我们不使用java库中已存在的类名,新起一个名字:MyString,不就可以避开双亲委派吗?
package java.lang;
/**
* 自定义的java.lang.MyString
*/
public class MyMath {
public MyMath() {
System.out.println("MyString Class init");
}
}
测试类中实例化对象,出现异常
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
...
查看ClassLoader类中源码中发现,官方不允许我们使用类名以java.开头,所以这种方式行不通。
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
...
}
但是我们可以通过 -Xbootclasspath 参数来指定 Bootstrap加载目录。
| 参数 | 效果 |
|---|---|
| -Xbootclasspath:
| 完全取代核心的Java class 搜索路径。不常用,否则要重新写所有Java 核心class |
| -Xbootclasspath/p:
| 前缀在核心class搜索路径前面。也就是优先搜索参数指定路径。不常用,避免引起不必要的冲突. |
| -Xbootclasspath/a:
| 后缀在核心class搜索路径后面。也就是其他路径都搜完了,再搜索参数指定路径。常用。 |
语法:
(分隔符与classpath参数类似,unix使用:号,windows使用;号,这里以unix为例)
java -Xbootclasspath/a:/usrhome/thirdlib.jar: -jar yourJarExe.jar
先把系统的Math类代码复制到我们自己写的Math类中(确保不会因缺少方法报错),再把当前项目打成jar包,最后执行命令java -Xbootclasspath/a:/xxxpath/xxx.jar -verbose > math.txt。查看math.txt文件。
这样,我们确实可以自己编写以"java."开头的代码,但必须交由Bootstrap ClassLoader加载。
本文详细介绍了Java的双亲委派机制,包括其工作流程、作用和实现方式。类加载器从Bootstrap到应用程序类加载器依次尝试加载类,防止类重复加载和核心API被篡改。此外,文章还探讨了如何通过线程上下文类加载器破坏双亲委派模型,并解答了能否自定义java.lang包下类的问题。
3753

被折叠的 条评论
为什么被折叠?



