深入浅出 JVM 之类加载-类加载器

深入浅出 JVM 之类加载-类加载器

文章目录

  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。

系统环境:

  • JDK 1.8

参考地址:

深入浅出 JVM 系列文章

一、什么是类加载器

在 Java 中,类的初始化分为几个阶段: 加载链接(包括验证、准备和解析)和 初始化。而 类加载器(Class Loader)则是加载阶段中,负责将本地或网络中的指定类的二进制流,加载到 Java 虚拟机中的工具。

二、抽象类 ClassLoader

在 Java 中存在一个类加载器抽象类 ClassLoader,大多数类加载器都是通过继承这个类来实现的类加载功能。以下是 ClassLoader 类的关键部分代码:

 1public abstract class ClassLoader {
 2
 3    /*
 4     * 类加载器的父加载器
 5     */
 6    private final ClassLoader parent;
 7
 8    /**
 9     * 根据类的全限定名加载类
10     * 
11     * @param name 类名称 
12     * @return     加载的Class对象
13     * @throws ClassNotFoundException 没有发现指定类异常
14     */
15    public Class<?> loadClass(String name) throws ClassNotFoundException {
16        // 调用loadClass方法加载类,其中设置resolve=false,表示不立即解析类
17        return loadClass(name, false);
18    }
19
20    /**
21     * 根据类的全限定名加载类
22     * 
23     * @param name    类名称
24     * @param resolve 是否解析这个类,true=解析,false=不解析 
25     * @return 加载的Class对象
26     * @throws ClassNotFoundException 没有发现指定类异常
27     */
28    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
29        synchronized (getClassLoadingLock(name)) {
30            // 检查类是否已经被加载
31            Class<?> c = findLoadedClass(name);
32            // 如果没有加载过
33            if (c == null) {
34                // 如果有父类加载器,则委托给父加载器去加载
35                // 如果没有父类加载器,则判断 Bootstrap 类加载器是否加载过
36                if (parent != null) {
37                    c = parent.loadClass(name, false);
38                } else {
39                    c = findBootstrapClassOrNull(name);
40                }
41                // 如果父类加载器都加载失败,则当前类加载器尝试自行加载
42                if (c == null) {
43                    c = findClass(name);
44                }
45            }
46            // 据 resolve 参数决定是否解析类
47            if (resolve) {
48                resolveClass(c);
49            }
50            return c;
51        }
52    }
53
54    /**
55     * 查找并加载指定名称的类
56     * 
57     * @param name 类名称 
58     * @return Class对象
59     * @throws ClassNotFoundException 没有发现指定类异常
60     */
61    protected Class<?> findClass(String name) throws ClassNotFoundException {
62        //1. 根据传入的类名,到在特定目录下去寻找类文件,把字节码文件读入内存
63        // ...
64        //2. 调用 defineClass 将字节数组转成 Class 对象
65        return defineClass(buf, off, len)
66    }
67
68    /**
69     * 将一个 byte[] 转换为 Class 类的实例
70     * 
71     * @param name 类名称,如果不知道此名称,则该参数为 null
72     * @param b    组成类数据的字节数组
73     * @param off  类数据的起始偏移量
74     * @param len  类数据的长度 
75     * @return Class对象
76     * @throws ClassFormatError 类格式化异常
77     */
78    protected final Class<?> defineClass(byte[] b, int off, int len) throws ClassFormatError {
79        ...
80    }
81
82}

类中定义的常用的类加载相关的方法:

方法名称 描述
getParent() 返回该类加载器的父类加载器
loadClass(String name) 加载指定名称的类,返回 java.lang.Class 实例
findClass(String name) 查找指定名称的类,返回 java.lang.Class 实例
findLoadedClass(String name) 查找已加载的指定名称的类,返回 java.lang.Class 实例
defineClass(String name, byte[] b, int off, int len) 将字节数组转换为一个 Java 类,返回 java.lang.Class 实例
resolveClass(Class c) 连接指定的 Java 类

三、类加载器类型

在 Java 虚拟机中类加载器主要有三种,分别为:

  • 引导类加载器 (BootstrapClassLoader)
  • 扩展类加载器 (ExtClassLoader)
  • 应用类加载器 (AppClassLoader)

3.1 引导类加载器 (BootstrapClassLoader)

引导类加载器 (Bootstrap Classloader) 是使用 C++ 语言实现的,没有继承 ClassLoader 类,嵌入在 Java 虚拟机内部,Java 程序无法直接操作这个类加载器。它主要用于加载 Java 中的核心类库,如 Java 虚拟机运行所需的一些依赖库。

引导类加载器一般情况下,会加载 JAVA_HOME 目录下的 /lib 文件夹中的 jar 和配置,或者被 -Xbootclasspath 参数所指定的目录中的 jar 和配置。不过出于安全考虑,引导类加载器只能加载包名为 javajavaxsum 等开头的类,然后将这些系统类加载到方法区内,比如:

