一篇图文彻底弄懂类加载器与双亲委派机制

无论你是跟同事、同学、上下级、同行、或者面试官讨论技术问题的时候,很容易卷入JVM大型撕逼现场。为了能够让大家从大型撕逼现场中脱颖而出,最近我苦思冥想如何把知识点尽可能呈现的容易理解,方便记忆。于是就开启了这一系列文章的编写。为了让JVM相关知识点能够形成一个体系,arthinking将编写整理一系列的专题,以尽量以图片的方式描述相关知识点,并且最终把所有相关知识点串成了一张图。持续更新中,欢迎大家阅读。有任何错落之处也请您高抬贵手帮忙指正,感谢!

导读:

  1. 类加载器是怎么被创建出来的?
  2. 什么是双亲委派机制?为什么要有这种机制?
  3. Class实例和类加载器究竟是在Java Heap中,还是在方法区中?

类加载器: 可以实现通过一个类的全限定名称来获取描述此类的二进制字节流。实现这个动作的代码模块成为”类加载器“。

通过自定义类加载器可以实现各种有趣而强大的功能更:OSGi,热部署,代码加密等。

1、类加载器的加载流程

image-20200105144705999

如上图为类加载器的加载流程。

这里简单描述下:

1.1、启动类加载器

启动类加载器:系统启动的时候,首先会通过由C++实现的启动类加载器,加载<JAVA_HOME>/lib目录下面的jar包,或者被-Xbootclasspath参数指定的路径并且被虚拟机识别的文件名的jar包。把相关Class加载到方法区中。

这一步会加载关键的一个类:sun.misc.Launcher。这个类包含了两个静态内部类:

  • ExtClassLoader:扩展类加载器内部类,下面会讲;
  • AppClassLoader:应用程序类加载器内部类,下面会讲

可以反编译rt.jar文件查看详细代码:

image-20200105124613663
image-20200105131342939

在加载到Launcher类完成后,会对该类进行初始化,初始化的过程中,会创建 ExtClassLoader 和 AppClassLoader,源码如下:

public Launcher() {
    ExtClassLoader extClassLoader;
    try {
      extClassLoader = ExtClassLoader.getExtClassLoader();
    } catch (IOException iOException) {
      throw new InternalError("Could not create extension class loader", iOException);
    }
    try {
      this.loader = AppClassLoader.getAppClassLoader(extClassLoader);
    } catch (IOException iOException) {
      throw new InternalError("Could not create application class loader", iOException);
    }
    Thread.currentThread().setContextClassLoader(this.loader);
    ...

由于启动类加载器是由C++实现的,所以在Java代码里面是访问不到启动类加载器的,如果尝试通过String.class.getClassLoader()获取启动类的引用,会返回null

问题:

  1. 启动类加载器,扩展类加载器和应用类加载器都是又谁加载的?

    1. 启动类加载器是JVM的内部实现,在JVM申请好内存之后,由JVM创建这个启动类加载器
    2. 扩展类加载器和应用程序类加载器是由启动类加载器加载进来的;
  2. 说说以下代码输出什么:

 public static void main(String[] args) {
     System.out.println("加载当前类的加载器:" + TestClassLoader.class.getClassLoader());
        System.out.println("加载应用程序类加载器的加载器"
                         + TestClassLoader.class.getClassLoader().getClass().getClassLoader());
        System.out.println("String类的启动类加载器" + String.class.getClassLoader());
   }

1.2、扩展类加载器

如上图,扩展类加载器负责加载<JAVA_HOME>/lib/ext目录下或者被java.ext.dirs系统变量指定的路径中的类。

1.3、应用程序类加载器

引用程序类加载器加载用户类路径下制定的类库,如果应用程序没有自定义过自己的类加载器,此类加载器就是默认的类加载器。

引用程序类加载器也叫系统类加载器,可以通过getSystemClassLoader方法得到应用程序类加载器。

注意,如上图通过以上三个类加载器加载类到方法区之后,方法区中分别对应有各自的类信息存储区。不同类加载器加载的同一个类文件不相等。

2、类加载器的双亲委派机制

2.1、双亲委派机制原理

双亲委派模型在JDK1.2之后被引入,并广泛使用,这不是一个强制性的约束模型,二货思Java设计者推荐给开发者的一种类加载器实现方式。我们也可以覆盖对应的方式,实现自己的加载模型。

类加载器的双亲委派机制如下:

image-20200105170731274

也就是说:

  • 一个类加载器收到了类加载请求,不会自己立刻尝试加载类,而是把请求委托给父加载器去完成,每一层都是如此,所有的家在请求最终都传递到最顶层的类加载器进行处理;
  • 如果父加载器不存在了,那么尝试判断有没有被启动类加载器加载;
  • 如果的确没有被夹在,则再自己尝试加载。

问题:

  1. 为什么要有这么复杂的双亲委派机制?
    1. 如果没有这种机制,我们就可以篡改启动类加载器中需要的类了,如,修自己编写一个java.lang.Object用自己的类加载器进行加载,系统中就会存在多个Object类,这样Java类型体系最基本的行为也就无法保证了。

2.2、双亲委派机制处理流程

JDK中默认的双亲委派处理流程是怎么的呢?接下来我们看看代码,以下是java.lang.ClassLoader.loadClass()方法的实现:

    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 {
                        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;
        }
    }

