类加载器及其加载原理

本贴最后更新于 1314 天前,其中的信息可能已经水流花落

概述

在之前的文章"类的加载流程"讲了一个 Class 文件从加载到卸载整个生命周期的过程,并且提到"非数组类在加载阶段是可控性最强的"。而这个优点很大程度上都是类加载器所带了的,因而本篇文章就着重讲一下类加载器的加载机制与加载原理。

首先我们思考一个问题:什么是类加载器?

简单来说就是加载类的二进制字节流的工具,那它是如何找到所要加载类的具体位置呢?

答案就是通过类的全限定名

因而我们可以这样说,类加载器就是用来完成“通过一个类的全限定名来获取描述该类的二进制字节流”这一加载动作的代码。

类加载器的作用

顾名思义,类加载器的作用是实现类的加载动作。但它的作用就仅限于此吗?

答案必然是否定的。

我们首先看下边这一段代码:

public class ClassLoaderTest {

    public static void main(String[] args) throws Exception {
    	//创建自定义的类加载器并重写loadClass方法
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        //使用自定义类加载器加载对象
        Object obj = myLoader.loadClass("test.ClassLoaderTest").newInstance();

        System.out.println(obj.getClass());
        System.out.println(obj instanceof test.ClassLoaderTest);
    }
}

代码运行结果如下:

class test.ClassLoaderTest
false

上述代码基本上所做的事情就是,创建了一个自定义的类加载器,然后使用这个类加载器加载了 ClassLoaderTest 类,并以该类为模板创建了对象。

输出结果上看,新创建的对象 obj 确实是以 test.ClassLoaderTest 为类模板创建的,但为何在判断是否是 test.ClassLoaderTest 的实例对象时结果是 false 呢?

这是因为在 Java 中一个类的唯一性不仅和类本身相关而且和加载它的类加载器相关,也就是说:任何一个类都必须由加载它的类加载器这个类本身一起确定其在 Java 中的唯一性,每一个类加载器都有一个独立的类命名空间。

换句话说,如果想要比较两个类是否相等时,只有这两个类是同一个类加载器的前提下才有意义,否则,即使这两个类是同一个 Class 文件,被同一个 Java 虚拟机加载,只要加载它们的类加载器不同,这两个类必然就不相等。

这里类的相等与否到底有何影响呢?

这里的相等包含了的 Class 对象的 equals() 方法、isAssignableForm()isInstance() 方法返回结果,也包括了使用 instanceof 关键字进行从属判断的各种情况。

通过上面的解释,我们应该就懂了为何例子程序中,在使用 instanceof 判断的时候返回结果是 false。因为在例子程序中,Java 虚拟中同时存在两个 test.ClassLoaderTest 类,一个是由虚拟机的"应用程序类加载器"加载的,另一个是由自定义加载器加载的,但在 Java 虚拟机中仍然是两个独立的类,因而在做类型检查时返回结果是 false。

类的加载机制

前边我们说了类加载器是参与类唯一性的判断的,并且我们的 Java 虚拟机是有多个类加载器的,因而这里就会有一个问题:多个类加载器在加载类的时候是如何进行协调的?它是如何解决重复加载这一问题的?

说到这里,我们不得不提一下,类加载器的一个加载机制--双亲委派机制。但在正式聊 双亲委派机制 之前,我们有必要了解一下类加载器的类别和它们之间的一个层次关系。

类加载器的种类与关系

类加载器的层次关系图,如下所示:

层次关系

从图中可以看到,系统提供的类加载器主要有三个:

  1. BootstrapClassLoader(启动类加载器) :用于加载系统类库中的类,是最顶层的加载类,由 C++ 实现,负责加载 %JAVA_HOME%/lib 目录下的 jar 包和类或者或被 -Xbootclasspath 参数指定的路径中的所有类。注意该加载器无法被 Java 程序直接引用,若在自定义加载器中需要委派给启动类加载器加载,直接返回 null 即可。
  2. ExtensionClassLoader(扩展类加载器) :用于加载系统类库扩展类,主要负责加载目录 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。但在 JDK9 之后,这种扩展机制被 模块化 的天然扩展能力所取代。
  3. AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。一般情况下,该加载器是默认加载器