文件 描述
charsets.jar 字符集支持包
net.properties JVM 网络配置信息
classlist 该文件内表示是引导类加载器应该加载的类的清单
jsse.jar 安全套接字拓展包 Java(TM) Secure Socket Extension
rt.jar rt 全称是 runtime,是一个运行环境包,J2SE 相关类的定义都在这个包内
jce.jar jce.jar 是一组包,它们提供用于加密、密钥生成和协商以及 Message Authentication Code (MAC) 算法的框架和实现

3.2 扩展类加载器 (ExtClassLoader)

扩展类加载器 (ExtClassLoader) 是 Java 语言编写的,继承了 ClassLoader 类,根据该类加载器名也可以知道,它主要负责加载 Java 的扩展类库。

扩展类加载器一般情况下,会加载 JAVA_HOME 目录下的 /lib/ext 文件夹中的 jar,或由 java.ext.dirs 系统变量指定目录中的 jar。

3.3 应用类加载器 (AppClassLoader)

应用类加载器 (AppClassLoader) 是 Java 语言编写的,继承了 ClassLoader 类,是应用程序中默认的类加载器。

应用类加载器可以加载 CLASSPATH 变量指定目录下的 jar,并且一般情况下,我们编写的 Java 类大部分都是使用该类加载器完成加载的。

四、双亲委派机制

4.1 双亲委派机制是什么

双亲委派机制是 Java 类加载机制中非常重要的一环,用于确保类加载的安全性和一致性。

双亲委派机制的核心思想就是: 当一个类加载器接收到类加载的请求时,它自己不会先去尝试加载这个类,而是把这个类加载请求委托给父类加载器。每个类加载器都采用相同的方式,直至委托给最顶层的引导类加载器(Bootstrap ClassLoader)为止。如果父类加载器无法加载委托的类时,子类加载器才会尝试自己去加载,如果子类加载器也无法加载该类,就会抛出一个 ClassNotFoundException 异常。

4.2 双亲委派机制的作用

  • ① 防止重复加载字节码文件: 将类加载请求先委托给父类,父类加载后子类就不会重复加载该类。所以,双亲委派机制可以防止对某个类重复加载;
  • ② 防止核心字节码文件被篡改: 一般情况下引导类加载器会先加载 JVM 核心类库,然后其它加载器才会执行,如果其它加载器要加载一个被篡改的核心字节码文件,会将该文件委托给父类加载器,当委托到引导类加载器时,加载器已经加载过该类,就不会对该类进行重复加载。而且就算能被加载,那么加载它的肯定不是相同的类加载器 (不会是引导类加载器),Java 虚拟机中只认可核心类加载器加载的核心类库,所以,双亲委派机制可以防止核心字节码文件被篡改。
  • ③ 简化加载逻辑: 通过委派模式,每个类加载器只需要关注自己负责的那部分类加载逻辑,而不必关心其他类加载器的加载细节,简化了类加载器的实现,降低了系统的复杂度。

考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的 String 类来动态替代 Java 核心 API 中定义的类型,这样会存在非常大的安全隐患。而双亲委托的方式,就可以避免这种情况,因为 String 已经在启动时就被引导类加载器 (BootstrcpClassLoader) 加载,所以用户自定义的 ClassLoader 永远也无法加载一个用户自己自定义的 String 类,除非你改变 JDK 中 ClassLoader 搜索类的默认算法。

4.3 如何判断两个字节码相同

Java 虚拟机在判定两个字节码是否相同时,不仅要判断类名是否相同,还要判断是否由同一个类加载器实例加载的。只有两者同时满足,虚拟机才认为两个字节码相同。如果内容相同,但由不同的类加载器加载,虚拟机也会认为它们是不同的字节码。

五、自定义类加载器

5.1 为什么要自定义类加载器

默认的类加载器只能加载指定目录下的 Jar 和 Class 文件。如果需要加载指定位置的类文件并实现一些自定义逻辑,就需要自定义类加载器。

比如,我要加载网络上的一个 Class 文件,通过动态加载到内存之后,要调用这个类中的方法实现用户自定义的业务逻辑,在这样的情况下,默认的 ClassLoader 就不能满足我们的需求了,所以需要我们自己去定义带有指定业务逻辑的 ClassLoader。

5.2 如何自定义一个类加载器

实现一个自定义类加载器很简单,只需要继承 ClassLoader 抽象类,然后重写里面的 findClass() 方法,就可以实现一个自定义的类加载器。

在默认的情况下,自定义的类加载器还是会遵循双亲委派机制,如果想自己实现的类加载器不遵循双亲委派机制,则需要在重写 findClass() 方法的同时,也要重写 loadClass() 方法。

