1.一道面试题

这是一道经典的面试题:请描述一下Tomcat如何实现自己独特的类加载机制。

这道题可以引申出很多类加载器的细节问题:

例如:Tomcat为什么要实现一套与JVM不同的类加载机制?

例如:Tomcat有哪几种类加载器?

例如:双亲委派模型不满足Tomcat的要求吗?

例如:自己定义一个恶意系统类(比如Object类),会对Tomcat造成伤害吗?

这些细节问题,涉及到"类加载器"的一些原理性知识,结合笔者之前的文章《【类加载机制】从一道面试题开始》,我们一起来学习一下类加载器。

2.类加载器的实战价值

类加载器是JVM类加载机制的具体实现。

image-20201224172135407

  • 类加载器的第一个实战用途就是定位异常:当使用一些三方件时,并且部署在某些容器中,运行时环境一旦出现ClassNotFoundException,就需要利用类加载器的知识进行问题定位。
  • 类加载器的第二个实战用途就是安全:java的二进制天然地容易被反编译,为了进行二进制保护,通常会自定义类加载器。

3.类加载器的分类

在Java8中,类加载器的逻辑关系如下图所示:

image-20201224175405793

  • 从提供者看

JVM自带1个类加载器:BootstrapClassLoader

JDK自带2个类加载器:ExtClassLoaderAppClassLoader

  • 从关系看

BootstrapClassLoader:它会创建ExtClassLoader和AppClassLoader,它是ExtClassLoader的父级加载器。

ExtClassLoader是AppClassLoader的父级加载器。

  • 从实现看

BootstrapClassLoader:用C++实现

ExtClassLoader:用Java实现,继承自ClassLoader

AppClassLoader:用Java实现,继承自ClassLoader

  • 从职责看

BootstrapClassLoader:加载(JAVA_HOME/jre/lib/*.jar或sun.boot.class.path下所有内容,用于提供JVM自身需要的类。

ExtClassLoader:加载属性java.ext.dirs所指定的目录中的类库,或从JDK的安装目录jre/lib/ext/*.jar。

AppClassLoader:加载环境变量classpath中的jar或属性java.class.path指定路径下的类库。

  • 限制

BootstrapClassLoader:为了安全,只加载包名为java、javax、sun开头的类文件

JVM自带了上述3种类加载器,就不得不面临两个问题:

1.加载流程问题:有一个类A,哪个类加载器负责去加载?

2.唯一性问题:有一个类A,不同的类加载器能同时加载它吗?

4.相对唯一

类加载机制这样定义类的唯一性,必须满足如下两个条件:

  • 第一、加载到JVM中的类模板本身是相同的
  • 第二、加载这个类的加载器是同一个

第一个条件比较好理解,这个被加载的类,来自同一个class文件、类的全限定名相同

第二个条件则体现了哲学定义中的"唯一”——在一定约束下,事物具备独一无二的性质。

反过来理解第二个条件,我们可以得到如下推论:

  • 推论1:两个类加载器可以加载同一个类
  • 推论2:如果发生了推论1,则此时JVM中加载完成的2个类模板被JVM认为是2个不同的类模板。

image-20201225072929911

5.双亲委派

明白了"相对唯一性”,JVM还是要讲一点**“江湖规矩”**——在没有某些特殊诉求的情况下,还是要约束一下类加载器们不能太渣,不要反复加载同一个类模板。

JVM定义了这么一套加载规则:

  • 类加载器存在上下级关系:Bootstrap是一把手,Ext是二把手,App是小弟。
  • 类加载器存在先后顺序:一把手(Bootstrap)初始化的时候,将二把手(Ext)、小弟(App)给创建出来。
  • 类加载器存在加载询问机制:
    • 当小弟(App)准备加载1个类模板,会先去求助一下二把手(Ext)加载过这个类吗、能不能帮我加载一下?
    • 二把手(Ext)收到小弟(App)的求助,也会马上去求助一把手(Bootstrap)。
    • 一把手(Bootstrap)没有更上级可以求助,于是看看自己能否加载,如果不能,就把权力下放给二把手(Ext)。
    • 二把手(Ext)一样的处理逻辑,如果不能加载,就把权力下放给小弟(App)。
    • 绕了这么一大圈,小弟(App)还得自己加载。

这套加载规则,就是双亲委派模型,Parents Delegation Model,也被称为溯源委派加载模型。

为啥JVM为这种加载规则取了这么奇怪的名字?

笔者听到一个段子觉得比较贴切——说,类加载的相对唯一性就是”渣男逻辑",而双亲委派模型就是”妈宝模型":

小明(App)的家庭作业不会做,就叫妈妈(Ext)帮忙做。

妈妈(Ext)有点忙就叫奶奶(Bootstrap)做。

奶奶(Bootstrap)说我现在不舒服,还是妈妈(Ext)做吧。

妈妈(Ext)说我现在忙着做饭,小明(App)你自己来吧。

小明(App)看绕了一圈,还得自己做作业。

理解清楚这个"双亲"的含义,我们就知道在很多讲JVM原理的文章中,提到的父级加载器、父类加载器,不是表示这三种类加载器是父子关系,而是上下级关系。

image-20201225075247217

6.测试

我们用几个测试用例,验证一下:

6.1.验证三种类加载器的上下级关系

  • 代码:

先获得AppClassLoader,然后调用getParent(),获得上一级ClassLoader

image-20201225093801335

  • 运行结果:

说明AppClassLoader的上级是ExtClassLoader,ExtClassLoader的上级是Bootrap(C++实现,所以打印为null)

image-20201225094741928

6.2.验证BootstrapClassLoader已经加载的类

  • 代码

通过getBootstrapClassPath,获得Bootstrap加载器已经加载的类

再找到Bootstrap加载器已经加载的某一个类对应的类加载器,反过来验证它的类加载器是否是Bootstrap。

image-20201225095543911

  • 运行结果

Bootstrap的职责是加载JVM自身需要的类,目录如下图:

image-20201225095633865

6.3.验证ExtClassLoader已经加载的类

  • 代码

获得java.ext.dirs对应的文件路径,再选择该路径下的类,查看类加载器类型

image-20201225100147120

  • 运行结果

ExtClassLoader的职责是加载java.ext.dirs下的类

image-20201225100228926

6.4.尝试破坏java.lang包

  • 代码

自建一个java.lang.String类

image-20201225101209635

image-20201225101245483

  • 运行结果

类加载器加载的时候,认为这是一个被禁止的包路径,防御了我们伪造java.lang包下的基础类

image-20201225101409744

7.总结

类加载器是类加载机制的具体实现,本文讲解了如下知识点:

  • 类加载器的分类
  • 类加载器的相对唯一性
  • 双亲委派模型
  • 通过代码,验证了:
    • 类加载器之间的上下级关系
    • Bootstrap、Ext类加载器的职责
    • JVM如何防御我们对核心库的破坏行为

8.参考

《深入理解Java虚拟机》-周志明