转成流程图,即是:

image-20200105174045231

如山图所以,总是先回尝试让父类加载器先加载,其次判断启动类加载器是否已经加载了,最后才尝试从当前类加载器加载。转换为更清晰的模型如下:

image-20200105195158889

双亲委派模型具有以下特点:

  • 可见性原则:
    • 应用类加载器是可以读取到由扩展类加载器和启动类加载器加载进来的Class的;
    • 扩展类加载器是可以读取到由启动类加载器加载进来的Class的;
  • 唯一性:
    • 类是唯一的,没有重复的类;

2.3、类加载器和Class实例的题外话

启动类加载器,扩展类加载器,应用程序类加载器,他们分别管理者各自方法区里的一个区块。

根据上一篇文章我们知道,方法区里面主要存储的是类的运行时数据结构,这个类的在方法区中的各种数据结构信息通过类的Class实例进行访问。

如下图:

image-20200105200625589

方法区里面存储着加载进来的类信息,方法区同时雇佣了两类工种帮忙干活:

  • 类加载器:负责管理各个存储区的类信息,如加载和卸载类信息;
  • Class实例:负责对接外部需求,如果外部有人想查看里面的类信息,则需要通过Class实例来获取;

另外,方法区里面,启动类加载器类信息对扩展两类加载器类信息可见,而前面两者的类信息又对应用程序类加载器类信息可见。

3、其他非双亲委派模型的案例

3.1、JDK 1.0遗留问题

在JDK1.0已经存在了ClassLoader类,但是当时还没有双亲委派机制,用户为了自定义类加载器,需要重新loadClass()方法,而我们知道,在JDK1.2以后,loadClass里面就是双亲委派机制的实现代码,此时,要实现自定义类加载器,需要重新findClass()类即可。

如果重新了loadClass()方法,也就意味着不再遵循双亲委派模型了。

3.2、线程上下文类加载器

为什么需要这个东西呢,我们还是从一个案例来说起。

Tomcat中的类加载器

我们知道Tomcat目录结构中有以下目录:

  • /common/: 该目录下的类库可被Tomcat和所有的WebApp共同使用;

  • /server/: 该目录下的类库可被Tomcat使用,但对所有的WebApp不可见;

  • /shared/: 该目录下的类库可被所有的WebApp共同使用,但对Tomcat自己不可见;