5.3 实现自定义类加载器示例

(1) 实现自定义类加载器

如下代码所示,是一个自定义类加载器的示例,实现了文件系统类加载器功能:

 1import java.io.ByteArrayOutputStream;  
 2import java.io.File;  
 3import java.io.FileInputStream;  
 4import java.io.IOException;  
 5import java.io.InputStream;  
 6  
 7public class FileSystemClassLoader extends ClassLoader {  
 8  
 9    private String rootDir;  
10  
11    public FileSystemClassLoader(String rootDir) {  
12        this.rootDir = rootDir;  
13    }  
14  
15    /**
16     * 获取类的字节码 
17     * 
18     * 注: 重写 findClass() 方法
19     */
20    @Override  
21    protected Class<?> findClass(String name) throws ClassNotFoundException {  
22        // 实现一些自定义功能
23        System.out.println("实现一些自定义功能...");
24        // 获取类的字节数组  
25        byte[] classData = getClassData(name);  
26        if (classData == null) {  
27            throw new ClassNotFoundException();  
28        } else {  
29            return defineClass(name, classData, 0, classData.length);  
30        }  
31    }  
32  
33    private byte[] getClassData(String className) {  
34        // 读取字节码文件
35        String path = classNameToPath(className);
36        try (InputStream ins = new FileInputStream(path)) {
37            ByteArrayOutputStream baos = new ByteArrayOutputStream();
38            int bufferSize = 4096;
39            byte[] buffer = new byte[bufferSize];
40            int bytesNumRead = 0;
41            while ((bytesNumRead = ins.read(buffer)) != -1) {
42                baos.write(buffer, 0, bytesNumRead);
43            }
44            return baos.toByteArray();
45        } catch (IOException e) {
46            e.printStackTrace();
47        }
48        return new byte[0];
49    }  
50  
51    /**
52     * 得到类文件的完全路径
53     * 
54     * @param  className 类名称
55     * @return 类文件的完全路径
56     */
57    private String classNameToPath(String className) {    
58        return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";  
59    }  
60
61}  

如上所示, 通过继承 ClassLoader 实现了 FileSystemClassLoader 类加载器,重写了里面的 findClass() 方法,而没有重写 ClassLoader 类的 loadClass() 方法,这是因为里面封装了双亲委派机制的实现,如:

  • 该方法会首先调用 findLoadedClass() 方法来检查该类是否已经被加载过;
  • 如果没有加载过的话,会调用父类加载器的 loadClass() 方法来尝试加载该类;
  • 如果父类加载器无法加载该类的话,就调用 findClass() 方法来查找该类;

因此,为了保证类加载器都正确实现双亲委派机制,在开发自己的类加载器时,只需要重写 findClass() 方法即可。当然,如果不想使用双亲委派机制时,就需要重写 loadClass() 方法。

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

(2) 创建用于测试的字节码文件

创建用于测试的 Sample 类,代码如下:

 1public class Sample {  
 2  
 3    private Sample instance;  
 4  
 5    public void setSample(Object instance) {  
 6        System.out.println("开始初始化对象: " + instance.toString());  
 7        this.instance = (Sample) instance;  
 8    }  
 9
10}

然后对类进行编译,将编译后的 .class 字节码文件放到 D:\\ 盘,方便后续测试。

(3) 使用自定义类加载器

如使用 FileSystemClassLoader 类加载器加载本地文件系统上的类,操作如下:

 1import java.lang.reflect.Method;  
 2  
 3public class ClassIdentity {  
 4  
 5    public void testClassIdentity() {
 6        // 指定加载字节码的目录
 7        String classDataRootPath = "D:\\";
 8        // 使用自定义类加载器进行加载
 9        FileSystemClassLoader fscl = new FileSystemClassLoader(classDataRootPath);
10        // 指定字节码包名(如果编译时的类没有指定包,则直接输入类名Sample)
11        String className = "mydlq.club.Sample";
12        try {
13            // 加载 Sample 类,使用反射创建 Sample 对象
14            Class<?> class1 = fscl.loadClass(className);
15            Object obj1 = class1.getDeclaredConstructor().newInstance();
16            // 使用反射调用方法 setSample(Object instance) 方法
17            Method sampleMethod = class1.getMethod("setSample", java.lang.Object.class);
18            sampleMethod.invoke(obj1, obj1);
19        } catch (Exception e) {
20            e.printStackTrace();
21        }
22    } 
23
24    public static void main(String[] args) {  
25        new ClassIdentity().testClassIdentity();  
26    }  
27  
28}

然后点击运行,可以看到控制台会输出如下内容:

1实现一些自定义功能...
2开始初始化对象: mydlq.club.Sample@b684286

---END---


  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。