Java类加载器

前言

类加载器是Java语言的一个创新,也是Java语言流行的重要原因之一。它使得Java 类可以被动态加载到 Java 虚拟机中并执行。

一般来说,Java 应用的开发人员不需要直接同类加载器进行交互。Java 虚拟机默认的行为就已经足够满足大多数情况的需求了。不过如果遇到了需要与类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就很容易花大量的时间去调试 ClassNotFoundExceptionNoClassDefFoundError 等异常。

下面我们来了解类加载器的一些概念,来使我们更好地认识类加载器。

正文

概念

类加载器(class loader)是用来加载 Java 类到 Java 虚拟机中的。

一般来说,Java 虚拟机使用 Java 类的方式如下:

  1. Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。
  2. 类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。每个这样的实例用来表示一个 Java 类。
  3. 通过此实例的 newInstance() 方法就可以创建出该类的一个对象。

PS: 实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。

java.lang.ClassLoader

基本上所有的类加载器都是 java.lang.ClassLoader 类的一个实例。

java.lang.ClassLoader 类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class 类的一个实例。

除此之外, ClassLoader 还负责加载 Java 应用所需的资源,如图像文件和配置文件等。

下面我们主要看下这个类中与加载类相关的方法。

方法说明
getParent()返回该类加载器的父类加载器。
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 类。

类加载器的树状组织结构

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:

  • 引导类加载器(BootstrapClassLoader):它用来加载 Java 的核心库,是用原生代码(C++)来实现的,并不继承自 java.lang.ClassLoader

  • 扩展类/平台类加载器(ExtClassLoader/PlatformClassLoader):扩展类加载器在JDK1.8之后变为平台类加载器。从JDK1.8之后的版本(JDK9,JDK10)提供有一个”PlatformClassLoader“类加载器,而在JDK1.8以前的版本里面提供的加载器为”ExtClassLoader“,因为在JDK的安装目录里面提供有一个ext目录,开发者可以将*.jar文件拷贝到此目录里面,这样就可以直接执行了,但是这样的处理并不安全。最初的时候也是不提倡使用的。所以在JDK9开始就将这样的操作彻底废除了,同时为了与系统类加载器和应用类加载器之间保持设计的平衡,提供了平台类加载器。

  • 系统类/应用类加载器(SystemClassLoader/AppClassLoader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。

除了系统提供的类加载器以外,我们可以通过继承 java.lang.ClassLoader 类的方式实现自己的类加载器,以满足一些特殊的需求。

除了引导类加载器之外,所有的类加载器都有一个父类加载器。通过 java.lang.ClassLoader 类的 getParent() 方法可以得到。

对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于我们编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,我们编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器,如下图:

upload successful

我们提供Java代码来看下类加载器的结构:

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}

会有如下输出(JDK1.8及以上):

1
2
jdk.internal.loader.ClassLoaders$AppClassLoader@726f3b58
jdk.internal.loader.ClassLoaders$PlatformClassLoader@368239c8

需要注意的是这里并没有输出引导类加载器,这是由于有些 JDK 的实现对于父类加载器是引导类加载器的情况, getParent() 方法返回 null

类加载器的代理模式(双亲委派机制)

类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理(委托)给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。

代理模式(委托模式)第一这样可以避免重复加载,第二是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object 类,也就是说在运行的时候, java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object 类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。

ClassLoader类的loadClass方法,我们可以看到这种委派机制,如下:

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
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
//首先从jvm缓存查找该类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//父类加载器不为空,委托给父类加载器进行加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类加载器为null,则委托给BootStrap加载器加载
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;
}
}

其大致流程如下:

  1. JVM 缓存查找该类,如果该类之前被加载过,则直接从 JVM 缓存返回该类。

  2. 如果 JVM 缓存不存在该类,则看当前类加载器是否有父加载器,如果有的话则委托父类加载器进行加载,否则委托 BootStrapClassloader 进行加载,如果还是没有找到,则调用当前 Classloaderfindclass 方法进行查找。

  3. 从本地Classloader指定路径进行查找,其中findClass方法在路径找到Class文件会加载二进制字节码到内存,然后后会调用native方法defineClass1解析字节码为JVM内部的kclass对象,然后存放到Java堆的方法区。

  4. 如果需要链接resolve=true,则当字节码加载到内存后进行链接操作,对文件格式和字节码验证,并为 static 字段分配空间并初始化,符号引用转为直接引用,访问控制,方法覆盖等。

加载类的过程

在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。

这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。

真正完成类的加载工作是通过调用 defineClass 来实现的;而启动类的加载过程是通过调用 loadClass 来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。

Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。

也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。