最后是自定义类加载器(User Class Loader):这个加载器是需要在继承 ClassLoader 的自定义加载器类 UserClassLoader 中重写 findClass() 来实现的。

此处可能会有些疑问,既然应用程序类加载器都已经是默认的加载器了?那自定义类加载器还有什么意义呢?

主要原因是 应用程序加载器 它只能加载在 classpath 下的所有类,但不在 classpath 下的 class 文件是无法被加载的,比如通过网络远程传输过来的 class 文件(远程调用)或者我们在桌面上有一个 class 文件,希望在运行的过程中被加载和使用,在这两种情况下,应用程序类加载器是无法加载的,此时如果想要加载必须靠自定义类。

为了说明这一点,我们可以看一个例子:

首先我们定义一个待加载的普通类,放置在 com.test 包中:

package com.test;

public class Test {
    public void hello() {
        System.out.println("我是Test,由 " + getClass().getClassLoader().getClass()
                + " 加载进来的");
    }
}

将编译生成后的 class 文件移动到其他位置,非当前项目的 classpath 下面,本例位置如下:

image.png

注意:

如果你是直接在当前项目里面创建,待 Test.java 编译后,请把 Test.class 文件拷贝走,再将 Test.java 删除。因为如果 Test.class 存放在当前项目中,根据双亲委派模型可知,会通过 sun.misc.Launcher$AppClassLoader 类加载器加载。为了让我们自定义的类加载器加载,我们把 Test.class 文件放入到其他目录。

如果此时我们想调用 Test 类,因为该类不在项目的 classpath 下,因而无法通过 系统加载器 进行加载,只能通过用户自定义加载器。

写一个用户自定义加载器,内容如下:

/**
 * 自定义类加载器,加载自定义位置下的class文件
 * @author vcjmhg
 *
 */
public class UserDefineClassLoader {
	 static class MyClassLoader extends ClassLoader {
	        private String classPath;

	        public MyClassLoader(String classPath) {
	            this.classPath = classPath;
	        }

	        private byte[] loadByte(String name) throws Exception {
	            name = name.replaceAll("\\.", "/");
	            FileInputStream fis = new FileInputStream(classPath + "/" + name
	                    + ".class");
	            int len = fis.available();
	            byte[] data = new byte[len];
	            fis.read(data);
	            fis.close();
	            return data;

	        }

	        protected Class<?> findClass(String name) throws ClassNotFoundException {
	            try {
	                byte[] data = loadByte(name);
	                return defineClass(name, data, 0, data.length);
	            } catch (Exception e) {
	                e.printStackTrace();
	                throw new ClassNotFoundException();
	            }
	        }

	    };

	    public static void main(String args[]) throws Exception {
	        MyClassLoader classLoader = new MyClassLoader("C:\\Users\\vcjmhg\\Desktop\\Test");
	        Class clazz = classLoader.loadClass("com.test.Test");
	        Object obj = clazz.newInstance();
	        Method helloMethod = clazz.getDeclaredMethod("hello", null);
	        helloMethod.invoke(obj, null);
	    }
}

上述代码,基本意思就是:定义了一个类加载器 MyClassLoader 可以对指定文件夹下的 class 文件进行加载,在加载完成之后通过反射创建了一个 obj 对象并调用了其 hello()方法。关于自定义加载器定义方法可以参考后续小节的"如何自定义类加载器?"

运行结果如下:

我是Test,由 class com.test.UserDefineClassLoader$MyClassLoader 加载进来的

上边的例子,就解释了自定义类加载器的作用:它可以按照用户要求加载指定的 class文件,无论 class文件 是通过网络传过来,还是本地的某个路径,都可以实现加载。

双亲委派机制

