我对类加载器 ClassLoader 的理解

8字数 2283阅读 2862

引言

当刚学 c/c++ 程序时,第一次在控制台下运行程序,需要将所有程序文件进行编译、链接,然后再运行。这很容易理解,毕竟我在一段程序中引用了另一个文件中定义的函数,当然要把这些文件连接到一起。但是在运行 java 程序时却不需要这么做了,顶多在运行时加一个 -classpath 参数。这背后的原因就是 jvm 和 java 中的 ClassLoader 机制。

什么是 ClassLoader

当 c/c++ 运行时,是将代码编译成机器码,链接成可运行文件扔给 cpu 运行,但是 java 不是这么做的,它的运行是在 jvm 中的,而关键是 java 并不是把所有要运行的代码打包一起扔给 jvm ,jvm 内部刚开始只加载了你 java XXX 命令中指定的那个初始类 (有 mian 函数的那个类),在这个类运行过程中需要其他类时,再使用类加载器将需要的类(.class 文件)加载到 jvm 中。到这里便引出了三问题

  • 类是经过哪些过程加载到 jvm 中的 ?
  • 什么时候是"需要其他类的时候" ?
  • 所谓的"类加载器"具体到代码到底是个啥玩意 ?

类是经过哪些过程加载到 jvm 中的 ?

一 、根据类名在指定的路径下找到对应的 .class 文件
二、 解析 class 文件,在方法区中(不了解方法区可以先看看 jvm 内存模型) 分配空间,并创建 Class 对象:Class 对象就是 Class 类型的对象 (类可以创建对象,而类本身就是一个 Class 类型的对象 ,它是保存着类的 类型信息 的对象。)深入请看https://blog.csdn.net/bingduanlbd/article/details/8424243
三、 对 Class 对象进行一系列初始化操作 (静态变量赋值,执行 static 代码块.....)

(在其他博客和书中会将这个加载过程分为五个步骤,但是要介绍这些步骤即会扯到文件格式验证,引用解析等一堆东西,这篇文章旨在简单介绍,不予赘述。)

什么时候是"需要其他类的时候" ?

一 、 使用 new 关键字实例化对象的时候、读取或设置一个静态字段 ( final 修饰除外),调用一个静态方法时。
二 、 使用反射加载一个类时
三、 加载一个类时它的父类还没有加载
四、 启动时指定的类,就是引言中所说的那种情况

(还有一种是与 java 对动态类型的支持相关的,并不常见,可以参考《深入理解 java 虚拟机》)

所谓的"类加载器"具体到代码到底是个啥玩意 ?

java 已经定义好的类加载器有:启动(Bootstrap)类加载器扩展(Extension)类加载器系统(App)类加载器安全(Secure)类加载器URL类加载器 它们的继承关系是这样的:


其中 ClassLoader 是一个抽象类,定义了类加载器的基本工作机制,SecureClassLoader 在此基础上支持了一些权限控制相关操作,URLClassLoader 则又加入了一些方便定义加载路径的接口,其余的三个类加载器 BootstarpClassLoader,AppClassLoader,ExtClassLoader 都会由 jvm 创建出实际的对象,完成实际的功能 (下文有介绍)。

分析 ClassLoader 的源码可以大致了解类加载器的工作原理,这里不贴源码分析了,介绍一下它的主要方法和调用关系,源码可以自己看。

ClassLoader 中与加载类相关的方法:

方法 说明
loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
resolveClass(Class<?> c) 链接指定的 Java 类。

调用关系:


整个过程的说明 :
一、先在缓存中找,已经加载过的就不加载了,直接返回 。

二、没有在缓存中找到,调用"父类"的 loadClass() 方法,委托"父类"加载,这就是所谓的双亲委派模型。加引号是因为不是真正的父类,而是在它的一个字段 parent 中保存的父类加载器,在 jvm 创建加载器的对象时指定,与上面继承关系图中表示的不同,委托关系图如下:


如果你想自定义一个类加载器用来加载你特殊文件路径下的类,或是要对加密字节码文件进行解码,可以继承 ClassLoader ,SecureClassLoader,URLClassLoader 中的一个,覆写 findClass() 方法,大多数情况下,继承自 URLClassLoader 最为简单。

