Java 类加载机制

类加载过程

class 文件在虚拟机的整个生命周期包括加载、验证、准备、解析、初始化、使用和卸载 7 个阶段,通过 ClassLoader.loadClass() 方法可以加载一个 Java 类到虚拟机中,并返回 Class 类型的引用。

  • 加载:通过一个类的完全限定名查找此类的字节码文件,并利用字节码文件创建一个 Class 对象;
  • 验证:目的在于确保 Class 文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括文件格式验证、元数据验证、字节码验证、符号引用验证这四种验证方式。
  • 准备:为类变量(即 static 修饰的字段变量)分配内存并且设置该类变量的初始值为 0 (如 static int i=5;这里只将 i 初始化为 0,至于 5 的值将在初始化时赋值),这里不包含用 final 修饰的 static,因为 final 在编译的时候就会分配,注意这里也不会对实例变量进行初始化。
  • 解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析等。
  • 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的 static 变量将会在这个阶段赋值,成员变量也将被初始化)。

类加载器

JVM 提供了 3 种类加载器:引导类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)。

启动类加载器

启动类加载器(BootstrapClassLoader)主要加载的是 JVM 自身需要的类,这个类加载使用 C++ 语言实现的,是虚拟机自身的一部分,负责将 <JAVA_HOME>/lib 路径下的核心类库或 -Xbootclasspath 参数指定的路径下的 jar 包加载到内存中。

注意由于虚拟机是按照文件名识别加载 jar 包的,如果文件名不被虚拟机识别,即使把 jar 包丢到 lib 目录下也无法被加载(出于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类)。

扩展类加载器

扩展类加载器(ExtensionClassLoader)是指 Sun 公司(已被 Oracle 收购)实现的 sun.misc.Launcher$ExtClassLoader 类,由 Java 语言实现的,是 Launcher 的静态内部类,负责加载 <JAVA_HOME>/lib/ext 目录下或者由系统变量 -Djava.ext.dir 指定路径中的类库,开发者可以直接使用标准扩展类加载器。

应用程序类加载器

应用程序加载器(AppClassLoader)是指 Sun 公司实现的 sun.misc.Launcher$AppClassLoader,负责加载系统类路径 java -classpath-D java.class.path 指定路径下的类库,即经常用到的 classpath 路径,开发者可以直接使用应用程序类加载器,一般情况下该类加载器是程序中默认的类加载器,通过ClassLoader.getSystemClassLoader() 方法可以获取到该类加载器。

类加载器关系

  • 启动类加载器,由 C++ 实现,没有父类;
  • 拓展类加载器(ExtClassLoader),由 Java 语言实现,父类加载器为 null;
  • 应用程序类加载器(AppClassLoader),由 Java 语言实现,父类加载器为 ExtClassLoader;
    • Launcher 初始化时首先会创建 ExtClassLoader 类加载器,然后再创建 AppClassLoader 并把 ExtClassLoader 传递给它作为父类加载器。
  • 自定义类加载器,父类加载器为 AppClassLoader。

双亲委派模型

工作原理:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。

双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码。

双亲委派模式的优点:

  • Java 类随着类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父类加载器已经加载该类时,子类加载器没有必要再加载一次。
  • 安全因素。可以保证 Java 核心 API 中定义类型不会被随意替换。

ClassLoader 源码分析

ClassLoader 是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器),这里关注一下 ClassLoader 中几个比较重要的方法。

loadClass(String)

1
2
3
public Class<?> loadClass(String name) throws ClassNotFoundException {    
return loadClass(name, false);
}

具体看 loadClass() 方法,resolve 参数代表是否生成 class 对象的同时进行解析相关操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {

// 先从缓存查找该 class 对象,找到就不用重新加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果找不到,则委托给父类加载器去加载
c = parent.loadClass(name, false);
} else {
//如果没有父类,则委托给启动加载器去加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// 如果都没有找到,则通过自定义实现的findClass去查找并加载
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//是否需要在加载时进行解析
resolveClass(c);
}
return c;
}
}

findClass(String)

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

当实现自定义类加载时,建议把自定义的类加载逻辑写在 findClass() 方法中,从前面的分析可知,findClass() 方法是在 loadClass() 方法中被调用的,当 loadClass() 方法中父加载器加载失败后,则会调用 findClass() 方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是 ClassLoader 类中并没有实现 findClass() 方法的具体代码逻辑,取而代之的是抛出 ClassNotFoundException 异常,同时应该知道的是 findClass() 方法通常是和 defineClass() 方法一起使用的。

defineClass(byte[] b, int off, int len)

1
2
3
4
5
6
7
8
9
10
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}

defineClass() 方法是用来将 byte 字节流解析成 JVM 能够识别的 Class 对象(ClassLoader 中已实现该方法逻辑),通过这个方法不仅能够通过 class 文件实例化 class 对象,也可以通过其他方式实例化 class 对象,如通过网络接收一个类的字节码,然后转换为 byte 字节流创建对应的 Class 对象。

