文章目录
深入JVM:从源码剖析双亲委派机制
一、序言
对于Java工程师而言,深入理解JVM(Java虚拟机)不仅是掌握Java程序运行机制的基础,也是提升系统性能、优化应用和解决复杂问题能力的重要一步,更是Java进阶之路的重中之重。
本文小豪将带大家探索双亲委派,从源码剖析类加载器的双亲委派机制,同时结合案例,分析如何打破双亲委派,话不多说,我们直接进入正题。
文章最后附有流程图,进一步帮我们梳理双亲委派业务流程
二、类加载器
上一篇【深入JVM:从类加载机制解读类的生命周期】我们了解到类加载器主要负责在程序运行时将类的字节码文件加载到内存中,同时创建对应的Class对象。类加载器在加载阶段完成类的加载、链接和初始化。
1、类加载器的分类
类加载器主要分为两类:
-
虚拟机底层实现的类加载器
由Java虚拟机底层源码实现的类加载器,源码位于Java虚拟机的源码中,与虚拟机底层实现语言一致(C或C++),用于加载支撑Java程序运行的一些基础核心类
-
Java实现的类加载器
由Java代码编写的类加载器,所有Java实现的类加载器都继承于
ClassLoader
抽象类,JDK中默认提供了多种处理不同渠道的类加载器,我们也可以根据业务需求自定义类加载器
2、JDK8默认的类加载器
对于大多数企业采用的Java8版本来说,默认会有三个类加载器:
-
启动类加载器(Bootstrap)
负责加载Java的基础核心类库,为虚拟机内置的类加载器,位于最顶层,没有父加载器,默认加载Java安装目录/jre/lib下的类文件。
-
扩展类加载器(Extension)
负责加载Java的特殊扩展类库,为启动类加载器
Bootstrap
的子加载器,默认加载Java安装目录/jre/lib/ext下的类文件。 -
应用程序类加载器(Application)
负责加载应用程序的类,为扩展类加载器
Extension
的子加载器,加载用户类路径classpath
下的类库。
这里的父/子加载器,不是Java中的父子类关系,只是代表一种层级结构
三、双亲委派机制
1、概念
双亲委派机制,相信大家都或多或少有所耳闻,首先双亲委派是Java类加载器中比较重要的一个知识点,本质上是为处理类加载过程中某个类具体应该交由谁来加载。
在JVM中,加载某一个类,先委托上一级的父加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托的父加载器没有被加载,子加载器就会尝试自己加载该类。即向上委托,向下加载。
2、源码分析
接下来我们结合源码分析一下这个过程。
扩展类加载器和应用程序类加载器都是使用Java代码编写的类加载器,均继承自URLClassLoader
类,而URLClassLoader
类又继承自SecureClassLoader
类,SecureClassLoader
类继承自ClassLoader抽象类。
ClassLoader
抽象类作为Java实现的类加载器的顶层抽象类,我们由它作为入口,先看一下它的内部构造:
本文粘贴的部分源码进行了精简,仅保留关键内容
public abstract class ClassLoader {
// 用于委托的父类加载器
private final ClassLoader parent;
// 类加载的入口,内部定义了双亲委派机制,调用findClass方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
// 默认未实现,由ClassLoader子类实现,调用defineClass方法
// 如URLClassLoader会根据文件路径去获取类文件中的二进制数据
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
// 校验类名,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
// 解析类,执行类生命周期中的连接阶段
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}
}
观察源码发现,ClassLoader
抽象类内部通过parent属性,定义了当前类加载器的父类加载器。每一个Java代码实现的类加载器都通过parent
属性,指向上一级父类加载器。
扩展类加载器
ExtClassLoader
较为特殊,其parent
=null
,如果类加载器没有parent
,则认为其父类加载器为启动类加载器Bootstrap
介绍一下里面的几个重要方法:
- loadClass():类加载的入口,内部定义了双亲委派机制
- findClass():查找类字节码文件,如
URLClassLoader
实现类会根据文件路径去查找 - defineClass():校验类名,加载字节码信息加载到JVM内存
- resolveClass():解析类,执行类生命周期中的连接阶段
其中loadClass()
方法为核心方法,其作用就是动态加载类,内部就是双亲委派模型的实现,loadClass()
方法源码如下:
// 类加载的入口,内部定义了双亲委派机制,调用findClass方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 加锁
synchronized (getClassLoadingLock(name)) {
// 检查此name的Class是否已经被加载
Class<?> c = findLoadedClass(name);
// 未被加载
if (c == null) {
long t0 = System.nanoTime();
try {
// 存在父类加载器,委托父加载器进行加载,递归调用
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// parent = null,不存在父类加载器,委托启动类加载器Bootstrap进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 如果父类加载器未加载成功,则尝试自己完成加载
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
// 解析类
if (resolve) {
resolveClass(c);
}
return c;
}
}
没错,逻辑就是这么朴实无华,大致总结一下:
- 检查缓存:当类加载器加载某个类时,会先检查该类是否已经被加载过,若缓存存在则直接获取。
- 委托父类加载器:如果该类未被加载过,则向上委托给父类加载器进行加载
- 存在父类加载器:递归调用父类加载器执行加载
- 不存在父类加载器:由启动类加载器
Bootstrap
执行加载
- 自身执行加载:若父类未加载成功,则尝试自己完成加载
至此,结合源码,我们也彻底搞懂了双亲委派机制的实现:类加载器通过内部维护一个parent
属性,指向父类加载器,当进行类加载时,优先传递给父类加载器执行加载,当父类加载器未加载成功后,则会自己完成加载。
那为何要如此设计呢?
答案也显而易见,通过双亲委派机制可以避免某一个类被重复加载,当父加载器已经加载后则子类无需重复加载,保证唯一性。同时也为了安全,保证基础类库API不会被修改。
3、结论验证
这里我们验证一下:
public static void main(String[] args) {
// 获取自定义类UserInfo的类加载器
ClassLoader cur = UserInfo.class.getClassLoader();
System.out.println("用户类的类加载器为:" + cur);
ClassLoader parent = cur.getParent();
System.out.println("父类加载器为:" + parent);
ClassLoader parentParent = parent.getParent();
System.out.println("父类加载器的父类加载器为:" + parentParent);
}
控制台输出:
用户类的类加载器为:sun.misc.Launcher$AppClassLoader@18b4aac2
父类加载器为:sun.misc.Launcher$ExtClassLoader@4f47d241
父类加载器的父类加载器为:null
这里也进一步验证了类加载器的层级关系:我们自己定义的UserInfo
类由应用程序类加载器AppClassLoader
进行加载,AppClassLoader
的parent
为扩展类加载器ExtClassLoader
,而ExtClassLoader
的parent
为null
,具体指向启动类加载器BootstrapClassLoader
(因为启动类加载器不是由Java语言实现的,而是由虚拟机底层实现的,因此打印不出来)。
四、打破双亲委派
1、自定义类加载器
在介绍打破双亲委派的方法之前,我们先认识一下如何实现一个自定义的类加载器。
步骤一:创建自定义加载器类,继承ClassLoader
抽象类
步骤二:定义加载的路径classPath
,通过构造方法传入classPath
步骤三:重写findClass()
方法,实现从指定的路径classPath
下读取编译后的.class文件,转为二进制数组,调用父类defineClass()
方法将二进制字节码信息加载到虚拟机内存中
public class UserClassLoader extends ClassLoader {
// 自定义类加载器加载的路径
private String classPath;
public UserClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}
private byte[] loadClassData(String className) {
String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
// 字节码文件转为二进制数据流
try (FileInputStream fis = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int ch;
while ((ch = fis.read()) != -1) {
baos.write(ch);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws Exception {
// 传入.class文件存放路径
UserClassLoader classLoader = new UserClassLoader("E:\\My_IDEAProjects\\jvmtest\\");
// 加载的文件名
Class<?> clazz = classLoader.loadClass("UserInfo");
// 自定义的类加载器
ClassLoader cur = clazz.getClassLoader();
System.out.println("类加载器:" + cur);
// 自定义的类加载器的父加载器
ClassLoader parent = cur.getParent();
System.out.println("父类加载器:" + parent);
}
}
对应目录下存放编译后的UserInfo.class
文件:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
public class UserInfo {
public static int age = 25;
public UserInfo() {
}
static {
age = 24;
}
}
执行main方法,控制台输出:
类加载器:com.xiaohao.jvm.UserClassLoader@68f7aae2
父类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
这里我们发现自定义的类加载器的父类加载器竟然是应用程序类加载器AppClassLoader
,这是由于我们自定义类加载器继承自ClassLoader
抽象类,ClassLoader
构造方法的第二个参数调用了getSystemClassLoader()
方法,其返回值为AppClassLoader
,最后将parent
属性设置为应用程序类加载器AppClassLoader
:
具体源码为:
protected ClassLoader() {
// getSystemClassLoader() 返回值 AppClassLoader
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
// AppClassLoader赋值给parent
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
于是,我们的类加载器关系变为:
这里小豪给大家留个疑问,如果我们定义多个自定义类加载器加载同一个类,会不会产生冲突呢?
2、方式一:自定义类加载器重写loadClass方法
在上文查看ClassLoader
抽象类源码时,我们知道,其loadClass()
方法内部就是双亲委派机制的实现,那么我们想要破坏双亲委派的话,就可以重写其loadClass()
方法。
将我们自定义的类加载器UserClassLoader
改造一下,重写loadClass()
方法,删除通过parent
属性向上委托的逻辑:
public class UserClassLoader extends ClassLoader {
// 自定义类加载器加载的路径
private String classPath;
public UserClassLoader(String classPath) {
this.classPath = classPath;
}
// 关键!!!
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// Java中所有的类默认继承Object,Object类由父类加载
if (name.contains("Object")) {
return super.loadClass(name);
}
// 删除判断parent是否存在,向上委托的逻辑
// 直接调用findClass方法
return findClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}
private byte[] loadClassData(String className) {
String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
// 字节码文件转为二进制数据流
try (FileInputStream fis = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int ch;
while ((ch = fis.read()) != -1) {
baos.write(ch);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws Exception {
// 传入字节码.class文件存放路径
UserClassLoader classLoader = new UserClassLoader("E:\\My_IDEAProjects\\jvmtest\\");
// 加载的文件名
Class<?> clazz = classLoader.loadClass("UserInfo");
// 自定义的类加载器
ClassLoader cur = clazz.getClassLoader();
System.out.println("类加载器:" + cur);
// 自定义的类加载器的父加载器
ClassLoader parent = cur.getParent();
System.out.println("父类加载器:" + parent);
}
}
没错,就是这么简单粗暴,我们通过重写loadClass()
方法,破坏掉了双亲委派机制。
3、方式二:线程上下文类加载器(SPI机制)
我们先看一下使用JDBC连接MySQL数据库的操作代码:
public static void main(String[] args) {
// 数据库配置信息
String url = "jdbc:mysql://localhost:3306/spring_study?serverTimezone=Asia/Shanghai";
String user = "root";
String password = "123456";
// 数据库连接对象
Connection conn = null;
// 用于执行SQL语句的对象
Statement stmt = null;
// 结果集对象
ResultSet rs = null;
try {
// 加载数据库驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 建立连接
conn = DriverManager.getConnection(url, user, password);
// 创建Statement对象
stmt = conn.createStatement();
// 执行查询
rs = stmt.executeQuery("select name,age from user");
// 处理结果
while (rs.next()) {
System.out.println(rs.getString("name") + ":" + rs.getInt("age"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
try {
if (rs != null) {
rs.close();
}
if (stmt != null) {
stmt.close();
}
if (conn != null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
核心流程比较简单,无非是加载数据库驱动、获取数据库连接、创建Statement
对象,之后执行相应的数据库操作。
// 加载数据库驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 建立连接
conn = DriverManager.getConnection(url, user, password);
// 创建Statement对象
stmt = conn.createStatement();
// 执行查询
rs = stmt.executeQuery("select * from user");
// 处理结果
而现在我们日常使用的JDK1.8和MySQL8已经不需要我们执行加载数据库驱动了,删除Class.forName()
代码,发现执行结果依然正常:
// 删除Class.forName()
// 直接建立数据库连接
conn = DriverManager.getConnection(url, user, password);
// ...
这是因为从自从JDK1.5之后,采用了JDBC4
,不再需要我们手动的调用Class.forName()
加载数据库驱动,而是系统自动发现识别并注册数据库驱动。
这里其实是因为JDBC4
采用了SPI机制。
SPI(Service Provider Interface)是Java平台内置的一种服务提供发现机制,它允许应用程序动态地加载和使用第三方提供的服务实现,而无需在代码中显式引用这些实现类。
SPI机制的核心思想是解耦,即将接口和其具体实现分离,这种机制在模块化设计中非常重要,因为它提高了框架的扩展性和可维护性。在Java中,SPI机制通常在Classpath路径下的META_INF/services文件夹中实现,其中以接口的全限定名命名文件,文件内容为接口的实现类的全限定名。这些实现类可以通过ServiceLoader类加载并实例化,ServiceLoader使用迭代器模式来加载实现类。
在我们引入的数据库连接mysql-connector-java.jar
包中META_INF/services
目录下,存在一个以java.sql.Driver
命名的文件,java.sql.Driver
为Driver
接口的全限定类名,文件中定义的内容com.mysql.cj.jdbc.Driver
为对应MySQL数据库驱动实现类的全限定类名:
而在我们通过启动类加载器加载位于Java安装目录/jre/lib下的rt.jar
基础核心包中,有一个数据库驱动管理类DriverManager
,DriverManager
采用SPI机制来识别到mysql-connector-java.jar
包META-INF/services
目录下配置的驱动实现类,通过ServiceLoader
类加载并实例化MySQL的Driver
对象:
这里通过调用
Thread.currentThread().getContextClassLoader()
方法,获取线程上下文的类加载器,使用线程上下文类加载器完成对MySQL驱动的加载
public class DriverManager {
/**
* 加载初始JDBC驱动程序
*/
static {
loadInitialDrivers();
}
private static void loadInitialDrivers() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 使用ServiceLoader类加载并实例化Driver
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 迭代器遍历加载
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
}
return null;
}
});
}
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程上下文的类加载器ClassLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
}
那这里的拿到的线程上下文类加载器具体是什么,我们验证一下:
public static void main(String[] args) {
// 打印线程上下文类加载器
System.out.println(Thread.currentThread().getContextClassLoader());
}
控制台输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
是的,正是我们的应用程序类加载器AppClassLoader
。
简要回顾一下整体流程:
- 启动类加载器优先加载了
DriverManager
驱动管理类 DriverManager
通过SPI机制加载jar包中对应的MySQL
驱动- SPI机制通过线程上下文类加载器(
AppClassLoader
)完成加载MySQL
驱动
看到这,我们发现DriverManager
采用的SPI机制打破了双亲委派,其驱动的实现类由启动类加载器委托给应用程序类加载器去完成加载的。
但关于SPI机制是否真正打破了双亲委派,在网上似乎存在不同的理解,小豪在这里结合自己的看法,总结一下:
-
从类加载的结果来说:并没有违背双亲委派,针对这里的两个jar包,
rt.jar
包中DriverManager
依然由启动类加载器执行的加载,用户类路径classpath
下mysql-connector-java.jar
包中MySQL
驱动则正常通过应用程序类加载器完成加载。 -
从类加载的过程来说:确实是违背了双亲委派机制,因为在执行过程中抛弃双亲委派加载流程,启动类加载器
Bootstrap
向下委托给应用程序类加载器AppClassLoader
去完成加载的,逆向的委托了类加载器。
五、流程图
六、后记
本文从类加载器的介绍开始,引申到类加载器的双亲委派机制的源码解析,最后额外扩展了打破双亲委派的实现方法,经过本文,相信大家已经充分掌握了双亲委派机制。
简而言之,双亲委派机制是Java安全性和类隔离的基石,确保了类加载的顺序性和安全性,防止类的重复加载以及规避安全风险,在应用开发中,如无必要,我们不应该打破这一机制,应坚守系统可靠性的基本原则。
未来一段时间,小豪将会持续更新JVM相关知识体系,如果大家觉得内容还不错,可以先点点关注,共同进步~