jvm_class_loader

第三十五章 Java类加载器(Class Loader)

1 概述

在Java虚拟机中,类加载器(Class Loader)的责任是实时的读取.class文件,并将类信息加载到内存中。由于类加载器的存在,Java虚拟机并不需要将Java标准库中的代码集成到Java虚拟机的代码中。Java标准库的代码是在编译成.class文件之后,再由类加载器动态的加载到Java虚拟机里的。在Java虚拟机初始化过程中,Java虚拟机规范并未规定是否需要加载整个标准库。这个问题可由具体的Java虚拟机的实现来决定。如果在启动时加载整个标准库,则会延长启动的时间。如果启动时只加载部分标准库,而其他的类按需加载的话,则会减慢程序运行时的速度。这两种方法各有利弊。

Java虚拟机支持两种类加载器(内置类加载器和用户自定义类加载器)。其中,Java虚拟机一般内置了三个类加载器(Bootstrap类加载器、扩展类加载器和应用类加载器);另外,Java虚拟机还支持用户自定义的类加载器(User-Defined Class Loader)。当开发人员未提供自定义类加载器时,所有的类都由内置类加载器加载。

Java虚拟机支持用户自定义类加载器的目的是为了给Java程序增加灵活性和可扩展性。一般情况下,除了Java标准库以外,用户的所有代码必须包含在CLASSPATH中,以便于Java虚拟机能够找到正确的类。但是,如果使用了用户自定义的类加载器,开发人员可以从任意一个地方加载类(例如:从网络上下载一个.class文件并加载)。甚至是开发人员可以动态的将其他格式的类信息实时的转换成.class文件格式(例如:从网络上下载一段Java代码,实时的将其编译成.class文件并加载)。

在Java虚拟机内部,一个类是由类名和类加载器两者共同唯一确定的。在加载一个类时,类加载器可以自行解析.class文件,完成加载;也可以将加载任务委派(Delegate)给另一个类加载器。换句话说,发起加载的类加载器不必是那个完成加载的类加载器。

在Java语言中,数据类型可大致分为三类:原始数据类型、类、和数组。基本数据类型不是类,所以,并不需要使用类加载器加载。Java虚拟机内部支持原始数据类型。数组类型也是由Java虚拟机内部支持的。数组类型不由类加载器加载。只有类是由类加载器加载的。

2 内置类加载器(Build-in Class Loaders)

在我们常用的Java虚拟机的实现版本中,除了Bootstrap类加载器,还内置了扩展类加载器(Extension Class Loader)和应用类加载器(Application Class Loader)。在Java虚拟机内部,它们有着不同的分工。

  1. Bootstrap类加载器主要加载Java标准库中最核心的类,例如:java.lang.Object。所有的类加载器都(直接/间接)继承自Bootstrap类加载器。
  2. 扩展类加载器直接继承自Bootstrap类加载器。它主要负责加载Java标准库中的扩展部分(例如:在$JAVA_HOME/lib/ext目录下的类)。
  3. 应用类加载器直接继承自扩展类加载器。它的责任是在CLASSPATH中查找并加载应用程序的类。

所以,它们之间的继承关系如下图所示。

图一 类加载器继承关系图

图一 类加载器继承关系图

3 自定义类加载器(User-defined Class Loaders)

3.1 ClassLoader类

Java标准库中提供了java.lang.ClassLoader类。ClassLoader类主要用于两个用途。其一,所有的自定义类加载器必须继承自Classloader类。ClassLoader类是一个抽象类(Abstract Class),所以,其自身不能实例化。自定义的类加载器需要实现ClassLoader.findClass()方法。其二,ClassLoader是用户查看内置类加载器和自定义类加载器的通道。如前文所述,在Java虚拟机内部,一个类是由其类名和类加载器唯一确定的。因此,在Class类中可以通过调用getClassLoader()方法获得ClassLoader对象。

如下面的代码所示,这段代码打印了加载类Integer和类ClassLoaderPrinter的类加载器。null代表的是Bootstrap类加载器,而第二行ClassLoaders$AppClassLoader表示的是ClassLoaders类中的一个内部类AppClassLoader。AppClassLoader就是上文所述的应用类加载器。

