我对类加载器 ClassLoader 的理解

引言

当刚学 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 了

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

推荐阅读更多精彩内容