深入浅出 JVM 之类加载-类加载器
文章目录
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。
系统环境:
- JDK 1.8
参考地址:
- JVM 学习笔记
- Java 类加载机制
- Oracle Java 虚拟机规范
- 深入理解Java虚拟机: JVM高级特性与最佳实践(第3版)
深入浅出 JVM 系列文章
- 01.深入浅出 JVM 之 Java 虚拟机
- 02.深入浅出 JVM 之 Class 字节码文件
- 03.深入浅出 JVM 之字节码-常量池常量分析
- 04.深入浅出 JVM 之字节码-指令集简介
- 05.深入浅出 JVM 之类加载-类加载流程
- 06.深入浅出 JVM 之类加载-类加载器
- 07.深入浅出 JVM 之运行时数据区
- 08.深入浅出 JVM 之运行时数据区-堆
- 09.深入浅出 JVM 之运行时数据区-方法区
- 10.深入浅出 JVM 之运行时数据区-虚拟机栈
- 11.深入浅出 JVM 之运行时数据区-程序计数器
- 12.深入浅出 JVM 之垃圾回收-垃圾回收概述与算法
- 13.深入浅出 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 和配置。不过出于安全考虑,引导类加载器只能加载包名为 java
、javax
、sum
等开头的类,然后将这些系统类加载到方法区内,比如:
文件 | 描述 |
---|---|
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---
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。