Loading
Loading...

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

系统环境:

  • JDK 1.8

参考地址:

深入浅出 JVM 系列文章

一、什么是类加载器

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

二、抽象类 ClassLoader

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

public abstract class ClassLoader {
/*
* 类加载器的父加载器
*/
private final ClassLoader parent;
/**
* 根据类的全限定名加载类
*
* @param name 类名称
* @return 加载的Class对象
* @throws ClassNotFoundException 没有发现指定类异常
*/
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 调用loadClass方法加载类,其中设置resolve=false,表示不立即解析类
return loadClass(name, false);
}
/**
* 根据类的全限定名加载类
*
* @param name 类名称
* @param resolve 是否解析这个类,true=解析,false=不解析
* @return 加载的Class对象
* @throws ClassNotFoundException 没有发现指定类异常
*/
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 检查类是否已经被加载
Class<?> c = findLoadedClass(name);
// 如果没有加载过
if (c == null) {
// 如果有父类加载器,则委托给父加载器去加载
// 如果没有父类加载器,则判断 Bootstrap 类加载器是否加载过
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
// 如果父类加载器都加载失败,则当前类加载器尝试自行加载
if (c == null) {
c = findClass(name);
}
}
// 据 resolve 参数决定是否解析类
if (resolve) {
resolveClass(c);
}
return c;
}
}
/**
* 查找并加载指定名称的类
*
* @param name 类名称
* @return Class对象
* @throws ClassNotFoundException 没有发现指定类异常
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
//1. 根据传入的类名,到在特定目录下去寻找类文件,把字节码文件读入内存
// ...
//2. 调用 defineClass 将字节数组转成 Class 对象
return defineClass(buf, off, len);
}
/**
* 将一个 byte[] 转换为 Class 类的实例
*
* @param name 类名称,如果不知道此名称,则该参数为 null
* @param b 组成类数据的字节数组
* @param off 类数据的起始偏移量
* @param len 类数据的长度
* @return Class对象
* @throws ClassFormatError 类格式化异常
*/
protected final Class<?> defineClass(byte[] b, int off, int len) throws ClassFormatError {
...
}
}

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

方法名称 描述
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.propertiesJVM 网络配置信息
classlist该文件内表示是引导类加载器应该加载的类的清单
jsse.jar安全套接字拓展包 Java(TM) Secure Socket Extension
rt.jarrt 全称是 runtime,是一个运行环境包,J2SE 相关类的定义都在这个包内
jce.jarjce.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) 实现自定义类加载器

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

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
/**
* 获取类的字节码
*
* 注: 重写 findClass() 方法
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 实现一些自定义功能
System.out.println("实现一些自定义功能...");
// 获取类的字节数组
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 new byte[0];
}
/**
* 得到类文件的完全路径
*
* @param className 类名称
* @return 类文件的完全路径
*/
private String classNameToPath(String className) {
return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
}
}

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

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

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

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

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

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

public class Sample {
private Sample instance;
public void setSample(Object instance) {
System.out.println("开始初始化对象: " + instance.toString());
this.instance = (Sample) instance;
}
}

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

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

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

import java.lang.reflect.Method;
public class ClassIdentity {
public void testClassIdentity() {
// 指定加载字节码的目录
String classDataRootPath = "D:\\";
// 使用自定义类加载器进行加载
FileSystemClassLoader fscl = new FileSystemClassLoader(classDataRootPath);
// 指定字节码包名(如果编译时的类没有指定包,则直接输入类名Sample)
String className = "mydlq.club.Sample";
try {
// 加载 Sample 类,使用反射创建 Sample 对象
Class<?> class1 = fscl.loadClass(className);
Object obj1 = class1.getDeclaredConstructor().newInstance();
// 使用反射调用方法 setSample(Object instance) 方法
Method sampleMethod = class1.getMethod("setSample", java.lang.Object.class);
sampleMethod.invoke(obj1, obj1);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new ClassIdentity().testClassIdentity();
}
}

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

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

---END---

本文作者:超级小豆丁 @ 小豆丁技术栈

本文链接:http://www.mydlq.club/article/131/

本文标题:深入浅出 JVM 之类加载-类加载器

本文版权:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!