其实从前边那个类的层次关系图中,,我们就可以简单了解双亲委派模型加载机制:

当一个类加载器收到类加载请求时,首先不会自己尝试加载这个类,而是把这个请求委派父类加载器去完成,每一层的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有父加载器无法完成这个加载请求(它的搜索范围内,找不到所需的类)时,子加载器才会尝试自己去完成加载。

结合自定义加载器,整个类的加载流程如下图所示:

image.png

  1. 当我们的 自定义类加载器 要加载一个类的时候,会首先判断给定的类是否被加载过,如果已经被加载过则不再加载,可以直接使用;如果没被加载,它也不会直接加载而是把加载任务委派给父加载器也就是 AppClassLoader 加载器。
  2. AppClassLoader 在收到委派的加载任务后,也不直接加载,也会做一个自己是否加载过的判断,,如果没有将将加载任务委派给 ExtClassLoader
  3. ExtClassLoader 收到委派的任务后,在自己没有加载过该类的情况下,会将加载任务委派给 BootstrapClassLoader,由于 BootstrapClassLoader 是顶层加载器没有父加载器,因而 BootstrapClassLoader 会开始尝试自己加载,如果说需要加载的类位于其加载范围(比如 -Xbootclasspath 参数指定的加载类),则直接返回加载结果。否则下沉到子类加载器进行加载,直到底层的 自定义类加载器
  4. 要注意,如果所有的类加载器最终都无法加载,会抛出一个 ClassNotFoundException

到这里可能就会有小伙伴有疑问了:这玩意有什么用?为什么要这样设计呢?

双亲委派机制有什么用?

这种加载机制一个显而易见的好处就是 Java 中的类随着它的它的类加载器一起具备了具有优先级的层级结构。比如类 java.lang.Object,它存放在 rt.jar 需要被顶层启动类加载器所加载,因而 Object 类在程序的各种类加载器的环境中都能保证是同一个类,避免了重复加载的情况发生,而这也避免了危险代码植入的风险(比如恶意替换 java.lang.Object 类)。

双亲委派模型对于 Java 程序的稳定性极其重要,但其实现却异常简单。

双亲委派机制如何实现的?

用以实现双亲委派的代码只有短短十多行,全部集中在 java.lang.ClassLoaderloadClass() 方法之中,具体实现代码如下:

//name:被加载类的全限定名
// resolove:是否连接了已加载的类
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先检查类是否已经被加载了
        Class<?> c = findLoadedClass(name);
	//未被加载的情况下,尝试用父类加载器进行加载
        if (c == null) {
            try {
		//存在父类加载器的情况下继续向上委托
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
		//使用顶层加载器BootstrapClassLoader进行加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父类加载器无法加载的情况下抛出ClassNotFoundException异常
                // 说明父类加载器无法完成加载请求
            }

            if (c == null) {
                // 父类加载器无法加载的情况下,调用本加载器的findClass()方法着手进行加载
                c = findClass(name);
            }
        }
	//如果resolve标记为true,则在加载操作完成后执行链接操作
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

这段代码逻辑非常简单:先检查类是否被加载,如果没有被加载则委托父类加载器进行加载,若父类加载器也无法加载则调用 findClass() 方法进行进行加载。

如何自定义类加载器?

实现一个自定义类加载器有两种情况:

第一种 不破坏“双亲委派机制”

loadClass() 的代码中可以看到,类加载的最后一步就是调用 findClass() 方法,因而如果要实现一个自定义类加载器需要首先继承 ClassLoader 类,然后重写其 findClass() 方法。

我们可以首先看下 findClass() 的默认实现:

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

可以看出,抽象类 ClassLoaderfindClass() 函数默认只是抛出异常的,因此要自定义类加载器必须要重写 findClass() 方法,根据传入的字符串(指定类文件路径)生成对应的 Class 对象

那如何生成一个 Class 对象呢?

很简单,Java 提供了 defineClass() 方法,通过这个方法,我们可以把一个字节数组转为 Class 对象。