三、"父类"加载失败,调用加载器自己定义的 findClass(String name) 函数,比如 ExtClassLoader 的 findClass() 执行的就是去 <JAVA_HOME>/lib/ext 这个路径下查找 class 文件,而 BootstarpClassLoader 则是去 <JAVA_HOME>/lib 下去查找,它们俩个都负责加载一些 java 程序运行需要的基础组件,然后是AppClassLoader,它负责到你的项目路径中加载你写的项目。如果你要自己写一个类加载器,应该继承自 URLClassloader ,然后重写它的 findClass() 方法,如果在构造方法中不指定"父类"加载器,则它的默认"父类"就是 AppClassLoader 。

(在看源码时你可能会发现 ExtClassLoader 甚至连 findClass()这个函数都没有,那是因为它继承自 URLClassLoader, 直接用路径完成父类构造,然后调用父类的 findClass() 就可以了,按上文理解也没有什么问题)

四、找到了 class 文件把字节码读进来,还要用 defineClass() 将字节码转变为真正的 Class 对象,也就是将字节码载入 jvm。

五、对生成的 Class 对象进行最后的链接操作,链接类或接口包括验证和准备类或接口、它的直接父类、它的直接父接口、它的元素类型(如果是一个数组类型)及其它必要的动作。

双亲委派模型的优点

Java 类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,Java 中的 Object 类,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 在各种类加载环境中都是同一个类。如果不采用双亲委派模型,那么由各个类加载器自己去加载的话,那么系统中会存在多种不同的 Object 类。

线程上下文类加载器(可以用来破坏双亲委派模型)

我刚看到这个东西时是真的懵逼,不知道上文说到的类加载器和它有什么关系,后来看了几篇博文和一些源码,才明白了这个东西就是在 Thread 类中的一个属性,Thread 中有它的 get 和 set 方法 : (说白了它就是在同一线程中可以共享的一个 ClassLoader ,用户可以对它自由设置,默认与父线程相同,没有父线程默认 AppClassLoader)

//这里是 Thread 类的源码
public class Thread implements Runnable {
    //.... 省略
    private ClassLoader contextClassLoader;
    //.... 省略
    public ClassLoader getContextClassLoader() {
          if (contextClassLoader == null)
              return null;
          SecurityManager sm = System.getSecurityManager();
          if (sm != null) {
                ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                   Reflection.getCallerClass());
          }
          return contextClassLoader;
    }
    public void setContextClassLoader(ClassLoader cl) {
          SecurityManager sm = System.getSecurityManager();
          if (sm != null) {
              sm.checkPermission(new RuntimePermission("setContextClassLoader"));
          }
          contextClassLoader = cl;
    }
}

破坏双亲委派模型

双亲委派模型好是好,但有时候也会成为实现功能过程中的绊脚石,在以前我们使用 jdbc 时,都要加上这么一句话

Class.forName("com.mysql.jdbc.Driver") ;

接下来就解释一下为什么加这句话?为什么后来不需要加它了?如果看明白了相信你肯定对 java 的类加载器有一个基本的掌握了。

为什么加这句话?

当我们使用 jdbc 时,使用的 api 是在 java.sql 这个包下的,这是由 java 语言的设计者写的包,它定义了一套 java 去操作数据库时要使用的 api,但是,不同的厂家开发出了不同的数据库,java 的设计者不可能去全部实现这些 api,所以他只定义接口,具体的实现由数据库厂商完成。

好了,当我们要调用 java.sql 这个包下的函数时,它的部分实现依赖于我们导入的 com.mysql.jdbc.Driver 这个包,而 java.sql 在 java 的核心库中,由 BootstrapClassLoader 进行加载,当 java.sql 中的代码需要加载类时,也会使用 BootstrapClassLoader 去加载 (会直接使用加载自己的加载器),然而,com.mysql.jdbc.Driver 这个包却不在 BootstrapClassLoader 的加载范围之内,更不能向上委派,造成了无法加载,只能在你的代码前加上一句 Class.forName ,利用应用中的 AppClassLoader 将驱动载入 jvm。

在大部分情况下,都是 "下层" 的的类依赖 "上层" 的类,符合委派模型的加载过程,但是 jdbc 这个例子就是一种特殊情况,在没有线程上下文类加载器之前,就只能使用这种不优雅的方式去写。

为什么后来不需要加它了?

因为有了线程上下文类加载器,只要将 AppClassLoader 设置为线程上下文类加载器 (默认就是),不管在哪儿都能拿出这个加载器来使用了,加载的代码也变成了这样:

//文件 ServiceLoader.java 此函数在 java.sql.DriverManager 初始化时会被调用
public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
 }

再也不需要麻烦用户写 Class.forName 了

推荐阅读更多精彩内容