JVM类加载深入理解

上一篇我们做了JVM类加载机制的零基础入门,搞懂了基础加载体系、双亲委派、沙箱机制和类与对象的关系。

今天我们直接进阶深挖 ,彻底吃透类加载的高阶核心能力


一、快速梳理:JDK8 类加载机制

先做深度复盘,区别于入门篇,今天我们重点区分:类加载器层级体系 + 类加载器实现类细节,逐个拆解原理、特点、优缺点。

先用三句话总结JDK8的类加载机制:

  1. 类缓存:每个类加载器对他加载过的类都有一个缓存。

  2. 双亲委派:向上委托查找,向下委托加载。

  3. 沙箱保护机制:不允许应用程序加载JDK内部的系统类。

JDK8中加载体系分类以下两种:

左侧是JDK中实现的类加载器,通过parent属性形成父子关系。应用中自定义的类加载器的parent都是AppClassLoader

右侧是JDK中的类加载器实现类。通过类继承的机制形成体系。未来我们就可以通过继承相关的类实现自定义类加载器。

下面进行详细介绍

1.1 JDK8 四大类加载器(层级+实现类深度解析)

很多人分不清:加载器是什么、底层谁实现的、各自优缺点是什么,这里一次性讲透。

1、启动类加载器(Bootstrap ClassLoader)

实现原理 :由 C++ 语言编写,JVM 底层原生实现,无 Java 实例,在 Java 层获取结果为 null。

负责加载jre/lib 下核心 rt.jar、charsets.jar 等系统底层核心类。

特点

  • 权限最高、优先级最高

  • JVM 启动第一时间加载,速度最快

  • 不继承 ClassLoader 父类,是独立底层实现

优缺点

  • 优点:底层稳定、速度快、安全性极高

  • 缺点:完全封闭,开发者无法干预、无法扩展

2、扩展类加载器(Extension ClassLoader)

实现类sun.misc.Launcher$ExtClassLoader

原理:纯 Java 实现,继承自 ClassLoader,遵循双亲委派,父加载器为 Bootstrap。

负责加载:JDK 扩展目录下的拓展工具类、系统辅助Jar包。

特点

  • 层级第二层,承接系统核心与业务代码

  • 只加载系统拓展类,不加载用户业务代码

优缺点

  • 优点:拓展系统能力、隔离系统与业务类

  • 缺点:使用场景极少,日常开发几乎感知不到

3、应用类加载器(Application ClassLoader)

实现类sun.misc.Launcher$AppClassLoader

原理 :Java实现,双亲委派,父加载器为扩展类加载器,我们项目默认加载器

负责加载:项目自定义类、Maven依赖Jar、classpath下所有业务代码。

特点:使用频率最高、承载所有业务代码加载。

优缺点

  • 优点:通用性强、适配所有常规业务项目、稳定无bug

  • 缺点:灵活性差、不支持热加载、不支持多版本类共存、无法加载外部动态Jar

4、自定义类加载器(Custom ClassLoader)

实现原理 :开发者继承 ClassLoader 重写加载方法,自主控制加载逻辑。

特点:完全自定义、自由度最高。

优缺点

  • 优点:支持热部署、代码加密、动态加载Jar、多版本类共存

  • 缺点:需要手动处理双亲委派、容易出现类冲突、实现有成本

JDK8中的类加载器都继承于一个统一的抽象类ClassLoader,类加载的核心也在这个父类中。其中,加载类的核心方法如下:

复制代码
//类加载器的核心方法
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
	synchronized (getClassLoadingLock(name)) {
		// 每个类加载起对他加载过的类都有一个缓存,先去缓存中查看有没有加载过
		Class<?> c = findLoadedClass(name);
		if (c == null) {
			//没有加载过,就走双亲委派,找父类加载器进行加载。
			long t0 = System.nanoTime();
			try {
				if (parent != null) {
					c = parent.loadClass(name, false);
				} else {
					c = findBootstrapClassOrNull(name);
				}
			} catch (ClassNotFoundException e) {
			}

			if (c == null) {
			long t1 = System.nanoTime();
			// 父类加载起没有加载过,就自行解析class文件加载。
			c = findClass(name);

			sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
			sun.misc.PerfCounter.getFindClasses().increment(); }
		}
		//这一段就是加载过程中的链接Linking部分,分为验证、准备,解析三个部分。
		// 运行时加载类,默认是无法进行链接步骤的。
		if (resolve) {
			resolveClass(c);
		}
		return c;
	}
}

这个方法就是最为核心的双亲委派机制。并且这个方法是protected声明的,这意味着,这个方法是可以被子类覆盖的。所以,双亲委派机制也是可以被打破的。

当一个类加载器要加载一个类时,整体的过程就是通过双亲委派机制向上委托查找,如果没有查找到,就向下委托加载。整个过程整理如下图:


1.2 沙箱保护机制(核心源码级理解)