public class ClassLoaderPrinter {
    public static void main(String[] args) {
        System.out.println("ClassLoader for Integer is " + Integer.class.getClassLoader());
        System.out.println("ClassLoader for ClassLoaderPrinter is " + ClassLoaderPrinter.class.getClassLoader());
    }
}

程序运行结果:

> java ClassLoaderPrinter
ClassLoader for Integer is null
ClassLoader for ClassLoaderPrinter is jdk.internal.loader.ClassLoaders$AppClassLoader@1affbebc

3.2 类加载器的委派工作模式

在加载一个类的过程中,类加载器在反复的重复两个步骤:1、查找.class文件或者资源;2、将.class文件或者资源加载入内存。类加载器的委派工作模式是当一个类加载器接收到加载请求后,先将该请求委派给父类。只有当父类无法满足这个请求后,才由自己来完成这个加载任务。当所有的类加载器都无法满足请求时,Java虚拟机会抛出ClassNotFoundException异常。

这两个步骤对应的方法是ClassLoader.findClass()方法和ClassLoader.loadClass()方法。ClassLoader.loadClass()方法已实现了将.class文件的二进制内容加载入内存的过程,所以,用户自定义的类加载器只需实现findClass()方法即可。

在下面的代码中,我们可以看到Java虚拟机首先进入了AppClassLoader尝试加载一个不存在的类A.NONEXISTING.CLASS。在AppClassLoader.loadClass()方法中,实际上是调用了BuiltinClassLoader.loadClass()方法。所以,最终运行加载操作的是在BuiltinClassLoader.loadClass()方法中。在OpenJDK 11中,Bootstrap类加载器命名为BuiltinClassLoader,扩展类加载器命名为PlatformClassLoader。

public class ClassLoaderPrintSearchPath {
    public static void main(String[] args) {
        try {
            Class<?> nonexistingClass = Class.forName("A.NONEXISTING.CLASS");
        } catch (ClassNotFoundException ex) {
            ex.printStackTrace();
        }
    }
}

程序运行结果:

> java ClassLoaderPrintSearchPath
java.lang.ClassNotFoundException: A.NONEXISTING.CLASS
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
        at java.base/java.lang.Class.forName0(Native Method)
        at java.base/java.lang.Class.forName(Class.java:315)
        at ClassLoaderPrintSearchPath.main(ClassLoaderPrintSearchPath.java:4)

3.3 自定义类加载器的使用方法

Java虚拟机还支持用户自定义类加载器,这为开发人员提供了更多的灵活度和扩展应用程序的方式。一个常见的、使用用户自定义类加载器的例子是程序动态创建.class文件,并将其加载入Java虚拟机中。Java虚拟机内置的类加载器都是从CLASSPATH中寻找.class文件,但是,如果.class文件是运行时动态生成的,那么,内置类加载器是无法找到它们的。所以,开发人员需要使用自定义类加载器,以实现自定义的寻找.class文件的方式。

所有的自定义加载器继承自ClassLoader类。我们先简短的介绍一下ClassLoader类中的几个重要的方法,然后再通过一个实例来解释自定义类加载器的使用方法。

3.3.1 loadClass方法
public Class<?> loadClass(String name);
protected Class<?> loadClass(String name, boolean resolve);

loadClass()方法的功能是寻找给定的类名,如果能找到该类,则将其加载入内存,并返回相应的Class对象。如果找不到该类,则抛出ClassNotFoundException异常。loadClass()方法有两种形式。第一种形式接收一个类的名称(例如:java.lang.String),并尝试加载该类。调用第一种方法等效于调用第二个方法,并且传递false给resolve参数。当resolve为true时,loadClass()方法不仅会加载该类,还会链接该类。

loadClass()按照如下的逻辑查找类。

  1. 调用findLoadedClass()方法查看该类是否已被加载。
  2. 调用父类的loadClass()方法。如果本类加载器没有父类的话,则使用Java虚拟机中的内置加载器的loadClass()方法。
  3. 调用findClass()方法查找类。
