类加载器ClassLoader

本文深入探讨Java类加载过程,包括加载、连接、初始化步骤,详解ClassLoader机制与双亲委派模型,阐述类加载器如何解决类加载需求,以及线程上下文类加载器的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

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

 

类装载流程

首先介绍下类装载流程,主要包括加载、连接、初始化;

1、加载

加载是类装载的第一步,首先通过class文件的路径读取到二进制流,并解析二进制流将里面的元数据(类型、常量等)载入到方法区,在java堆中生成对应的java.lang.Class对象。

2、连接

连接过程又分为3步,验证、准备、解析

2.1、验证

验证的主要目的就是判断class文件的合法性,比如class文件一定是以0xCAFEBABE开头的,另外对版本号也会做验证,例如如果使用java1.8编译后的class文件要再java1.6虚拟机上运行,因为版本问题就会验证不通过。除此之外还会对元数据、字节码进行验证,具体的验证过程就复杂的多了,可以专门查看相关资料去了解。

2.2、准备

准备过程就是分配内存,给类的一些字段设置初始值,例如:

public static int v=1;

这段代码在准备阶段v的值就会被初始化为0,只有到后面类初始化阶段时才会被设置为1。

但是对于static final(常量),在准备阶段就会被设置成指定的值,例如:

public static final  int v=1;

这段代码在准备阶段v的值就是1。

2.3、解析

解析过程就是将符号引用替换为直接引用,例如某个类继承java.lang.object,原来的符号引用记录的是“java.lang.object”这个符号,凭借这个符号并不能找到java.lang.object这个对象在哪里?而直接引用就是要找到java.lang.object所在的内存地址,建立直接引用关系,这样就方便查询到具体对象。

3、初始化

初始化过程,主要包括执行类构造方法、static变量赋值语句,staic{}语句块,需要注意的是如果一个子类进行初始化,那么它会事先初始化其父类,保证父类在子类之前被初始化。所以其实在java中初始化一个类,那么必然是先初始化java.lang.Object,因为所有的java类都继承自java.lang.Object。

说完了类加载过程,我们来介绍一下这个过程当中的主角:类加载器。

 

什么是 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);
 }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值