另外Web应用程序还有自身的类库,放在/WebApp/WEB-INF目录中:这里面的类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
为了实现以上各个目录的类库可见性效果,Tomat提供了如下的自定义类加载器:

image-20200105205509075

现在如下场景:

我们发现Tomcat下面有若干个webapp,每个webapp都用到了spring,于是我们把spring的jar包放到了shared目录中。

于是问题出现了:由于spring的jar包是由Shared类加载器加载的,假设我们要使用SpringContext的getBean方法,获取webapp中的Bean,如果是按照双亲委派模型,就会有问题了,因为webapp中的Java类是对SharedClassLoader不可见的:

image-20200105213630571

Spring中的线程上下文类加载器

为了解决这个问题,Spring使用了线程上下文类加载器,即从ThreadLocal中获取到当前线程的上下文类加载器,来加载所有的类库和类。

关于Spring初始化源码相关解读,参考我的这边文章:Spring IoC原理剖析

Spring中的bean类加载器

ApplicationContext中有一个beanClassLoader字段,这个是bean的类加载器,在prepareBeanFactory()方法中做了初始化:

beanFactory.setBeanClassLoader(getClassLoader());

getClassLoader方法如下:

    @Override
    @Nullable
    public ClassLoader getClassLoader() {
        return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
    }

ClassUtils.getDefaultClassLoader()方法:

    @Nullable
    public static ClassLoader getDefaultClassLoader() {
        ClassLoader cl = null;
        try {
            cl = Thread.currentThread().getContextClassLoader();
        }
        catch (Throwable ex) {
            // Cannot access thread context ClassLoader - falling back...
        }
        if (cl == null) {
            // No thread context class loader -> use class loader of this class.
            cl = ClassUtils.class.getClassLoader();
            if (cl == null) {
                // getClassLoader() returning null indicates the bootstrap ClassLoader
                try {
                    cl = ClassLoader.getSystemClassLoader();
                }
                catch (Throwable ex) {
                    // Cannot access system ClassLoader - oh well, maybe the caller can live with null...
                }
            }
        }
        return cl;
    }

可以发现,这里最终取了当前线程上下文中的ClassLoader。

加载Bean

我们来看看Spring加载Class的代码。这里我们直接找到实例化Singletons的方法跟进去找需要关注的代码:

我们发现在加载Bean Class的时候调用了这个方法:

AbstractBeanFactory:

ClassLoader beanClassLoader = getBeanClassLoader();

也就是用到了ApplicationContext中的beanClassLoader,线程上下文类加载器来加载Bean Class实例。

总结

Spring作为一个第三方类库,可能被任何的ClassLoader加载,所以最灵活的方式是直接使用上下文类加载器。

3.3、模块热部署

主要是类似OSGi这类的模块化热部署技术。在OSGi中不再是双亲委派模型中的树状结构,而是更复杂的网状结构。

References

Where are static methods and static variables stored in Java?

ClassLoader in Java

真正理解线程上下文类加载器(多案例分析)

《深入理解Java虚拟机-JVM高级特性与最佳实践》

Chapter 5. Loading, Linking, and Initializing


本文为arthinking基于相关技术资料和官方文档撰写而成,确保内容的准确性,如果你发现了有何错漏之处,烦请高抬贵手帮忙指正,万分感激。

大家可以关注我的博客:itzhai.com 获取更多文章,我将持续更新后端相关技术,涉及JVM、Java基础、架构设计、网络编程、数据结构、数据库、算法、并发编程、分布式系统等相关内容。

如果您觉得读完本文有所收获的话,可以关注我的账号,或者点赞的,您的支持就是我写作的动力!关注我的公众号,及时获取最新的文章。


本文作者: arthinking

博客链接: https://www.itzhai.com/jvm/what-is-classloader-and-what-is-parents-delegation-model.html

一篇图文彻底弄懂类加载器与双亲委派机制

版权声明: 版权归作者所有,未经许可不得转载,侵权必究!联系作者请加公众号。


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