3.3.2 defineClass方法

defineClass()方法将按照.class文件格式排列的二进制的数据加载入内存,并返回相应的Class对象。在使用这个Class对象之前,需要链接并初始化该Class对象。如果在二进制数据中未发现Class信息,则抛出ClassFormatError错误。因为defineClass()方法是final的,所以,子类不能覆盖该方法。

protected final Class<?> defineClass(String name, byte[] b, int off, int len);
3.3.3 findClass方法

findClass()方法是自定义类加载器中最常见,最重要的一个方法。因为当父类加载器无法加载时,loadClass()方法会调用findClass()以寻找待加载的类。

protected Class<?> findClass(String name);
3.3.4 getParent方法

getParent()方法返回父类加载器。

public final ClassLoader getParent();
3.3.5 自定义类加载器示例

我们在下面的例子中创建了一个新的类加载器UserDefinedClassLoader。它继承自ClassLoader类,实现了findClass()方法。本例展示了一个从网络地址获取.class文件并加载的例子。该例子还可以改造成从文件系统加载、从数据库中加载、从压缩文件中加载的类加载器。

每当使用UserDefinedClassLoader加载类时,findClass()方法会在一个网络地址处寻找待加载的类。在本例中,待加载的类放在https://resource.littlewaterdrop.com/course/cs/java/UserDefinedClassLoader.class

当UserDefinedClassLoader.class文件的数据全部读入字符数组classData中后,调用defineClass()方法将其转化为Class对象。此时,UserDefinedClassLoader.class已成功加载如Java虚拟机中。

import java.net.URL;
import java.net.URLConnection;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;

public class UserDefinedClassLoader extends ClassLoader {
    @Override
    public Class<?> findClass(String className) throws ClassNotFoundException {
        try {
            // 构建UserDefinedClassLoader.class所在的网址
            URL url = new URL( String.format("https://resource.littlewaterdrop.com/course/cs/java/%s.class", className) );
            URLConnection connection = url.openConnection();
            InputStream input = connection.getInputStream();
            
            // 读取放在网络上的.class文件。
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            int nRead;
            byte[] data = new byte[1024];
            while ((nRead = input.read(data, 0, data.length)) != -1) {
                buffer.write(data, 0, nRead);
            }
            buffer.flush();
            byte[] classData = buffer.toByteArray();

            // 将二进制数据转换成Class对象。
            return defineClass(className, classData, 0, classData.length);
        } catch (Exception ex) {
            throw new ClassNotFoundException();
        }
    }

    public static void main(String[] args) {
        try {
            UserDefinedClassLoader cl = new UserDefinedClassLoader();
            Class<?> cls = cl.loadClass("UserDefinedClassLoader");

            // 此时UserDefinedClassLoader类已成功加载入Java虚拟机中,可以打印它的类名称。
            System.out.println(cls.getName());
        } catch (Exception ex) {}
    }
}

程序运行结果:

> java UserDefinedClassLoader
UserDefinedClassLoader

4 为什么采用类加载器的委派工作模式

我们认为采用委派工作模式至少有两个好处。其一:安全性考虑。因为Bootstrap类加载器是Java虚拟机的内置加载器,用于加载Java标准库中最为核心的类。正确的加载这些类(例如:java.lang.Object)是Java程序能够正确运行的基础与保证。所以,在调用loadClass()方法的过程中,Java虚拟机会首先调用父类,其目的是Bootstrap和其他内置加载器有更高的优先级,确保Java标准库中最核心的类是由内置加载器加载的;而不是由用户自定义的类加载器加载的。其二:可扩展性。为了给与开发人员更大的自由,Java虚拟机开发了类加载器接口,开发人员可以根据不同的需求,开发不同类型的类加载器。

5 总结

本章介绍了类加载器的原理和使用方法。类加载器是Java虚拟机核心部件之一,它负责将编译完成的.class文件加载入内存中,以便于Java虚拟机进行随后的链接和初始化工作。如何解析.class文件是加载过程的一个重要步骤,我们将在下一章介绍.class文件的结构。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.