一句话概括
- 双亲委派机制保证了JVM的严谨性、安全性
- 双亲委派机制标准有四层父子关系classLoader(类加载器)(并非父子继承关系,而是机制定义的上下层关系)
- 类加载的验证:每个类由指定path的classLoader加载,加载前会先判断上层父类classLoader是否已加载相同包名+类名的类,若已加载则直接获取类对象使用,不会再加载;若没加载则继续传递上层判断
- 类加载的执行:经过验证判断均没加载过该类后,委派最上级classLoader执行加载,若在自己加载范围内无法加载则向下传递
- 打破双亲委派机制:tomcat、SPI、OSGi
类加载器classLoader
整个类加载过程任务非常繁重,虽然这活儿很累,但总得有人干。类加载器做的就是上面 5 个步骤的事。
如果你在项目代码里,写一个 java.lang 的包,然后改写 String 类的一些行为,编译后,发现并不能生效。JRE 的类当然不能轻易被覆盖,否则会被别有用心的人利用,这就太危险了。
那类加载器是如何保证这个过程的安全性呢?其实,它是有着严格的等级制度的。
类加载器的种类
- Bootstrap ClassLoader
这是加载器中的大 Boss,任何类的加载行为,都要经它过问。它的作用是加载核心类库,也就是 rt.jar、resources.jar、charsets.jar 等。当然这些 jar 包的路径是可以指定的,-Xbootclasspath 参数可以完成指定操作。
这个加载器是 C++ 编写的,随着 JVM 启动。
- Extention ClassLoader
扩展类加载器,主要用于加载 lib/ext 目录下的 jar 包和 .class 文件。同样的,通过系统变量 java.ext.dirs 可以指定这个目录。
这个加载器是个 Java 类,继承自 URLClassLoader。
- App ClassLoader
这是我们写的 Java 类的默认加载器,有时候也叫作 System ClassLoader。一般用来加载 classpath 下的其他所有 jar 包和 .class 文件,我们写的代码,会首先尝试使用这个类加载器进行加载。
这个加载器是个 Java 类,继承自 URLClassLoader。
它的父类加载器为Extention ClassLoader
- Custom ClassLoader
自定义加载器,支持一些个性化的扩展功能。
双亲委派机制
双亲委派机制的意思是除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委派给它的父加载器进行加载。这样一层层向上传递,直到祖先们都无法胜任,它才会真正的加载。
打个比方。有一个家族,都是一些听话的孩子。孙子想要买一块棒棒糖,最终都要经过爷爷过问,如果力所能及,爷爷就直接帮孙子买了。
但你有没有想过,“类加载的双亲委派机制,双亲在哪里?明明都是单亲?”
我们还是用一张图来讲解。可以看到,除了启动类加载器,每一个加载器都有一个parent,并没有所谓的双亲。但是由于翻译的问题,这个叫法已经非常普遍了,一定要注意背后的差别。
我们可以翻阅 JDK 代码的 ClassLoader#loadClass 方法,来看一下具体的加载过程。和我们描述的一样,它首先使用 parent 尝试进行类加载,parent 失败后才轮到自己。同时,我们也注意到,这个方法是可以被覆盖的,也就是双亲委派机制并不一定生效。
这个模型的好处在于 Java 类有了一种优先级的层次划分关系。比如 Object 类,这个毫无疑问应该交给最上层的加载器进行加载,即使是你覆盖了它,最终也是由系统默认的加载器进行加载的。
如果没有双亲委派模型,就会出现很多个不同的 Object 类,应用程序会一片混乱。
详谈双亲委派的好处
这种委托双亲的模式保证了Java核心不会被恶心篡改:
启动类加载器可以抢在标准扩展类装载器之前去装载类,而标准扩展类装载器可以抢在类路径加载器之前去装载那个类,类路径装载 器又可以抢在自定义类加载器之前去加载它。所以Java虚拟机先从最可信的Java核心API查找类型,这是为了防止不可靠的类扮演被信任的类。
试想一下,网络上有个名叫java.lang.Integer的类,它是某个黑客为了想混进java.lang包所起的名字,实际上里面含有恶意代码,但是这种伎俩在双亲模式加载体系结构下是行不通的,因为网络类加载器在加载它的时候,它首先调用双亲类加载器,这样一直向上委托,直到启动类加载器,而启动类加载 器在核心Java API里发现了这个名字的类,所以它就直接加载Java核心API的java.lang.Integer类,然后将这个类返回,所以自始自终网络上的 java.lang.Integer的类是不会被加载的。但是如果这个移动代码不是去试图替换一个被信任的类(就是前面说的那种情况),而是想在一个被信任的包中插入一个全新的类型,情况会怎样呢?
比如一个名为 java.lang.Virus的类,经过双亲委托模式,最终类装载器试图从网络上下载这个类,因为网络类装载器的双亲们都没有这个类(当然没有了,因为是病毒嘛)。假设成功下载了这个类,那你肯定会想,Virus和lang下的其他类痛在java.lang包下,暗示这个类是Java API的一部分,那么是不是也拥有修改Java.lang包中数据的权限呢?
答案当然不是,因为要取得访问和修改java.lang包中的权限,java.lang.Virus和java.lang下其他类必须是属于同一个运行时包的,什么是运行时包?运行时包是指由同一个类装载器装载的、属于同一个包的、多个类型的集合。考虑一下,java.lang.Virus和java.lang其他类是同一个类装载器装载的吗?不是的!java.lang.Virus是由网络类装载器装载的!
打破双亲委派机制(自定义加载器)
下面我们就来聊一聊可以打破双亲委派机制的一些案例。为了支持一些自定义加载类多功能的需求,Java 设计者其实已经作出了一些妥协。
案例一:tomcat
tomcat 通过 war 包进行应用的发布,它其实是违反了双亲委派机制原则的。简单看一下 tomcat 类加载器的层次结构。
对于一些需要加载的非基础类,会由一个叫作 WebAppClassLoader 的类加载器优先加载。等它加载不到的时候,再交给上层的 ClassLoader 进行加载。这个加载器用来隔绝不同应用的 .class 文件,比如你的两个应用,可能会依赖同一个第三方的不同版本,它们是相互没有影响的。
如何在同一个 JVM 里,运行着不兼容的两个版本,当然是需要自定义加载器才能完成的事。
那么 tomcat 是怎么打破双亲委派机制的呢?可以看图中的 WebAppClassLoader,它加载自己目录下的 .class 文件,并不会传递给父类的加载器。但是,它却可以使用 SharedClassLoader 所加载的类,实现了共享和分离的功能。
但是你自己写一个 ArrayList,放在应用目录里,tomcat 依然不会加载。它只是自定义的加载器顺序不同,但对于顶层来说,还是一样的。(也就是仍会自上而下检查是否有相同类已经加载)
案例二:SPI
Java 中有一个 SPI 机制,全称是 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。
这个说法可能比较晦涩,但是拿我们常用的数据库驱动加载来说,就比较好理解了。在使用 JDBC 写程序之前,通常会调用下面这行代码,用于加载所需要的驱动类。
Class.forName("com.mysql.jdbc.Driver")
这只是一种初始化模式,通过 static 代码块显式地声明了驱动对象,然后把这些信息,保存到底层的一个 List 中。但你会发现,即使删除了 Class.forName 这一行代码,也能加载到正确的驱动类。
简而言之,通过在 META-INF/services 目录下,创建一个以接口全限定名为命名的文件(内容为实现类的全限定名),即可自动加载这一种实现,这就是 SPI。
SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,主要使用 java.util.ServiceLoader 类进行动态装载。
但这个ServiceLoader是属于rt.jar的,它的类加载器是 Bootstrap ClassLoader,也就是最上层的那个。而具体的数据库驱动,却属于业务代码,这个启动类加载器是无法加载的。
即ServiceLoader加载时是由Bootstrap ClassLoader加载,但加载ServiceLoader必须加载具体的类方法实现,而类方法实现在应用代码,因由App ClassLoader加载,故出现了反向双亲委派的情况。
在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载。而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而 线程上下文类加载器(双亲委派模型的破坏者)就是很好的选择。
从图可知rt.jar核心包是有Bootstrap类加载器加载的,其内包含SPI核心接口类,由于SPI中的类经常需要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,当然这也使得Java类加载器变得更加灵活。
我们可以一步步跟踪代码,来看一下这个过程。
//part1:DriverManager::loadInitialDrivers
//jdk1.8 之后,变成了lazy的ensureDriversInitialized
...
ServiceLoader <Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
...
//part2:ServiceLoader::load
public static <T> ServiceLoader<T> load(Class<T> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
通过代码你可以发现 Java 玩了个魔术,它把当前的类加载器,设置成了线程上下文类加载器。那么,对于一个刚刚启动的应用程序来说,它当前的线程上下文加载器是谁呢?也就是说,启动 main 方法的那个加载器,到底是哪一个?
所以我们继续跟踪代码。找到 Launcher 类,就是 jre 中用于启动入口函数 main 的类。我们在 Launcher 中找到以下代码。
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
...
}
到此为止,事情就比较明朗了,当前线程上下文类加载器,是应用程序类加载器。使用它来加载第三方驱动,是没有什么问题的。
这样就可以更好的看清一个打破规则的案例。
案例三:OSGi
OSGi 曾经非常流行,Eclipse 就使用 OSGi 作为插件系统的基础。OSGi 是服务平台的规范,旨在用于需要长运行时间、动态更新和对运行环境破坏最小的系统。
OSGi 规范定义了很多关于包生命周期,以及基础架构和绑定包的交互方式。这些规则,通过使用特殊 Java 类加载器来强制执行,比较霸道。
比如,在一般 Java 应用程序中,classpath 中的所有类都对所有其他类可见,这是毋庸置疑的。但是,OSGi 类加载器基于 OSGi 规范和每个绑定包的 manifest.mf 文件中指定的选项,来限制这些类的交互,这就让编程风格变得非常的怪异。但我们不难想象,这种与直觉相违背的加载方式,肯定是由专用的类加载器来实现的。
随着 jigsaw 的发展(旨在为 Java SE 平台设计、实现一个标准的模块系统),我个人认为,现在的 OSGi,意义已经不是很大了。OSGi 是一个庞大的话题,你只需要知道,有这么一个复杂的东西,实现了模块化,每个模块可以独立安装、启动、停止、卸载,就可以了。
不过,如果你有机会接触相关方面的工作,也许会不由的发出感叹:原来 Java 的类加载器,可以玩出这么多花样。