深入浅出 JVM ClassLoader

# 前言

在 JVM 综述里面,我们说,JVM 做了三件事情,Java 程序的内存管理,
Java Class 二进制字节流的加载(ClassLoader),Java 程序的执行(执行引擎)。我们也说,我们大部分情况下只关注前2个。在前面的文章中,我们已经分析了内存关系相关的,包括运行时数据区,GC 相关。今天我们要讲的就是类加载器。

JVM 综述 里,我们已经大致分析了一些概念。而今天的文章将详细的阐述类加载器。

首先,我们要了解类加载器,当然,了解的目的是为了更好的开发,通过对类加载器的解读,看看我们能不能做些什么,比如修改类加载器的加载逻辑,比如加入自定义的类加载器等等功能。

让我们开始吧!

# 1. 类加载器介绍

对于 Java 虚拟机来说,Class 文件是一个重要的接口,无论使用何种语言进行软件开发,只要能将源文件编译为正确的 Class 文件,那么这种语言就可以在 Java 虚拟机上运行。可以说,Class 文件就是虚拟机的基石。

如图所示:

各种语言都可以在 JVM 上运行

从上图可以看出,虚拟机不拘泥于 Java 语言,任何一个源文件只要能编译成 Class 文件的格式,就可以在JVM 上运行!Class 文件格式就像是一个接口,只要遵守这个接口,就能够在 JVM 上运行。

# 2. 类加载器的工作流程

Class 文件通常是以文件的方式存在(任何二进制流都可以是 Class 类型),但只有能被 JVM 加载后才能被使用,才能运行编译后的代码。系统装在 Class 类型可以分为加载,链接和初始化三个步骤。其中,链接也可分为验证,准备和解析3步骤。如图所示:

Class 文件转载过程

其中,只有加载过程是程序员能够控制的,后面的几个步骤都是有虚拟机自动运行的。因此,我们的关注点主要放在加载阶段。

# 3. 类加载流程中的 “加载”

上面说了,类加载器3个流程中,唯一能让程序员 “做手脚” 的就是加载过程,上面是加载过程呢?其主要作用就是从系统外部获得 Class 二进制数据流。

JVM 不会无故装载 Class 文件,只有在必要的时候才装载,哪几个时候呢?

  1. 当创建一个类的实例是,比如使用 new 关键字,或者通过反射,克隆,反序列化。
  2. 当调用类的静态方法时,即当使用字节码 invokstatic 指令。
  3. 当使用类或接口的静态字段时(final 常量除外),比如,使用 getstatic 或者 pustatic 指令。
  4. 当时用 Java.lang.reflect 包中的方法反射类的方法时。
  5. 当初始化子类,要求先初始化父类。
  6. 作为启动虚拟机,含有 main()方法的那个类。

以上6种情况属于主动调用,主动调用会触发初始化,还有一种情况是被动调用,则不会引起初始化。

# 3.1 ClassLoader 抽象类介绍

Java 类加载器的具体实现就在 java.lang.ClassLoader,该类是一个抽象类,并且提供了一些重要的接口,用于自定义Class 的加载流程和加载方式。主要方法如下:

  1. public Class<?> loadClass(String name) throws ClassNotFoundException
    给定一个类名,加载一个雷,返回代表这个类的 Class 实例,如果找不到类,则返回异常。

  2. protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
    根据给定的字节码流 b 定义一个类,off 表示位置,len 表示长度。该方法只有子类可以使用。

  3. protected Class<?> findClass(String name) throws ClassNotFoundException
    查找一个类,也是只能子类使用,这是重载 ClassLoader 时,最重要的系统扩展点。这个方法会被 loadClass 调用,用于自定义查找类的逻辑,如果不需要修改类加载默认机制,只是想改变类加载的形式,就可以重载该方法。

  4. protected final Class<?> findLoadedClass(String name)
    同样的,这个方法也只有子类能够使用,他会去寻找已经加载的类,这个方法是 final 方法,无法被修改。

同时,在该类中,还有一个字段非常重要:parent,他也是一个 ClassLoader 的实例,这个字段所表示的 ClassLoader 也称为这个 ClassLoader 的双亲,在类加载的过程中,ClassLoader 可能会将某些请求交给自己的双亲处理。

# 3.2 类加载器的双亲委派模型

在标准的 Java 程序中,从虚拟机的角度讲,只有2种类加载器:

  1. 启动类加载器(BootStrap ClassLoader),C++ 语言实现,虚拟机自身的一部分
  2. 另一种就是所有其他的类加载器,由 Java 语言实现,独立于虚拟机外部,并且全部继承自抽身类 java.lang.ClassLoader。

从程序员的角度讲,虚拟机会创建 3 中类加载器,分别是:Bootstrap ClassLoader(启动类加载器),Extension ClassLoader(扩展类加载器)和 APPClassLoader(应用类加载器,也称为系统类加载器)。此外,每一个应用程序还可以拥有自定义的 ClassLoader,扩展 Java 虚拟机获取 Class 数据的能力。

而这 3 个类加载器有着层次关系。

先来看一个著名的图:

