作者:ZeaTalk
链接:https://www.zhihu.com/question/49667892/answer/690161827
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
类加载器(classloader)
先从类加载器说起,凡事先问是什么,首先什么是类加载器?
我们知道,一个 *.java 的代码源文件要执行起来之前,必须通过 javac 构建抽象语法树并编译成字节码,字节码仍然是不能被机器所识别,那么一个 .class 文件要被机器识别并执行的前提就是将字节码转化成机器码加载到内存里,这一转化过程就是类加载的执行过程。
当然,这整个过程细节并非这个问题的讨论重点。
类加载器便是在在这个过程里的加载阶段起作用,负责将 .class 文件字节码提取出来,转化成二进制字节流。
(但是这离成为真正的类还有十万八千里。。。)
一个Java应用中通常存在三个类加载器(Classloader):
- Bootstrap Classloader 启动类加载器:负责加载<JRE>/lib 下的核心类库,此加载器本身内嵌于JVM,在 Java 中找不到具体的引用;
- Extension Classloader 扩展类加载器:负责加载<JRE>/lib/ext 下的扩展类库;
- Application Classloader 系统应用类加载器:负责-classpath 指定路径类库加载。
他们三者并非典型的继承关系,而是子指派父为自己的 parent。
(这里有个源码细节,由于启动类加载器是内嵌于 JVM 且无法被引用,因此 Extension Classloader 指派 parent 为 null,即等同于指派启动类加载器为自己的父加载器)
为了有足够灵活性,类加载器也是允许自定义的。这不禁思考,这么多类加载器之间是怎么协调类加载任务的?
这就引出了本文的重点,双亲委派模型。
双亲委派(parents deletation model)
双亲委派模型是什么?
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.
由于翻译问题,这里双亲的 “双” 并没有太多特殊含义,双亲就是父母。简单来说,当一个类加载器得到一个类加载任务 t 的时候,首先会委派其 parent A 去加载,A 拿到任务后,也会进一步委派到 A 的 parent B。层层向上递归直到委派到启动类加载器。
但我们知道,每个 classloader 负责的加载域是不一样的,启动类加载器需根据 t 给出的类全限定名(如 com.Test)在其所负责的域里搜寻此类字节码,如果找到,则加载之;如果找不到,则表示无法加载,把代理权限往下(父->子)转移,直到某个加载器在负责的加载域中找到该类为止。
这一逻辑的代码实现是这样的:
java.lang.ClassLoader#loadClass(java.lang.String, boolean)
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;
}
findLoadedClass :先确认类加载任务指定的类全限定名是否已经被加载,没有加载过才能委派;
findBootstrapClassOrNull :如果 parent 为空这是指定了 bootstrap 加载器。
为什么要有双亲委派模型?
一个类在同一个类加载器中具有唯一性(Uniqueness),而不同类加载器中是允许同名类存在的,这里的同名是指全限定名相同。但是在整个JVM里,纵然全限定名相同,若类加载器不同,则仍然不算作是同一个类,无法通过 instanceOf 、equals 等方式的校验。
由于唯一性的存在,Class 被替换就有可能了,而双亲委派模型定义了一套类加载的优先层级,很好的防止核心类库被恶意替换。毕竟核心类库是 bootstrap classloader 加载的,而 bootstrap 是内嵌于JVM的,在双亲委派模型基础上,任何类加载任务都会交由 bootstrap classloader 这位大佬经手过目一次,只要是核心类库中的类,都会被 bootstrap classloader 加载,间接确保核心类库不被其他类加载器加载。
换言之,在遵循了双亲委派模型的规则之下,是不允许出现核心类库被替换或取代的可能,即不能在自己的 classpath 定义 java.lang.*之类的Class去替换JRE 中的Class。
classloader 加载模型是否适用所有场景?
未必。这个模型最大的局限在于,假定 A 作为 B 的 parent,A 加载的类 对 B 是可见的; 然而 B 加载的类 对 A 却是不可见的。
这是由 classloader 加载模型中的可见性(visibility)决定的
Visibility principle allows child class loader to see all the classes loaded by parent ClassLoader, but parent class loader can not see classes loaded by child.
最典型不适用的场景便是 SPI 的使用。
SPI(Service Provider Interface)
Java 在核心类库中定义了许多接口,并且还给出了针对这些接口的调用逻辑,然而并未给出实现。开发者要做的就是定制一个实现类,在 META-INF/services 中注册实现类信息,以供核心类库使用。
java.sql.Driver 是最为典型的 SPI 接口,java.sql.DriverManager 通过扫包的方式拿到指定的实现类,完成 DriverManager的初始化。
等等,似乎有什么不对,根据双亲委派的可见性原则,启动类加载器 加载的 DriverManager 是不可能拿到 系统应用类加载器 加载的实现类 ,这似乎通过某种机制打破了双亲委派模型。
双亲委派模型并非强制模型
SPI 是如何打破双亲委派模型的呢?
java.sql.DriverManager#loadInitialDrivers
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
java.util.ServiceLoader#load(java.lang.Class<S>)
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
通过从线程上下文(ThreadContext)获取 classloader ,借助这个classloader 可以拿到实现类的 Class。
(源码上讲,这里是通过 Class.forName 配合 classloader拿到的)
线程上下文 classloader并非具体的某个loader,一般情况下是 application classloader, 但也可以通过 java.lang.Thread#setContextClassLoader 这个方法指定 classloader。
综上,为什么说 Java SPI 的设计会违反双亲委派原则呢?
首先双亲委派原则本身并非 JVM 强制模型。
SPI 的调用方和接口定义方很可能都在 Java 的核心类库之中,而实现类交由开发者实现,然而实现类并不会被启动类加载器所加载,基于双亲委派的可见性原则,SPI 调用方无法拿到实现类。
SPI Serviceloader 通过线程上下文获取能够加载实现类的classloader,一般情况下是 application classloader,绕过了这层限制,逻辑上打破了双亲委派原则。