为什么 Java 程序员必须要懂类加载机制?

问题 16

想要成为一名高级 Java 开发人员,光会写业务代码可不行。 我们都知道 Java 源文件会被编译为 class 文件,然后在 Java 运行时类加载器负责加载 class 文件。 那么你有没有想过,JVM 中有几种类加载器,它们是如何分工的,以及加载过程中经历了什么?

答案

说起类加载过程,我们首先得了解 ClassLoader 类加载器 类加载器负责将 class 数据加载到 Java 运行时环境中,它控制 着JVM 去何处(本地文件系统、远程网络或者其他环境)加载 class 信息,以及 class 数据格式的规范性。

类加载器历史

在 Java 1.1 及之前的版本中,各个类加载之间不存在联系。 例如系统类加载器负责加载应用,以及 classpath 目录下的 class 文件和资源; 而 applet 的类加载器负责和服务器端交互以加载 applets 应用和它相关的 class 文件和资源。

在 J2SE 1.2 版本(如果你知道 J2SE 这个称呼,证明你是一名老程序员了,哈哈),类加载器之间产生了一种关系,这种关系也就是我们熟知的 parent delegation(中文译作双亲委派) 机制。

双亲委派是什么

简单来说双亲外派机制就是当前的类加载器去加载一个 class 数据之时,它会先委托它的父加载器去做这件事,父加载器它会递归去委托自己的父加载器去加载,直到父加载器不存在,或者父加载器加载不到的时候才自己去加载(注意:此处的父加载器并不是 Java 中的继承关系,而是职责上的关系)。

JDK 中提供了如下 3 种常见的类加载器:

BootstrapClassLoader: 俗称启动类加载器,是最顶层的类加载器,也称为 root 类加载器,负载加载 JRE/lib/rt.jar 中的 class 文件,加载目录可以通过 -Xbootclasspath 改变。

ExtClassLoader: 俗称扩展类加载器,负责加载 JRE/lib/ext 目录下的 class 文件,可以通过设置环境变量 java.ext.dirs 改变加载目录,优先级次于 BootstrapClassLoader。

AppClassLoader: 俗称应用类加载器,也称系统类加载器,负责加载我们的应用 class 文件和 classpath 环境变量指定目录下的 class 文件,优先级次于 ExtClassLoader。

这种机制的好处是可以明确的分工每种类加器的职责,同时保证 class 加载的唯一性,当一个 class 文件被其父加载器加载过以后,后续类加载器就不会加载了。

双亲委派机制的弊端

它也有不足之处,例如 Java 的 SPI 机制,这种双亲委派机制就不能很好的支持,因此又引入了上下文类加载器。

SPI 全称 Service Provider Interface,它是 Java 发现服务的一种规范。JDK 负责提供服务的接口规范,第三方厂商负责来实现该服务。例如我们熟知的 JDBC 就是采用这种机制来实现。

JDBC 的接口规范由 JDK 定义在 rt.jar 中,我们知道这个 jar 中 class 是由 BootstrapClassLoader 来负责加载的,然而 JDBC 的实现类是由 AppClassLoader 来负责加载的。 因此当 JDBC 接口需要用到实现类时就无法完成操作了,但是鸡贼的 Java 大神们引入了线程上下文类加载器来解决这个问题。

如果你不做特殊设置的话,通常线程的上下文类加载器就是系统类加载器,即为 AppClassLoader,使用它恰巧可以加载厂商提供的实现类的 class 文件,有兴趣的同学可以参考 JDK 中 java.sql 包下的 DriverManager 中的部分源码如下:

//  Worker method called by the public getConnection() methods.

private static Connection  getConnection

(

throws SQLException  {

/*

* When callerCl is null, we should check the application’s

* (which is invoking this class indirectly)

* classloader, so that the JDBC driver class outside rt.jar

* can be loaded from here.

ClassLoader callerCL = caller !=  null ? caller.getClassLoader() :  null ;

synchronized (DriverManager.class) {

// synchronize loading of the correct classloader.

if (callerCL ==  null ) {

callerCL = Thread.currentThread().getContextClassLoader();

}

/**省略部分源码**/

}

通过上面我们了解了 JDK 中几种类加载器的分工,也讨论了双亲委派加载机制的本质。 接下来让我们一起看看一个 class 文件在被加载到 Java 运行时环境中变成一个可以使用的 java.lang.Class 实例之前经过了哪些步骤。

类加载步骤

一个 class 文件变为 Java 运行时环境中的可以使用的 Class 实例时,主要经过了加载、链接和初始化 3 个步骤。

1. 加载

这个阶段总共会做 3 件事:

1.通过类的全限定名获得定义该类的二进制字节流。

2.将字节流转换为 JVM 运行时数据结构。

3.在 JVM 中生成代表该类的 Class 实例,以供后续使用。

2. 链接

该阶段主要分为了验证、准备和解析 3 个步骤:

验证 是链接第一步,首先验证文件格式,确认 class 文件否和当前虚拟机规范,例如以魔数 0xCAFEBABE 开头,class 版本号在当前虚拟机处理范围内等等; 其次是分析代码语义,确认其描述的语义否和 Java 语言规范;

准备 是链接的第二步,该阶段将为类变量(static 修饰)分配内存,如果它是一个常量(static final 修饰),则直接初始化为目标常量。

解析 是链接的第三步,该阶段虚拟机会将常量池中符号引用替换为直接引用。

3. 初始化

该阶段是最贴近程序员编码的,主要执行所有类变量的初始化和静态代码块,同时虚拟机会保证在子类初始化操作之前完成父类(接口除外,接口只有在直接使用到接口的静态属性时候才会初始化)的初始化。

如上即为我们今天要介绍的 Java 类加载器,以及双亲委派机制,类加载的主要过程,小伙伴们是否有疑问呢,欢迎留言与我讨论。