如类 com.example.Outer 引用了类 com.example.Inner ,则由类 com.example.Outer 的定义加载器负责启动类 com.example.Inner 的加载过程。

方法 loadClass() 抛出的是 java.lang.ClassNotFoundException 异常;方法 defineClass() 抛出的是 java.lang.NoClassDefFoundError 异常。

类加载器在成功加载某个类之后,会把得到的 java.lang.Class 类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。

也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass 方法不会被重复调用。

线程上下文类加载器

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。

java.lang.Thread 中的方法 getContextClassLoader()setContextClassLoader(ClassLoader cl) 用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl) 方法进行设置的话,线程将继承其父线程的上下文类加载器。

Java应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。

线程上下文类加载器所解决的问题:

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。

线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

Class.forName

Class.forName 是一个静态方法,同样可以用来加载类。

该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)Class.forName(String className)

第一种形式的参数 name 表示的是类的全名; initialize 表示是否初始化类;loader 表示加载时使用的类加载器。

第二种形式则相当于设置了参数 initialize 的值为 trueloader 的值为当前类的类加载器。Class.forName 的一个很常见的用法是在加载数据库驱动的时候。

Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance() 用来加载 Apache Derby 数据库的驱动。

开发自己的类加载器

虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,我们还是需要为应用开发出自己的类加载器。比如我们的应用通过网络来传输 Java 类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候我们就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在 Java 虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。

文件系统类加载器

我们先编写一个类加载器用来加载存储在文件系统上的 Java 字节代码。

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
41
public class FileSystemClassLoader extends ClassLoader {

private String rootDir;

public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}

private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}

如上所示,类 FileSystemClassLoader 继承自类 java.lang.ClassLoader 。在 java.lang.ClassLoader 类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name) 方法即可。

如上面源码所示,loadClass() 封装了前面提到的代理模式的实现。该方法会首先调用 findLoadedClass() 方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass() 方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用 findClass() 方法来查找该类。

因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass() 方法,而是覆写 findClass() 方法。

FileSystemClassLoaderfindClass() 方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。

网络类加载器

下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。

即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。

NetworkClassLoader 负责通过网络下载 Java 类字节代码并定义出 Java 类。它的实现与 FileSystemClassLoader 类似。

在通过 NetworkClassLoader 加载了某个版本的类之后,一般有两种做法来使用它。

第一种做法是使用 Java 反射 API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用 Java 反射 API 可以直接调用 Java 类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。

代码如下:

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
41
42
43
public class NetworkClassLoader extends ClassLoader {

private String rootUrl;

public NetworkClassLoader(String rootUrl) {
this.rootUrl = rootUrl;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}

private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
URL url = new URL(path);
InputStream ins = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

private String classNameToPath(String className) {
return rootUrl + "/"
+ className.replace('.', '/') + ".class";
}
}
1
2
3
4
5
6
public interface Versioned {
String getVersion();
}
public interface ICalculator extends Versioned {
String calculate(String expression);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CalculatorTest {
public static void main(String[] args) {
String url = "http://localhost:8080/ClassloaderTest/classes";
NetworkClassLoader ncl = new NetworkClassLoader(url);
String basicClassName = "com.example.CalculatorBasic";
String advancedClassName = "com.example.CalculatorAdvanced";
try {
Class<?> clazz = ncl.loadClass(basicClassName);
ICalculator calculator = (ICalculator) clazz.newInstance();
System.out.println(calculator.getVersion());
clazz = ncl.loadClass(advancedClassName);
calculator = (ICalculator) clazz.newInstance();
System.out.println(calculator.getVersion());
} catch (Exception e) {
e.printStackTrace();
}
}
}

同时需要在服务器提供CalculatorBasicCalculatorAdvanced的实现,它们继承ICalculator接口。

1
2
3
4
5
6
7
8
9
10
public class CalculatorAdvanced implements ICalculator {

public String calculate(String expression) {
return "Result is " + expression;
}

public String getVersion() {
return "2.0";
}
}
1
2
3
4
5
6
7
8
9
10
public class CalculatorBasic implements ICalculator {

public String calculate(String expression) {
return expression;
}

public String getVersion() {
return "1.0";
}
}

结语

类加载器是 Java 语言的一个创新。它使得动态安装和更新软件组件成为可能。

本文详细介绍了类加载器的相关话题,包括基本概念、代理模式、线程上下文类加载器等。我们在遇到 ClassNotFoundExceptionNoClassDefFoundError 等异常的时候,应该检查抛出异常的类的类加载器和当前线程的上下文类加载器,从中可以发现问题的所在。

在开发自己的类加载器的时候,需要注意与已有的类加载器组织结构的协调。




-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

您的支持就是我创作的动力!

欢迎关注我的其它发布渠道