类加载器双亲委派模型

如图所示:从 ClassLoader 的层次自顶向下为启动类加载器,扩展类加载器,应用类加载器和自定义类加载器,当系统需要适用一个类时,在判断类是否已经被加载时,会先从当前底层类加载器进行判断,但系统需要加载一个类时,会从顶层类开始加载,依次向下尝试,直到成功。

注意,我们无法访问启动类加载器,当试图获取启动类加载器的时候,返回 null,因此,如果返回的是 null,并不意味没有类加载器为它服务,而是指哪个类为启动类加载器。

那么这些类加载路径是哪些呢?

  1. BootStrap 类加载器负责加载 <JAVA_HOME>/lib 目录中的,或者别-Xbootclasspath 参数指定的路径。并且是被虚拟机识别的,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会加载。

  2. 扩展类加载器有 sun.misc.Launcher$ExtClassLoader 实现,负责加载 <JAVA_HOME>/lib/ext 目录中的。或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。

  3. 应用类加载器由 sun.misc.Launcher$AppClassLoader 实现,由于这个类是 ClassLoader 中的 getSystemClassLoader 方法的返回值,也称为系统类加载器,负载加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器。一般情况下,这个就是程序中默认的类加载器。

  4. 自定义类加载器用于加载一些特殊途径的类,一般也是用户程序类。

系统中的 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 {
                        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;
        }
    }

代码中,如果双亲是 null,则使用启动类加载器加载,如果事变,则使用当前类加载器加载。

双亲为 null 一般有2种情况,1. 双亲是启动类加载器。2. 自己就是启动类加载器。

其中加载类的逻辑有2个注意的地方。

  1. 判断是否已经加载?当判断类是否需要加载时,是从底层开始判断,如果底层已经加载了,则不再请求双亲。

  2. 当系统准备加载一个类时。会先从双亲加载,也就是最顶层的启动类加载器,逐层向下,直到找到该类。和上面的是相反的。

# 3.3 类加载器的双亲委派模型缺陷和补充

双亲模型固然有着优点,能够让整个系统保持了类的唯一性。但在有些场合,却不适合,也就是说,顶层的启动类加载器的代码无法访问到底层的类加载器。如 rt.jar 无法中代码无法访问到应用类加载器。

你肯定要问,为什么需要访问呢?

在 Java 平台中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,通常可以称为 Service Provider Interface,即 SPI。

在 rt.jar 中的抽象类需要加载继承他们的在应用层的子类实现,但是以目前的双亲机制是无法实现的。

因此 JDK 引用了一个不太优雅的设计,上下文类加载器。也就是讲类加载放在线程上下文变量中。通过 Thread.getContextClassLoader(), Thread.setContextClassLoader(ClassLoader) 这两个方法获取和设置 ClassLoader,这样,rt.jar 中的代码就可以获取到底层的类加载了。

# 3.4 突破双亲模式

双亲模式是虚拟机的默认行为,但并非必须这么做,通过重载 ClassLoader 可以修改该行为。事实上,很多框架和软件都修改了,比如 Tomcat,OSGI。具体实现则是通过重写 loadClass 方法,改变类的加载次序。比如先使用自定义类加载器加载,如果加载不到,则交给双亲加载。

# 4. 类加载的扩展---热替换

我们知道:由不同的 ClassLoader 加载的同名类属于不同的类型,不能相互转化和兼容。

而这个特性就是我们实现热替换的关键。过程如图所示:

热替换基本思路

# 总结

好了,到这里,基本的类加载器就介绍结束了。我们总结了类加载的工作流程,包括加载,连接,初始化。然后我们重点介绍了加载,因为加载阶段是我们程序员唯一有所作为的地方。然后介绍了加载阶段的一些细节,比如双亲委派,然后说了双亲委派的缺点和补充,然后探讨了如何修改默认的类加载方式,最后通过类加载的特性实现了热替换。当然也看了核心类 ClassLoader 的源码。不过,这肯定不是类加载器的全部。我们将在后面的文章中将类加载的其他特性一一解开。

good luck!!!!

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

推荐阅读更多精彩内容

  • 原文链接:http://iaspecwang.iteye.com/blog/1931043 一.概述 定义:虚拟机...
    晴天哥_王志阅读 6,774评论 1 35
  • 0、前言 读完本文,你将了解到: 一、为什么说Jabalpur语言是跨平台的 二、Java虚拟机启动、加载类过程分...
    vivi_wong阅读 1,226评论 0 10
  • ClassLoader翻译过来就是类加载器,普通的java开发者其实用到的不多,但对于某些框架开发者来说却非常常见...
    时待吾阅读 1,073评论 0 1
  • 工作室的任务呢,规定每人不仅看学习视频,还要写感想,即学习笔记。周二呢,由于课程比较多,只有再晚上抽出一点时间看了...
    在路上_80f5阅读 384评论 1 1
  • 2016年的8月19日,郎平闪着泪光接受央视的采访,就在刚刚,与弟子朱婷的相拥而泣,似乎已经不是那个叱咤风云的“铁...
    二双阅读 223评论 0 0