JVM-11-双亲委派模型

JVM-11-双亲委派模型

前序

​ 在上一篇中,我们介绍了类加载的过程,包括五个阶段

  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化

mark

本篇博客,我们来介绍Java虚拟机的双亲委派模型,在介绍之前,我先抛出一个问题:

我们知道,在JDK源码中,有各种Java自带的类,比如java.lang.String,java.util.List等,那么我们自己的项目中,能够写一个命名为java.lang.String.java 等JDK源码中存在的类,并且在项目中使用吗?

1. 类加载器

什么是类加载器?上篇中我们介绍类加载的第一个阶段–加载,作用是“通过一个类的全限定名来获取描述这个文件的二进制流” ,那么这个加载过程就是类加载器来完成的

从Java虚拟机角度出发,只有两种不同的类加载器

  • 一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++ 语言实现,是虚拟机自身的一部分;
  • 另一种是所有其他类的加载器,这些类加载器都是由Java语言实现的。但是从Java开发人员角度来看,类加载器可以分为以下四种:
  1. 启动类加载器(Bootstrap ClassLoader)

负责将存放在 /lib 目录中的,或者被-Xbootclasspath 参数所指定的路径中的,并且是虚拟机按照文件名识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。

启动类加载器无法被Java程序直接使用。

JDK中的源码类大都是由启动类加载器加载,比如前面说的java.lang.String ,java.lang.List等,需要注意的是,启动类 main Class也是由启动类加载器加载。

  1. 扩展类加载器(Extension ClassLoader)

这个类加载器由 sun.misc.Launcher$ExtClassLoader 实现,负责加载<JAVA_HOME>/lib/ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。

开发人员可以直接使用扩展类加载器。

  1. 应用程序类加载器(Application ClassLoader)

sun.misc.Launcher$AppClassLoader 实现。由于这个类加载器是ClassLoader.getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。

它负责加载用户类路径ClassPath上所指定的类库,开发者可以直接使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

通常项目中自定义的类,都会放在类路径下,由应用程序类加载器加载。

  1. 自定义类加载器(User ClassLoader)

这是由用户自己定义的类加载器,一般情况下我们不会自定义类加载器,但是有些特殊情况下,比如JDBC能够通过连接各种不同的数据库就是由自定义类加载器实现的,具体在后文中会介绍。

2. 双亲委派模型

回到文章开头提出的问题,如果有不法分子在你项目中构造了一个java.lang.String类,并在该类中植入了一些不良代码,但你自己浑然不知,以为使用的String类还是 rt.jar 包下的,那可能会给你系统造成不良的影响。

聪明的Java虚拟机实现者也想到了这个问题,于是,他们引入双亲委派模型法来解决这个问题。

下面是双亲委派模型的加载流程机制:

mark

总结来说:

  • 双亲委派机制就是如果一个类加载器收到了类加载的请求,它首先不会自己尝试去加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此。

  • 因此,所有的加载请求最终都应该传送到了顶层的启动类加载器中,只有父类加载器反馈到无法完成这个加载请求(它的搜索范围没有找到这个类),子类加载器才会自己尝试去加载。

  •  其实,这里叫双亲委派可能有点不妥,因为按道理来讲只有父加载器,这里的“双亲”是“parents”的直译,并不表示汉语中的父母双亲。另外,这里的父加载器也不是继承的关系。

1
2
3
4
5
6
7
8
9
10
11
12
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader classLoader1 = ClassLoaderTest.class.getClassLoader();
ClassLoader classLoader2 = classLoader1.getParent();
ClassLoader classLoader3 = classLoader2.getParent();


System.out.println(classLoader1);
System.out.println(classLoader2);
System.out.println(classLoader3);
}
}

结果如下:

mark

那么知道了什么是双亲委派机制,双亲委派机制有什么好处呢?

回到上面提出的问题,如果你自定义了一个 java.lang.String类,你会发现这个自定义的String.java可以正常编译,但是永远无法被加载运行。因为加载这个类的加载器,会一层一层的往上推,最终由启动类加载器来加载,而启动类加载的会是源码包下的String类,不是你自定义的String类。

3. 双亲委派模型源码

可以打开 java.lang.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
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;
}
}

实现方式很简单:

  • 首先会检查该类是否已经被加载过了,若加载过了就直接返回(默认resolve取false)
  • 若没有被加载,则调用父类加载器方法,若父类加载器为空则默认使用启动类加载器组作为父类加载器,如果父类加载器失败,则抛出ClassNotFoundException 异常后,使用自己的findClass方法进行加载

4. 自定义类加载器

先说说我们为什么要自定义类加载器?

  1. 加密

我们知道Java字节码是可以进行反编译的,在某些安全性高的场景,是不允许这种情况发生的。那么我们可以将编译后的代码用某种加密算法进行加密,加密后的文件就不能再用常规的类加载器去加载类了。而我们自己可以自定义类加载器在加载的时候先解密,然后在加载。

  1. 动态创建

比如很有名的动态代理。

  1. 从非标准的来源加载代码

我们不用非要从class文件中获取定义此类的二进制流,还可以从数据库,从网络中,或者从zip包等。

明白了为什么要自定义类加载器,接下来我们再来详述如何自定义类加载器。

  • 通过上面第三小节的java.lang.ClassLoader 类的源码分析,类加载器根据双亲委派模型辉县一层层找到父类加载器,如果加载失败,则会调用当前加载器的findClass()方法来完成加载,因此我们自定义类加载器,需要两个步骤
  1. 继承 ClassLoader 类
  2. 重写findClass() 方法
打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2019-2022 Zhuuu
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信