如何自定义类加载器及如何打破双亲委派模型?
1. 类加载机制
1.1. 什么是类加载机制?
类加载(Class Loading)是指Java虚拟机将class
字节码文件加载到内存中并生成Class
对象的过程。
虚拟机参数:可以通过
-verbose:class
来打印类加载的日志。
1.2. 类加载有哪些步骤?
类加载有以下几个步骤:
- 通过全限定类名获取到类的
class
二进制字节流,即加载[1](Loading); - 验证
class
二进制字节流是否符合《Java虚拟机规范》定义的规范,即验证[2](Verification); - 为类的静态字段(
final
修饰的静态字段除外)分配内存并赋零值,即准备[3](Preparation); - 将符号引用转为直接引用,即解析[4](Resolution);
- 执行类构造器
<clinit>
方法,即初始化[5](Initializing)。
1.2.1. 在类加载的加载阶段加载不存在的类会抛出什么异常?
编写一个测试类,使用Class#forName
去加载一个并不存在的类java.lang.NotExistClass
。运行结果如下,从堆栈信息中可以看到虚拟机使用了AppClassLoader
去加载该类,且在执行URLClassLoader#findClass
方法时并未找到该类,故抛出了ClassNotFoundException
异常。
Exception in thread "main" java.lang.ClassNotFoundException: java.lang.NotExistClass
at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:264)
at com.remeio.upsnippet.jvm.classloader.LoadNotExistClassTest.main(LoadNotExistClassTest.java:6)
1.2.2. 在类加载的验证阶段验证失败会抛出什么错误?
编写一个类并将其编译为class
文件,然后在class
文件内容起始位置添加ERROR_FILE_CLASS
字符串,最后运行程序。运行结果如下,从堆栈信息中可以看到虚拟机在执行ClassLoader#defineClass1
方法时抛出了ClassFormatError
错误,原因是class
文件头部不是有效的魔数0xCAFEBABE
。
~\upsnippet\upsnippet-jvm\target\classes>java com.remeio.upsnippet.jvm.classloader.VerifyErrorTest
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 1163022927 in class file com/remeio/upsnippet/jvm/classloader/VerifyErrorTest
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(Unknown Source)
at java.security.SecureClassLoader.defineClass(Unknown Source)
at java.net.URLClassLoader.defineClass(Unknown Source)
at java.net.URLClassLoader.access$100(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.launcher.LauncherHelper.checkAndLoadMain(Unknown Source)
1.2.3. 在类加载的准备阶段不同类型的静态字段的零值分别是什么?
编写一个类并为其定义不同类型的非final
静态字段,然后打印各字段对应的零值。代码如下:
public class ZeroValueTest {
private static boolean booleanZeroValue;
private static byte byteZeroValue;
private static char charZeroValue;
private static short shortZeroValue;
private static int intZeroValue;
private static long longZeroValue;
private static float floatZeroValue;
private static double doubleZeroValue;
private static Object objectZeroValue;
private static int[] arrayZeroValue;
static {
printZeroValues(
"boolean", "byte", "char",
"short", "int", "long",
"float", "double", "object",
"array");
}
public static void main(String[] args) {
// Load this class
}
private static void printZeroValues(String... names) {
try {
for (String name : names) {
System.out.println(name + ": " + ZeroValueTest.class.getDeclaredField(name + "ZeroValue").get(ZeroValueTest.class));
}
} catch (Exception ignore) {
}
}
}
从打印信息中可以看到不同类型的静态字段的零值,运行结果如下:
boolean: false
byte: 0
char:
short: 0
int: 0
long: 0
float: 0.0
double: 0
object: null
array: null
1.2.4. 在类加载的初始化阶段初始化失败会抛出什么错误?
编写一个类并为其定义一个非final
修饰的静态字段,然后为该字段赋值为1 / 0
。代码如下:
public class InitializerErrorTest {
private static int notFinalStaticField = 1 / 0;
static {
notFinalStaticField = 1 / 0;
}
public static void main(String[] args) {
// Load this class
}
}
运行结果如下,从堆栈信息中可以看到虚拟机在执行InitializerErrorTest#<clinit>
方法时抛出了java.lang.ExceptionInInitializerError
错误,原因是初始化时抛出了ArithmeticException
异常。
Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.ArithmeticException: / by zero
at com.remeio.upsnippet.jvm.classloader.InitializerErrorTest.<clinit>(InitializerErrorTest.java:4)
在以上代码中并未定义<clinit>
方法,为什么在运行程序时会执行该方法?<clinit>
方法是虚拟机根据用户编写的非final
修饰的静态字段的赋值代码和类的静态代码块自动生成的,在类加载的初始化阶段会执行该方法。通过javap -v InitializerErrorTest.class
命令可以看到字节码如下:
...
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: iconst_1
1: iconst_0
2: idiv
3: putstatic #2 // Field notFinalStaticField:I
6: iconst_1
7: iconst_0
8: idiv
9: putstatic #2 // Field notFinalStaticField:I
12: return
LineNumberTable:
line 4: 0
line 7: 6
line 8: 12
...
1.3. 类加载的时机?
类的加载发生在类的初始化之前。《Java虚拟机规范》规定了当对一个类型进行主动引用时会触发类的初始化[5],有以下几种情况:
当执行Java程序的
main
方法时。当执行
new
,getstatic
,putstatic
,invokestatic
字节码指令时。new
字节码指令对应使用new
关键字创建对象。getstatic
字节码指令对应获取类中的静态字段(final
修饰的静态字段除外)。putstatic
字节码指令对应设置类中的静态字段。invokestatic
字节码指令对应调用类中的静态方法。
当使用反射的方式获取类型时。
- 当使用
Class#forName
加载类的时候; - 当使用
java.lang.reflect
包中的方法的时候。
- 当使用
父类要先于子类初始化。
当对一个类型进行被动引用时不会触发类的初始化,有以下几种情况:
- 当使用子类调用父类中的静态字段或方法时。
- 当使用数组定义来引用类时。
- 当获取被
final
修饰的类的静态字段时。
下面我们将以示例的方式来探究类加载的时机,首先编写一个类并定义一些内部类,并通过不同的方式来触发内部类的初始化。代码如下:
public class InitializerMomentTest {
public static void main(String[] args) {
// -verbose:class
// new
LoadWhenNew loadWhenNew = new LoadWhenNew();
// getstatic
int field = LoadWhenGetStatic.field;
// putstatic
LoadWhenPutStatic.field = 0;
// invokestatic
LoadWhenInvokeStatic.invoke();
// Class#forName
try {
Class.forName("com.remeio.upsnippet.jvm.classloader.InitializerMomentTest$LoadWhenClassForName");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
public static class LoadWhenNew extends LoadWhenBeforeChildLoad {
}
public static class LoadWhenGetStatic {
public static int field = 0;
}
public static class LoadWhenPutStatic {
public static int field = 0;
}
public static class LoadWhenInvokeStatic {
public static void invoke() {
}
}
public static class LoadWhenClassForName {
}
public static class LoadWhenBeforeChildLoad {
}
}
运行结果如下:
- 通过执行程序的
main
方法触发InitializerMomentTest
类的初始化。 - 通过实例化类来触发
LoadWhenNew
类的初始化。 - 由于父类要先于子类初始化,所以要先初始化
LoadWhenBeforeChildLoad
类。 - 通过获取类的非
final
修饰的静态字段来触发LoadWhenGetStatic
类的初始化。 - 通过设置类的非
final
修饰的静态字段来触发LoadWhenPutStatic
类的初始化。 - 通过访问类的静态方法来触发
LoadWhenInvokeStatic
类的初始化。 - 通过反射的方式触发
LoadWhenClassForName
类的初始化。
[Loaded com.remeio.upsnippet.jvm.classloader.InitializerMomentTest from file:/E:/project/remeio/upsnippet/upsnippet-jvm/target/classes/]
[Loaded com.remeio.upsnippet.jvm.classloader.InitializerMomentTest$LoadWhenBeforeChildLoad from file:/E:/project/remeio/upsnippet/upsnippet-jvm/target/classes/]
[Loaded com.remeio.upsnippet.jvm.classloader.InitializerMomentTest$LoadWhenNew from file:/E:/project/remeio/upsnippet/upsnippet-jvm/target/classes/]
[Loaded com.remeio.upsnippet.jvm.classloader.InitializerMomentTest$LoadWhenGetStatic from file:/E:/project/remeio/upsnippet/upsnippet-jvm/target/classes/]
[Loaded com.remeio.upsnippet.jvm.classloader.InitializerMomentTest$LoadWhenPutStatic from file:/E:/project/remeio/upsnippet/upsnippet-jvm/target/classes/]
[Loaded com.remeio.upsnippet.jvm.classloader.InitializerMomentTest$LoadWhenInvokeStatic from file:/E:/project/remeio/upsnippet/upsnippet-jvm/target/classes/]
[Loaded com.remeio.upsnippet.jvm.classloader.InitializerMomentTest$LoadWhenClassForName from file:/E:/project/remeio/upsnippet/upsnippet-jvm/target/classes/]
2. 类加载器
2.1. 有哪些类加载器?
- 启动类加载器(Bootstrap Class Loader),用来加载
${JAVA_HOME}/lib
下的jar
。 - 扩展类加载器(Extension Class Loader),用来加载
${JAVA_HOME}/lib/ext
下的jar
。 - 应用类加载器(Application Class Loader),用来加载
classpath
下的jar
。 - 自定义类加载器(User Class Loader),用来加载自定义的
class
二进制字节流。
加载不同的类使用的是什么类型的类加载器?
编写一段代码去加载java.lang.String
,javafx.event.Event
和DiffClassLoaderTest
类,并打印各类型对应的类加载器。代码如下:
import javafx.event.Event;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DiffClassLoaderTest {
public static void main(String[] args) {
printAllClassLoaders();
}
private static void printAllClassLoaders() {
// Bootstrap Class Loader
printClassLoader(String.class);
// Extension Class Loader
printClassLoader(Event.class);
// Application Class Loader
printClassLoader(DiffClassLoaderTest.class);
}
private static void printClassLoader(Class<?> clazz) {
log.info("The class loader of '{}' is '{}'", clazz, clazz.getClassLoader());
}
}
运行结果如下,可以看到:
- 加载
java.lang.String
(类所在路径为jre/lib/rt.jar
)的类加载器为启动类加载器(打印出来为null
)。 - 加载
javafx.event.Event
(类所在路径为jre/lib/ext/jfxrt.jar
)的类加载器为扩展类加载器。 - 加载
DiffClassLoaderTest
(类所在路径为classpath
)的类加载器为应用类加载器。
[Loaded java.lang.String from D:\program\java\jdk1.8\jre\lib\rt.jar]
[Loaded javafx.event.Event from file:/D:/program/java/jdk1.8/jre/lib/ext/jfxrt.jar]
[Loaded com.remeio.upsnippet.jvm.classloader.DiffClassLoaderTest from file:/E:/project/remeio/upsnippet/upsnippet-jvm/target/classes/]
The class loader of 'class java.lang.String' is 'null'
The class loader of 'class javafx.event.Event' is 'sun.misc.Launcher$ExtClassLoader@5010be6'
The class loader of 'class com.remeio.upsnippet.jvm.classloader.DiffClassLoaderTest' is 'sun.misc.Launcher$AppClassLoader@18b4aac2'
2.2. 类加载器加载类的过程?
- 首先,类加载器会判断该类是否被加载过,如果被加载过则直接返回;
- 如果没有被加载过,则判断该类加载器是否存在父类加载器,如果存在则委派给父类加载器去加载该类;
- 如果父类加载器未加载到该类,则当前类加载器去加载该类。
2.3. 如何自定义类加载器?
一般来说自定义类加载器,只需要重写ClassLoader#findClass
方法,在此方法中去实现class
二进制字节流的获取,并使用ClassLoader#defineClass
从class
二进制字节流中创建Class
对象。
除了加载JAVA_HOME
和classpath
下的类,如果我们还需要从别的路径加载类便需要自定义类加载器。我们定义一个自定义类加载器CustomClassPathClassLoader
,继承ClassLoader
类并重写ClassLoader#findClass
方法,让其从指定路径的文件中去获取class
二进制字节流,然后使用ClassLoader#defineClass
来创建Class
对象。代码如下:
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class CustomClassPathClassLoader extends ClassLoader {
private final String classpath;
public CustomClassPathClassLoader(String classpath) {
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
byte[] classData;
try {
classData = getClassDataFromFile(className);
} catch (IOException e) {
throw new ClassNotFoundException("File read fail", e);
}
return defineClass(className, classData, 0, classData.length);
}
private byte[] getClassDataFromFile(String className) throws IOException {
final String filePath = String.format("%s%s%s.class", classpath, File.separatorChar, className.replace('.', File.separatorChar));
int len;
try (FileInputStream inputStream = new FileInputStream(filePath);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
while ((len = inputStream.read()) != -1) {
outputStream.write(len);
}
return outputStream.toByteArray();
}
}
}
public class CustomClassLoaderTest {
public static void main(String[] args) throws Exception {
final String customClassPath = "E:\\project\\remeio\\upsnippet\\upsnippet-jvm\\src\\main\\java\\";
ClassLoader customClassPathClassLoader = new CustomClassPathClassLoader(customClassPath);
Class<?> clazz = customClassPathClassLoader.loadClass("com.remeio.upsnippet.jvm.classloader.lib.Test");
System.out.println("Class loader of '" + clazz + "': " + clazz.getClassLoader());
}
}
运行结果如下,可以看到Test
类是由自定义类加载器加载的。
Class loader of 'class com.remeio.upsnippet.jvm.classloader.lib.Test': com.remeio.upsnippet.jvm.classloader.CustomClassPathClassLoader@279f2327
3. 双亲委派模型
3.1. 什么是双亲委派模型?
双亲委派模型[1](Parents Delegation Model)是指当使用类加载器去加载一个类时,会首先委派给其父类加载器去加载,若父类加载器加载失败,自身才会去尝试加载。
3.2. 为什么需要双亲委派模型?
- 使类的加载具有层次性,避免类的重复加载;
- 为了安全性的考虑,避免用户自定义类来覆盖JDK中的类,如自定义
java.lang.Object
类。
3.3. 如何打破双亲委派模型?
- 双亲委派模型的实现逻辑在
ClassLoader#loadClass
方法中,可以通过自定义类加载器重写该方法打破双亲委派模型; - 使用SPI(Service Provider Interface)机制,使用线程上下文中的类加载器加载类。
3.4. 什么是SPI机制?
SPI(Service Provider Interface)机制是指为满足服务商不同的需求,由JDK源码定义服务接口,服务商提供不同的服务实现。加载JDK源码(这里指rt.jar
)使用的是启动类加载器,无法加载classpath
下的服务商提供的服务实现,而SPI机制则利用线程上下文中的类加载器即应用类加载器去加载服务商提供的服务实现,如此以来便打破了双亲委派模型。
下面将以示例的方式探究SPI机制是如何打破双亲委派模型的。首先编写使用MySQL
驱动去连接数据库的代码,并打印当前线程上下文中的类加载器,加载服务定义java.sql.Connection
的类加载器以及加载服务实现com.mysql.cj.jdbc.ConnectionImpl
的类加载器。代码如下:
import lombok.extern.slf4j.Slf4j;
import java.sql.*;
@Slf4j
public class SPITest {
public static void main(String[] args) throws SQLException {
// Context class loader of current thread
printContextClassLoaderOfCurrentThread();
// rt.jar/Connection -> Bootstrap Class Loader
printClassLoader(Connection.class);
// classpath -> App Class Loader
printClassLoader(SPITest.class);
try (Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/upsnippet",
"root",
"******")) {
// SPI -> Context class loader of current thread
printClassLoader(connection.getClass());
try (Statement statement = connection.createStatement()) {
ResultSet resultSet = statement.executeQuery("select 1 + 2");
while (resultSet.next()) {
log.info("1 + 2 = {}", resultSet.getInt(1));
}
}
}
}
private static void printClassLoader(Class<?> clazz) {
log.info("The class loader of '{}' is '{}'", clazz, clazz.getClassLoader());
}
private static void printContextClassLoaderOfCurrentThread() {
log.info("The class loader of '{}' is '{}'", Thread.currentThread(), Thread.currentThread().getContextClassLoader());
}
}
运行结果如下,可以看到当前线程上下文中的类加载器为应用类加载器,加载服务定义java.sql.Connection
的类加载器为启动类加载器,加载服务实现com.mysql.cj.jdbc.ConnectionImpl
的类加载器同当前线程上下文中的类加载器一致,都是应用类加载器。
The class loader of 'Thread[main,5,main]' is 'sun.misc.Launcher$AppClassLoader@18b4aac2'
The class loader of 'interface java.sql.Connection' is 'null'
The class loader of 'class com.remeio.upsnippet.jvm.classloader.SPITest' is 'sun.misc.Launcher$AppClassLoader@18b4aac2'
The class loader of 'class com.mysql.cj.jdbc.ConnectionImpl' is 'sun.misc.Launcher$AppClassLoader@18b4aac2'
1 + 2 = 3
从源码上讲,java.sql.DriverManager
类在类加载的初始化阶段会调用DriverManager#loadInitialDrivers
方法加载所有由服务商提供的服务接口java.sql.Driver
的服务实现。具体步骤为:
- 调用
ServiceLoader#load
方法返回一个ServiceLoader
; - 从
META-INF/services/java.sql.Driver
文件中获取由服务商提供的服务实现的类名; - 通过当前线程上下文中的类加载器即应用类加载器去加载该服务实现。
对应源码所在位置:
java.sql.DriverManager#loadInitialDrivers
。java.util.ServiceLoader.LazyIterator#nextService
。
4. 参考文档
- 1.Chapter 5. Loading, Linking, and Initializing - Creation and Loading ↩
- 2.Chapter 5. Loading, Linking, and Initializing - Verification ↩
- 3.Chapter 5. Loading, Linking, and Initializing - Preparation ↩
- 4.Chapter 5. Loading, Linking, and Initializing - Resolution ↩
- 5.Chapter 5. Loading, Linking, and Initializing - Initialization ↩