双亲委派机制有一个最大的作用就是要保护JDK内部的核心类不会被应用覆盖。而为了保护JDK内部的核心类, JAVA在双亲委派的基础上,还加了一层保险。入门我们只懂概念,进阶我们直接看底层源码逻辑,搞懂沙箱到底怎么防篡改。

1.2.1 沙箱保护核心源码原理

沙箱安全本质:依靠双亲委派 + JVM 类加载校验,禁止用户篡改系统核心包类

核心源码位于 ClassLoader.loadClass() 核心逻辑:

复制代码
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
	if (!checkName(name))
		throw new NoClassDefFoundError("IllegalName: " + name);
	// 不允许加载核心类
	if ((name != null) && name.startsWith("java.")) { throw new SecurityException
	("Prohibited package name: " +
		name.substring(0, name.lastIndexOf( '. ')));
	}
	if (pd == null) {
		pd = defaultDomain;
	}
	if (name != null) checkCerts(name, pd.getCodeSource());
	return pd;
}

1、优先向上委派,让启动类加载器加载java.langjava.util 等核心类;

2、如果用户自定义 java.lang.String,委派到顶层加载器后,系统已经存在该类

3、JVM 校验发现:系统核心包不允许用户自定义加载,直接抛出安全异常,拒绝加载用户自定义类。

源码核心结论

沙箱不是独立机制,是双亲委派 + 系统包校验共同实现的安全能力,杜绝恶意覆盖JDK核心类,保证底层运行安全。


二、Linking 类链接过程(完整阶段)

很多人只知道类加载分「加载、链接、初始化」,但完全不懂链接做了什么。

**链接(Linking)**是加载之后、初始化之前的核心步骤,分为三步:

2.1 验证(Verify)

校验 class 文件合法性:格式校验、字节码校验、权限校验、系统安全校验。

作用:防止恶意、损坏、不规范的字节码破坏JVM运行。

2.2 准备(Prepare)

给类的静态变量分配内存、赋默认初始值(int=0、boolean=false、引用=null)。

注意:这一步不会赋值自定义初始值,自定义赋值在初始化阶段执行。

2.3 解析(Resolve)

把代码中的符号引用 (字符串形式的类名、方法名),替换成直接内存地址引用

如果A类中有一个静态属性,引用了另一个B类。那么在对类进行初始化的过程中,因为A和B这两个类都没有初始化,JVM并不知道A和B这两个类的具体地址。所以这时,在A类中,只能创建一个不知道具体地址的引用,指向B类。这个引用就称为符号引用 。而当A类和B类都完成初始化后, JVM自然就需要将这个符号引用转而指向B类具体的内存地址,这个引用就称为直接引用

通俗总结

其中关于半初始化状态就是JDK在处理一个类的static静态属性时,会先给这个属性分配一个默认值,作用是占住内存。然后等连接过程完成后,在后面的初始化阶段,再将静态属性从默认值修改为指定的初始值。


三、类加载器加载外部Jar:优缺点与适用场景

常规项目只能加载classpath内的Jar,而自定义类加载器可以动态加载磁盘任意外部Jar包

3.1 实现原理

通过 URLClassLoader 指定外部Jar路径,动态读取、加载外部class字节码,无需重启项目。

3.2 核心优点

  • 无需重启服务,动态引入新Jar、新功能

  • 实现插件化架构,功能可插拔

  • 隔离外部Jar与项目内部类,避免版本冲突

3.3 缺点

  • 容易出现类重复加载、内存泄漏

  • 需要手动管理Jar资源释放

  • 调试难度大,线上问题排查复杂

3.4 适用场景

  • 插件化系统(IDE插件、后台功能插件)

  • 动态扩展功能、无需重启更新

  • 多版本Jar共存、解决依赖冲突


四、自定义类加载器实现Class代码混淆(加密与落地)

企业级常用防护手段:防止class反编译、防止源码泄露、保护核心业务代码

4.1 Class文件加密常用方法

1、简单异或加密(最常用、轻量)

对class字节码数组进行固定密钥异或运算,修改原始字节码,普通反编译工具直接乱码,无法查看源码。

2、AES对称加密(高强度)

对class文件整体AES加密,安全性极高,适合核心机密业务代码。

3、字节码乱序、冗余插入(混淆加固)

打乱字节码指令、插入无效指令,增大反编译阅读难度。

4.2 自定义加载器解密执行原理

常规加载器读取加密class文件会报错,自定义加载器可以:

  1. 读取加密字节码

  2. 内存中实时解密还原正常class字节码

  3. 交给JVM正常加载运行

关键点 :磁盘上永远是加密文件,内存中临时解密,外部无法获取源码

4.3 项目应用场景

  • 商业项目、付费源码保护

  • 核心算法、核心业务逻辑防泄露

  • 私有化部署项目,防止客户反编译篡改代码

  • 规则引擎、统一审批规则、订单状态规则等


五、自定义类加载器实现热加载

热加载 = 代码修改无需重启服务,实时生效,底层完全依赖自定义类加载器。

5.1 热加载核心原理

JVM 判定类唯一标准:全类名 + 加载器实例

同一个类名,更换新的自定义加载器实例加载,就会生成全新的Class对象,实现热替换。

