双亲委派机制
需求: 在默认情况下,一个限定名的类只会被一个类加载器加载并解析使用,这样在程序中,他就是不唯一的,不会产生歧义。
如何实现这种需求?
JVM的开发者引入了双亲委派模型,这个名字听上去很高大上,其实逻辑非常简单,我们通过这张图来理解一下:
解释一下这张图,也就是说:在被动的情况下,当一个类收到加载请求,他不会首先自己去加载,而是传递给自己的父亲加载器,这样所有的类都会传递到最上层的Bootstrap ClassLoader ,只有父亲加载器无法完成加载,那么此时儿子加载器才会自己去尝试加载,什么叫无法加载?就是根据类的限定名类加载器没有在自己负责的加载路径中找到该类,这里注意:父亲加载器、儿子加载器,不同于父加载器,子加载器,因为上图中这些箭头并不表示继承关系,而是一种逻辑关系,实际上是通过组合的方式来实现的,这也是很多博客上没有写清楚的容易误导人的一点。接下来我们就通过源码来看下双亲委派机制具体是怎么实现的。
代码很简单(取自java.lang.ClassLoader):
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 {
// parent == null 代表 parent为bootstrap classloader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 说明parent加载不了,当前loader尝试 findclass
// 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;
}
}
首先检查该类是否已经被加载过,如果没有,则开启加载流程,如果有,则直接读取缓存。parent变量代表了当前classloader的父亲加载器,这里就体现了,不是通过继承而是通过组合的方式实现类加载器之间的 父子关系。如果parent==null,约定parent是bootstrap classloader ,因为最开始我们也说过,bootstrap classloader 是由JVM内部实现的,没有办法被程序引用,所以这里就约定为null,当parent为null,就调用findBootstrapClassOrNull这个方法,让bootstrap classloader 尝试进行加载,如果parent不为null,那么就让parent根据限定名去尝试加载该类,并返回class对象。如果返回的class对象为null,那么就说明parent没有能力去加载这个类,那么就调用findClass,findClass表示如何去寻找该限定名的class需要各个类加载器自己实现,比如Extension ClassLoader 和Application ClassLoader都使用了这段逻辑来实现自己的findClass。
(取自java.net.URLClassLoader)
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
这里可以看到,通过将类的限定名转化为文件path,再通过ucp这个对象去进行寻找,找到文件资源后,再调用defineClass去进行类加载的后续流程,
defineClass 方法(java.net.URLClassLoader)
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
int len = b.remaining();
// Use byte[] if not a direct ByteBufer:
if (!b.isDirect()) {
if (b.hasArray()) {
return defineClass(name, b.array(),
b.position() + b.arrayOffset(), len,
protectionDomain);
} else {
// no array, or read-only array
byte[] tb = new byte[len];
b.get(tb); // get bytes out of byte buffer.
return defineClass(name, tb, 0, len, protectionDomain);
}
}
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
defineClass 方法是由java.lang.ClassLoader中一个被final修饰的方法,意味着获取到class二进制流以后呢,最终将会由java.lang.classloader 来进行后续的操作,因为它是被final修饰的,即不允许被外部重写,这符合了我们最开始所说的类加载过程中除了读取二进制流的操作外剩余逻辑都是有JVM内部实现的设计,这就是双亲委派模型。
我们在看一下上回提到的两个问题:
问题:
1.不同的类加载器,除了读取二进制流的动作和范围不一样,后续的加载器逻辑是否也不一样?
2.遇到限定名一样的类,这么多类加载器会不会产生混乱?
解答:
1.我们认为除了Bootstrap ClassLoader,所有的非Bootstrap ClassLoader都继承了java.lang.ClassLoader,都由这个类的defineClass进行后续处理。
2.越核心的类库越被上层的类加载器加载,而某限定名的类一旦被加载过了,被动情况下,就不会再加载相同限定名的类。这样,就能够有效避免混乱。
破坏双亲委派
第一次破坏双亲委派
但是双亲委派模型,并不是一个具有强约束力的模型。因为它存在设计缺陷,在大部分被动情况下,也就是上层开发者正常写代码,没有骚操作的情况下,他是生效并且好用的。在一些情况下,双亲委派模型可以被主动破坏,细心的同学可能已经发现了,我上面自己写的用于被证明类加载器存在命名空间的demo就是一次对双亲委派模型的破坏,可以看到,这里自定义的类加载器直接重写了java.lang.ClassLoader的loadClass方法,而双亲委派的逻辑就是存在于这个方法内的,那么我的这个重写就代表了对原有双亲委派逻辑的破坏,所以就出现了一个限定名对应两种不同class的情况,
需要提出的 是,除非是有特殊的业务场景,一般来说不要去主动破坏双亲委派模型,那么JVM推荐并希望开发者遵循双亲委派模型,那么为什么不把loadClass方法像defineClass方法一样设定成final来修饰?那这样的情况,就没有办法去重写loadClass方法,也就代表着上层开发者尽量遵循双亲委派的逻辑了。
因为这是JVM开发者必须面对,但是无法解决的问题,java.lang.ClassLoader 的loadClass方法,在java很早的版本就有了,而双亲委派模型是在JDK1.2引入的特性,Java是向下兼容的,也就是说,引入双亲委派机制时,世界上已经存在了很多像上面一样的代码。JVM既然无法拒绝支持,只能默默接受,一点补救措施呢,就是在JDK1.2版本后引入了findClass方法,推荐用户去重写该方法而不是直接重写loadClass方法,这样就毅然能符合双亲委派,这是史上第一次破坏双亲委派。
第二次破坏双亲委派
我们举个例子:比如JDK想要提供操作数据库的功能。
那么数据库有很多种,并且随着时间的推移,将会出现更多的品种的数据库,比较合理的方式是,JDK提供一组规范、一组接口,各个不同的数据库厂商按照这个接口去自己实现自己类库。
这里就问题就出现了:
对JDK代码包中的加载肯定使用了上层的类加载器,比如说bootstrapClassLoader 但当你去调用JDK 中的接口时,接口所在的类将会引起第三方类库的加载这就不符合自下而上的委派加载顺序了,而是出现了上层类加载器放下身段去调用下层类加载器的情况,这就产生了对双亲委派模型的破坏。
这就是Java的SPI
我们可以把SPI理解成一种服务发现机制,各大厂商的服务注册到JDK提供的接口上,上层在调用JDK的接口时,JDBC是SPI的其中一种功能,在上面的例子中我们在JDBC上注册了mysql Driver,h2 Driver这两种服务,那么这里SPI究竟是如何对双亲委派进行破坏的呢,我们看一下DriverManager的源码来简单看一下:
可以看到DriverManager会主动的对第三方Driver进行加载,扫描到所有注册为java.sql.driver类型的第三方类就使用serviceLoader去进行加载,而serviceLoader内部使用了当前线程context中的类加载器,一般线程context中的类加载器默认为application ClassLoader ,所以这些第三方类也就能够被正常加载了,所以再结合这些输出内容。
第三次破坏双亲委派
随着人们对模块化的追求,希望在程序运行时,能够动态的对部分组件代码进行替换,这就是所谓的热替换、热部署,想想也能够大致猜到,这里又将会出现很多的自由的类加载操作,所以又将是一次对双亲委派模型的践踏。
问题: 能不能自己写一个限定名为java.lang.String的类,并在程序中调用它?