深刻浅出 JVM ClassLoader

# 前言

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

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

首先,咱们要了解类加载器,固然,了解的目的是为了更好的开发,经过对类加载器的解读,看看咱们能不能作些什么,好比修改类加载器的加载逻辑,好比加入自定义的类加载器等等功能。框架

让咱们开始吧!ide

# 1. 类加载器介绍

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

如图所示:spa

各类语言均可以在 JVM 上运行

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

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

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

Class 文件转载过程

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

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

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

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

  2. 扩展类加载器有 sun.misc.Launcher$ExtClassLoader 实现,负责加载 /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!!!!

相关文章
相关标签/搜索