5.2 热加载常用方案

1、自定义类加载器重载(原生方案)

监测class文件变动,销毁旧加载器、新建加载器重新加载类,轻量高效。

2、Spring Boot DevTools

底层就是自定义类加载器实现的快速重启、热加载能力。

3、JRebel 热加载插件

商用级热部署,基于类加载器动态替换,支持几乎所有代码热更新。

5.3 应用场景

  • 开发环境提速,不用频繁重启项目

  • 线上紧急BUG热修复、不停机更新

  • 动态业务规则更新、配置类动态刷新


六、打破双亲委派:实现同类多版本共存

双亲委派是默认规则,但很多中间件、容器必须打破它。

6.1 打破双亲委派的核心方法

双亲委派的核心逻辑在 loadClass() 方法,想要打破,只需重写loadClass方法

默认流程:先委派父加载器 → 父加载不到自己加载

打破后流程:优先自己加载,再委派父加载器,彻底反转加载顺序。

6.2 核心作用:同类多版本共存

不同自定义加载器,可以加载全类名完全一致、版本不同的类,实现多版本共存。

6.3 经典落地场景:Tomcat 类加载体系

类加载体系如下图:

tomcat的几个主要类加载器:

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本;
  • Jsp类加载器:针对每个JSP页面创建一个加载器。这个加载器比较轻量级,所以Tomcat还实现了热加载,也就是JSP只要修改了,就创建一个新的加载器,从而实现了JSP页面的热更新。

Tomcat 是打破双亲委派的典型代表:

  • Tomcat 自定义 WebappClassLoader

  • 优先加载当前项目Web应用的类,再委派父加载器

  • 实现不同Web项目,依赖不同版本Jar、同名类互不冲突

通俗解释:一台Tomcat部署多个项目,A项目spring5、B项目spring6,互不冲突,就是靠打破双亲委派实现的。

6.4 适用场景总结

  • web容器多项目隔离

  • 中间件多版本依赖兼容

  • 插件化架构多版本共存

  • 热部署、动态替换类场景


七、类加载器能不能不用反射?SPI扩展机制落地

很多人以为动态加载类必须用反射,其实 SPI 机制可以替代反射,实现无侵入扩展

7.1 核心问题解答:类加载可以不用反射吗?

可以

反射是「运行时主动获取类信息、调用方法」;

SPI 是基于类加载器的自动发现机制,无需手写反射代码

7.2 SPI 扩展机制原理

SPI(服务提供者接口):JDK自带的服务发现机制。

核心逻辑:

  1. 定义统一接口规范

  2. 在固定配置文件中写入接口实现类全限定名

  3. 通过 ServiceLoader 利用类加载器自动加载所有实现类

全程无需反射代码,自动实例化、自动发现扩展实现

7.3 SPI 应用场景

  • JDBC 驱动自动加载(经典SPI落地)

  • 框架扩展点、自定义插件实现

  • 中间件扩展、策略模式自动注入

  • 解耦接口与实现,无需硬编码反射

7.4 核心总结

类加载 + SPI = 优雅替代反射,实现解耦、可扩展、插件化架构,是各大框架底层核心设计思想。


八、全文串联总结

  1. 四大类加载器各有优劣,Bootstrap安全底层、AppClassLoader通用但死板、自定义加载器灵活强大。

  2. 沙箱机制基于双亲委派+系统校验,从源码层面杜绝核心类被篡改。

  3. 链接过程负责校验、静态内存分配、符号引用解析,是类初始化的前置关键步骤。

  4. 自定义加载器可动态加载外部Jar,适配插件化架构,但需注意内存泄漏。

  5. Class加密混淆依靠自定义加载器内存解密,实现源码保护、防泄露。

  6. 热加载依靠更换类加载器实例,实现类动态替换、无需重启服务。

  7. 重写loadClass可打破双亲委派,支撑Tomcat多项目多版本类共存。

  8. SPI机制依托类加载器实现服务自动发现,优雅规避反射硬编码,实现高度扩展。

相关推荐
程序员二叉1 小时前
【JVM】OOM详解+JVM参数+FullGC排查+CPU飙高+死锁+内存泄漏+命令大全
java·开发语言·jvm·面试
不知名的老吴2 小时前
线程的生命周期之线程同步
java·开发语言·jvm
cfm_29145 小时前
JVM执行引擎初步了解
jvm
骑士雄师6 小时前
18.3 LangGraph 长期存储案例
jvm
未若君雅裁7 小时前
JVM 垃圾回收算法与分代回收机制
java·jvm·算法
未若君雅裁7 小时前
JVM 垃圾回收器全景与G1深度解析
java·开发语言·jvm
hereitis贝壳8 小时前
GC.lsp:AutoCAD 中实用的轻量化公差标注插件
jvm·里氏替换原则
未若君雅裁8 小时前
JVM 调优与线上排障:参数工具内存泄漏和CPU飙高
jvm
cfm_29149 小时前
JVM类加载机制初步了解
java·jvm