defineClass() 方法通常与 findClass() 方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖 ClassLoader 的 findClass() 方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用 defineClass() 方法生成类的 Class 对象。

需要注意的是,如果直接调用 defineClass() 方法生成类的 Class 对象,这个类的 Class 对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行。

resolveClass(Class≺?≻ c)

1
2
3
protected final void resolveClass(Class<?> c) {    
resolveClass0(c);
}

使用该方法可以让类的 Class 对象在创建完成的同时也被解析。

URLClassLoader

ClassLoader 是一个抽象类,很多方法没有实现,比如 findClass()findResource()等,而 URLClassLoader 这个类为这些方法提供了具体的实现,并新增了 URLClassPath 类协助取得 Class 字节码流等功能。

在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免自己去编写 findClass() 方法及其获取字节码流的方式。

ExtClassLoader 和 AppClassLoader,这两个类都继承自 URLClassLoader,是 sun.misc.Launcher 的静态内部类。sun.misc.Launcher 主要被系统用于启动主应用程序,ExtClassLoader 和 AppClassLoader 都是由 sun.misc.Launcher 创建的。

显示加载与隐式加载

class 文件的显示加载与隐式加载的方式是指 JVM 加载 class 文件到内存的方式。

显示加载:在代码中通过调用 ClassLoader 加载 class 对象,如直接使用 Class.forName(name)this.getClass().getClassLoader().loadClass() 加载 class 对象。

隐式加载:通过虚拟机自动加载到内存中,如在加载某个类的 class 文件时,该类的 class 文件中引用了另外一个类的对象,此时额外引用的类将通过 JVM 自动加载到内存中。

线程上下文类加载器

在 Java 应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供具体实现,常见的 SPI 有 JDBC、JNDI 等,这些 SPI 的接口属于 Java 核心库,一般存在 rt.jar 包中,由 BootstrapClassLoader 加载,而 SPI 的第三方实现类则是作为 Java 应用所依赖的 jar 包被存放在 classpath 路径下,由 AppClassLoader 加载。

BootstrapClassLoader 是无法找到 SPI 的实现类的(因为它只加载 Java 的核心库),按照双亲委派模型,BootstrapClassLoader 无法委派 AppClassLoader 去加载类。也就是说,类加载器的双亲委派模式无法解决这个问题。

线程上下文类加载器( ThreadContextClassLoader )是从 JDK 1.2 开始引入的,可以通过 java.lang.Thread 类中的 getContextClassLoader()setContextClassLoader(ClassLoader cl) 方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源。

线程上下文类加载器从根本解决了一般应用不能违背双亲委派模式的问题,使得 Java 类加载体系显得更灵活。

Tomcat 与 Spring 的类加载器案例

在 Tomcat 目录结构中,有三组目录( /common/*,/server/* 和 /shared/* )可以存放公用 Java 类库,此外还有第四组 Web 应用程序自身的目录 /WEB-INF/*,把 Java 类库放置在这些目录中的含义分别是:

  • 放置在 common 目录中的类库可被 Tomcat 和所有的 Web 应用程序共同使用;
  • 放置在 server 目录中的类库可被 Tomcat 使用,但对所有的 Web 应用程序都不可见;
  • 放置在 shared 目录中的类库可被所有的 Web 应用程序共同使用,但对 Tomcat 自己不可见;
  • 放置在 /WebApp/WEB-INF目录中的类库仅仅可以被此 Web 应用程序使用,对 Tomcat 和其他 Web 应用程序都不可见。

为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat 自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,如下图所示:

灰色背景的 3 个类加载器是 JDK 默认提供的类加载器;而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 则是 Tomcat 自己定义的类加载器。

  • CommonClassLoader:加载 /common/* 中的类库;
  • CatalinaClassLoader:加载 /server/* 中的类库;
  • SharedClassLoader:加载 /shared/* 中的类库;
  • WebAppClassLoader: 加载 /WebApp/WEB-INF/* 中的类库。

其中 WebApp 类加载器和 JSP 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 JSP 类加载器。

从图中的委派关系中可以看出,CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

如果有 10 个 Web 应用程序都用到 Spring,可以把 Spring 的 jar 包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个 Web 应用程序的 Bean,getBean 时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的 Class 呢?

答案是 Spring 使用线程上下文类加载器( TCCL)来加载类,而 TCCL 默认设置为 WebAppClassLoader,也就是说哪个 WebApp 应用调用了 Spring,Spring 就去获取该应用对应的 WebAppClassLoader 来加载 Bean。

参考

  1. 深入理解Java类加载器(ClassLoader)
  2. 真正理解线程上下文类加载器(多案例分析)