1.一道面试题
这是一道经典的面试题:请描述一下Tomcat如何实现自己独特的类加载机制。
这道题可以引申出很多类加载器的细节问题:
例如:Tomcat为什么要实现一套与JVM不同的类加载机制?
例如:Tomcat有哪几种类加载器?
例如:双亲委派模型不满足Tomcat的要求吗?
例如:自己定义一个恶意系统类(比如Object类),会对Tomcat造成伤害吗?
这些细节问题,涉及到"类加载器"的一些原理性知识,结合笔者之前的文章《【类加载机制】从一道面试题开始》,我们一起来学习一下类加载器。
2.类加载器的实战价值
类加载器是JVM类加载机制的具体实现。
- 类加载器的第一个实战用途就是定位异常:当使用一些三方件时,并且部署在某些容器中,运行时环境一旦出现ClassNotFoundException,就需要利用类加载器的知识进行问题定位。
- 类加载器的第二个实战用途就是安全:java的二进制天然地容易被反编译,为了进行二进制保护,通常会自定义类加载器。
3.类加载器的分类
在Java8中,类加载器的逻辑关系如下图所示:
- 从提供者看
JVM自带1个类加载器:BootstrapClassLoader
JDK自带2个类加载器:ExtClassLoader、AppClassLoader
- 从关系看
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个不同的类模板。
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原理的文章中,提到的父级加载器、父类加载器,不是表示这三种类加载器是父子关系,而是上下级关系。
6.测试
我们用几个测试用例,验证一下:
6.1.验证三种类加载器的上下级关系
- 代码:
先获得AppClassLoader,然后调用getParent(),获得上一级ClassLoader
- 运行结果:
说明AppClassLoader的上级是ExtClassLoader,ExtClassLoader的上级是Bootrap(C++实现,所以打印为null)
6.2.验证BootstrapClassLoader已经加载的类
- 代码
通过getBootstrapClassPath,获得Bootstrap加载器已经加载的类
再找到Bootstrap加载器已经加载的某一个类对应的类加载器,反过来验证它的类加载器是否是Bootstrap。
- 运行结果
Bootstrap的职责是加载JVM自身需要的类,目录如下图:
6.3.验证ExtClassLoader已经加载的类
- 代码
获得java.ext.dirs对应的文件路径,再选择该路径下的类,查看类加载器类型
- 运行结果
ExtClassLoader的职责是加载java.ext.dirs下的类
6.4.尝试破坏java.lang包
- 代码
自建一个java.lang.String类
- 运行结果
类加载器加载的时候,认为这是一个被禁止的包路径,防御了我们伪造java.lang包下的基础类
7.总结
类加载器是类加载机制的具体实现,本文讲解了如下知识点:
- 类加载器的分类
- 类加载器的相对唯一性
- 双亲委派模型
- 通过代码,验证了:
- 类加载器之间的上下级关系
- Bootstrap、Ext类加载器的职责
- JVM如何防御我们对核心库的破坏行为
8.参考
《深入理解Java虚拟机》-周志明