Java类加载机制-笔记4(双亲委派机制)

双亲委派机制

需求: 在默认情况下,一个限定名的类只会被一个类加载器加载并解析使用,这样在程序中,他就是不唯一的,不会产生歧义。

如何实现这种需求?
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的类,并在程序中调用它?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容