defineClass() 方法的默认实现如下:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError  {
        return defineClass(name, b, off, len, null);
}

当重写 findClass() 首先要获取到 Class 文件的字节流数据,然后将字节流数据传递给 defineClass() 方法最终获取到一个 Class 对象,至此在不破坏双亲委派机制的情况下,就完成了自定义类加载器。具体实现可以参考前边的"例子",该自定义类加载器的基本思路就是重写了 findClass() 方法。

第二种 “破坏双亲委托机制”

可能在某些场景下,需要使用自定义加载器加载一些特殊的类文件,比如位于 classpath 路径下的一些类文件,如果直接重写 findClass() 方法,由于 双亲委派机制 该类必然会被 AppClassLoader 所加载。因而若要实现这样的类加载器,必须要重写 loadClass() 方法。

如何破坏双亲委托?

因为 双亲委派机制 并不是一个强制性约束的模型,而是 Java 设计者推荐给开发者们的类加载器实现方式,因而在"模块化"出现之前,双亲委托模型主要出现了 3 次被较大规模“被破坏”的情况:

第一次:为了保证兼容性

由于 双亲委派模型 是在 JDK1.2 之后才被引入的,而类加载器这一概念 java.lang.ClasssLoader 这一概念在 Java 第一个版本中便存在了。因而为了保证向前兼容性,兼用 JDK1.2 之前已经存在的自定义类加载器代码,Java 设计者在引入双亲委派模型的时候做了一些妥协,不直接以技术手段避免 loadClass() 被覆盖的可能性,而是将 双亲委派模型 的逻辑代码写在 loadClass() 方法中。并且引导用户在编写自定义类加载器时,尽量重写新添加的 findClass() 方法,而不是覆盖 loadClass() 方法。

第二次:为了实现 SPI 技术

某些情况下,基础类型需要调用用户的代码,比如 JNDI 技术(对资源几种查找和管理的技术),它需要调用其他厂商实现并部署在应用程序的 ClassPath 下的 JNDI 服务提供者接口 SPI 的代码,但是启动类绝不可能认识和加载这些代码,那该怎么办呢?

为了解决该问题,Java 设计团队设计了一个线程上下文加载器(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 setContextLoader() 方法进行设计,如果创建线程时,没有设置它将从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那么这个类加载器默认就是应用程序类加载器。

有了这些上下文类加载器之后,JNDI 服务使用这个线程上下文加载器去加载所需要的 SPI 代码,这是一种父加载器请求自类加载器的类加载行为。

第三次:用户对应用程序动态性的追求所导致的

为了追求应用程序的动态性,IBM 在 2008 年提出了 OSGI 技术,用来实现模块化的热部署。

其实现热部署的原理如下:

OSGI 实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(Bundle)都有一个自己的类加载器,当需要替换一个 Bundle 时,就把 Bundle 连同类加载器一起替换掉以实现代码的热替换。在 OSGI 环境下,类加载器不再是双亲委派模型推荐的树状结构,而逐步发展成了网状结构,当收到请求加载时,OSGI 将按照如下顺序进行类搜索:

  1. 将以 java.* 开头的类,委派给父类加载器加载
  2. 否则,将委派列表名单的类,委派给父加载器进行加载
  3. 否则,将 Import 列表中的类委派给 Export 这个类的 Bundle 的类加载器进行加载
  4. 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器进行加载
  5. 否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle 的类加载器进行加载
  6. 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载
  7. 否则,类查找失败

从上边流程中我们可以看到,只有前两条符合双亲委派模型,其他的均不符合。

总结

本文主要讲了常用的类加载器,比如启动类加载器、扩展类加载器、应用类加载器以及自定义类加载器,详细介绍了类加载器在加载一个类时的原理以及加载所使用的双亲委派机制。以及使用双亲委派机制的好处以及破坏该机制的一些情况。

引用

  1. Java 自定义类加载器与双亲委派模型
  2. 深入理解 jvm 虚拟机 第三版
  3. 类加载器
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...