浅析java双亲委派机制及其作用及代码示例
首先我们了解下什么是双亲委派机制?当某个类加载器需要加载某个.class
文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且,加载某个类的class文件时,Java虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式。
其工作原理:
(1)如果一个类加载器收到了类加载请求,它并不会自己先加载,而是把这个请求委托给父类的加载器去执行
(2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的引导类加载器;
(3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制
(4)父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常
一、知识准备:类加载器的类别
在介绍双亲委派机制的时候,不得不提ClassLoader(类加载器)。说ClassLoader之前,我们得先了解下Java的基本知识。
Java是运行在Java的虚拟机(JVM)中的,但是它是如何运行在JVM中了呢?我们在IDE中编写的Java源代码被编译器编译成.class的字节码文件。然后由我们的ClassLoader负责将这些class文件给加载到JVM中去执行。
(1)我们定义的类,如果我们要在编码中用到这个类,首先就是要先把“*.java”这个文件编译成class文件,然后由对应的“类加载器”加载到JVM中,我们才能够使用这个“类对象”。
(2)一般的场景下,类的加载是在我们程序启动的时候由jvm来完成,但是有些场景可能需要我们手动去指定加载某个类或找到某个类,这时候就要用到 Class.forName(String className) 加载/找到 这个className对应的类。
(3)如果比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这个两个类就必定不相等。
JVM中提供了三层的ClassLoader:
1、BootstrapClassLoader(启动类加载器)
Bootstrap classLoader主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。JDK自带的一款类加载器,用于加载JDK内部的类。Bootstrap类加载器用于加载JDK中$JAVA_HOME/jre/lib下面的那些类,比如rt.jar包里面的类。
c++
编写,加载java
核心库 java.*
,构造ExtClassLoader
和AppClassLoader
。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。2、ExtClassLoader (标准扩展类加载器)
ExtClassLoader主要负责加载 jre/lib/ext 目录下的一些扩展的jar。主要用于加载JDK扩展包里的类。一般$JAVA_HOME/lib/ext下面的包都是通过这个类加载器加载的,这个包下面的类基本上是以javax开头的。
java
编写,加载扩展库,如classpath
中的jre
,javax.*
或者java.ext.dir
指定位置中的类,开发者可以直接使用标准扩展类加载器。
3、AppClassLoader(系统类加载器)
AppClassLoader:主要负责加载应用程序的主函数类。用来加载开发人员自己平时写的应用代码的类的,加载存放在classpath路径下的那些应用程序级别的类的。
java
编写,加载程序所在的目录,如user.dir
所在的位置的class
其实还有一种类加载器,就是下面第4种。
4、CustomClassLoader(用户自定义类加载器)
java
编写,用户自定义的类加载器,可加载指定路径的class
文件
5、为什么应该叫做“父委派模型”,而不是“双亲委派机制”
这是个很蛋疼的翻译问题,实际上在oracle官方文档上,人家是这样描述的:
The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.
java平台通过委派模型去加载类。每个类加载器都有一个父加载器。当需要加载类时,会优先委派当前所在的类的加载器的父加载器去加载这个类。如果父加载器无法加载到这个类时,再尝试在当前所在的类的加载器中加载这个类。(所以,java的类加载机制应该叫做“父委派模型”,不应该叫做“双亲委派机制”,“双亲委派机制”这个名字太具有误导性了。)
二、双亲委派机制
如果有一个我们写的Hello.java编译成的Hello.class文件,它是如何被加载到JVM中的呢?别着急,请继续往下看。
1、源码分析
打开AndroidStudio,搜索“ClassLoader”,然后打开“java.lang”包下的ClassLoader类,然后将代码翻到loadClass方法:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先检查这个classsh是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
其实这段代码已经很好的解释了双亲委派机制,下面这张图来描述一下上面这段代码的流程:
从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。
1、不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。
2、父类中同理也会先检查自己是否已经加载过,如果没有再往上。
注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。
3、直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层。
4、如果没有任何加载器能加载,就会抛出ClassNotFoundException。
5、“父委派模型”是怎么工作的?
举个例子,当前有个Test.class,需要加载rt.jar中的java.lang.String,那么加载的流程如下图所示,整体的加载流程是向上委托父加载器完成的。
如果整个链路中,父加载器都没有加载这个类,且无法加载这个类时,才会由Test.class所在的加载器去加载某个类(例如希望加载开发人员自定义的类 Test2.class)。
三、为什么要设计这种机制
这种设计有个好处是,如果有人想替换系统级别的类:String.java,篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
“父委派模型”保证了系统级别的类的安全性,使一些基础类不会受到开发人员“定制化”的破坏。
如果没有使用父委派模型,而是由各个类加载器自行加载的话,如果开发人员自己编写了一个称为java.lang.String的类,并放在程序的ClassPath中,那系统将会出现多个不同的String类, Java类型体系中最基础的行为就无法保证。应用程序也将会变得一片混乱。
双亲委派机制的作用:
1、防止重复加载同一个.class
。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class
不能被篡改。通过委托方式,不会去篡改核心.clas
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。
有人总结了一张脑图如下:
四、代码示例
1、举例 1 :我自己建立一个 java.lang.String 类,写上 static 代码块
package java.lang;
public class String {
static{
System.out.println("我是自定义的String类的静态代码块");
}
}
在另外的程序中加载 String 类,看看加载的 String 类是 JDK 自带的 String 类,还是我们自己编写的 String 类
public class StringTest {
public static void main(String[] args) {
java.lang.String str = new java.lang.String();
System.out.println("hello,atguigu.com");
StringTest test = new StringTest();
System.out.println(test.getClass().getClassLoader());
}
}
程序并没有输出我们静态代码块中的内容,可见仍然加载的是 JDK 自带的 String 类
为什么呢?由于我们定义的String类本应用系统类加载器,但它并不会自己先加载,而是把这个请求委托给父类的加载器去执行,到了扩展类加载器发现String类不归自己管,再委托给父类加载器(引导类加载器),这时发现是java.lang包,这事就归引导类加载器管,所以加载的是 JDK 自带的 String 类
2、举例 2 :
在我们自己的 String 类中整个 main() 方法
package java.lang;
public class String {
static{
System.out.println("我是自定义的String类的静态代码块");
}
//错误: 在类 java.lang.String 中找不到 main 方法
public static void main(String[] args) {
System.out.println("hello,String");
}
}
由于双亲委派机制找到的是 JDK 自带的 String 类,但在引导类加载器的核心类库API里的 String 类中并没有 main() 方法
3、举例 3:
在 java.lang 包下整个 ShkStart 类 (自定义类名)
package java.lang;
public class ShkStart {
public static void main(String[] args) {
System.out.println("hello!");
}
}
会报错,出于保护机制,java.lang 包下不允许我们自定义类
通过上面的例子,我们可以知道,双亲机制可以
(1)避免类的重复加载
(2)保护程序安全,防止核心API被随意篡改
- 自定义类:java.lang.String (没用)
- 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
参考文档: