当前位置: 首页 > news >正文

JVM 类加载机制深挖:双亲委派不是银弹

上次写 JVM 结构的时候,类加载器那部分写得很浅。

面试官追问了三个问题,我答得稀烂:

“双亲委派模型能保证什么?”
“SPI 机制是怎么打破双亲委派的?”
“Tomcat 的类加载器有什么特殊之处?”

当场社死。

回来老老实实翻了一遍《深入理解JVM虚拟机》,今天把我踩过的坑整理出来。


一、类加载到底是干嘛的?

在说双亲委派之前,先搞清楚类加载到底做了什么。

一句话:把 class 文件变成 Class 对象。

但这个过程分了三步,每步都有门道。

加载(Loading)

三件事:

  1. 通过类的全限定名找到 class 文件(可以是磁盘、可以是 jar、可以是网络)
  2. 读取字节流,转成方法区的运行时数据结构
  3. 在堆里生成一个java.lang.Class对象,作为程序访问方法区数据的入口

关键点:

我当初以为 Class 对象就是存在堆里的,这没问题。但方法区存的是什么?这个搞不清楚,JVM 调优时会很懵。

方法区存的是类的元信息:类名、父类、实现的接口、字段描述、方法字节码等。Class 对象只是个"入口指针"。

链接(Linking)

分三步:

验证(Verify)

这个阶段我之前完全忽略过。JVM 要校验:

  • 文件格式是否正确(魔数 0xCAFEBABE)
  • 字节码指令是否合法
  • 类型安全(父类是否是 final 的、字段方法是否存在)

为什么要验证?

因为 class 文件可以被篡改。恶意代码编译成字节码后,验证阶段会发现问题,直接拒绝加载。

准备(Prepare)

给静态变量分配内存,设置默认值。

publicclassUser{staticintage=18;// 这里 age = 0,不是 18staticStringname="Tom";// 这里 name = null,不是 "Tom"}

重点:准备阶段只赋默认值,不赋初始值。初始值在初始化阶段才赋。

我之前就搞混过,面试题问"静态变量的赋值时机",我答"类加载时就赋值",直接扣分。

解析(Resolve)

把符号引用替换成直接引用。

符号引用是什么?比如字节码里写着CONSTANT_Methodref #5,这个 #5 只是个编号,指向常量池里的方法描述。

解析阶段要做的是:把符号引用变成真正的内存地址(直接引用)。

初始化(Initialization)

这才是真正执行代码的地方。

  • 执行静态代码块
  • 给静态变量赋初始值(注意:这里的赋值和准备阶段的默认值是两回事)
publicclassUser{staticintage=18;// 准备阶段 age=0,初始化阶段 age=18staticStringname;static{name="Tom";// 静态代码块,初始化阶段执行}}

触发初始化的时机:

  1. new对象
  2. 调用静态方法
  3. 访问静态字段(注意:访问 final 修饰的静态字段不会触发,因为已经在常量池里了)
  4. 反射Class.forName()
  5. 子类加载时先触发父类

二、双亲委派模型

怎么实现的?

三层类加载器:

Bootstrap ClassLoader ← C++ 实现,加载 JDK_HOME/jre/lib ↑ Extension ClassLoader ← 加载 JDK_HOME/jre/lib/ext ↑ Application ClassLoader ← 加载 classpath(我们写的代码)

每个类加载器都有个 parent 引用,向上委托。

核心代码逻辑

protectedClass<?>loadClass(Stringname,booleanresolve){// 1. 先查缓存Class<?>c=findLoadedClass(name);if(c==null){try{// 2. 向上委托给父加载器if(parent!=null){c=parent.loadClass(name,false);}else{// 3. 父为空,说明到了 Bootstrap,从 Bootstrap 开始找c=findBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){// 父加载器找不到}if(c==null){// 4. 父加载器找不到,自己来加载c=findClass(name);}}returnc;}

双亲委派保证了什么?

面试被问到这个问题,我直接愣住。

保证了三件事:

1. 类的唯一性

同一个类被两个不同的类加载器加载过,那这两个 Class 对象是不同的。

因为 Class 对象的 identity 包括:类名 + 类加载器实例。

// 两个不同的类加载器ClassLoaderloader1=newCustomLoader();ClassLoaderloader2=newCustomLoader();// 同一个类被两个加载器加载Class<?>c1=loader1.loadClass("com.test.User");Class<?>c2=loader2.loadClass("com.test.User");// c1 != c2,这是两个不同的类型!

2. 核心类的安全性

你没法自定义一个java.lang.String来替换 JDK 的 String。

因为java.lang.String会先被 Bootstrap ClassLoader 加载,你自定义的String根本没机会。

3. 类的层次关系

同一个类被父加载器加载后,子加载器不需要再加载一次。


三、双亲委派的例外:SPI 机制

面试官追问:“那 SPI 机制是怎么打破双亲委派的?”

这个问题我当时完全没准备到。

什么是 SPI?

SPI = Service Provider Interface。

典型例子:JDBC。

JDBC 的驱动接口是 JDK 定义的(java.sql.Driver),但具体实现是 MySQL、PostgreSQL 这些厂商写的。

问题来了:

  • DriverManager(Bootstrap ClassLoader 加载的)
  • 驱动实现类(Application ClassLoader 加载的)

DriverManager 怎么找到 MySQL 驱动的?

答案是:Thread Context ClassLoader。

线程上下文加载器

每个线程都有一个类加载器引用,存在 Thread 类的contextClassLoader字段里。

JNDI 用它访问 SPI 服务,JDBC 用它加载驱动实现。

// JDBC 驱动加载的源码大概是这样的ServiceLoader<Driver>drivers=ServiceLoader.load(Driver.class);// ServiceLoader 内部会使用 Thread.currentThread().getContextClassLoader()// 来加载驱动实现类

为什么能打破双亲委派?

因为这段代码是 JDK 写的,它明确指定了用 contextClassLoader 来加载。

没有 contextClassLoader 的话,DriverManager 就只能用 Bootstrap 加载,而 Bootstrap 加载不了 MySQL 的驱动类(因为驱动类在 classpath 里,不在 jre/lib 里)。


四、我踩过的坑

坑1:Tomcat 的类加载器顺序

Tomcat 根本不是标准的双亲委派!

Tomcat 的加载顺序:

1. Bootstrap ClassLoader 2. System ClassLoader 3. /WEB-INF/classes 下的自定义类 ← 优先于 classpath 4. /WEB-INF/lib/*.jar 5. Common ClassLoader(Tomcat 全局)

为什么要打破双亲委派?

因为 Tomcat 要部署多个应用,每个应用可能依赖不同版本的同一个类(比如 Spring)。

如果用标准的双亲委派,Tomcat 上的所有应用都只能用同一份类。

Tomcat 的做法:先自己加载,自己加载不到再委托父加载器。

这叫"反向委托"或"子供委派"。

坑2:MyBatis 的 classpath 优先级

我之前遇到一个问题:

项目里有两个版本的 mybatis.jar,一个在 WEB-INF/lib,一个在 Tomcat 的 lib。结果运行时用的是 Tomcat 里的旧版本。

这其实就是类加载器优先级的问题。Tomcat 加载器先找到旧版本,就用了旧版本。

解决方案:

  • 搞清楚依赖的类加载器层级
  • 必要时把依赖放到 WEB-INF/lib(优先级更高)
  • 或者升级 Tomcat lib 里的版本

坑3:OSGI 和模块化

之前公司想上 OSGI 框架,遇到一个经典问题:

OSGI 的类加载机制是"网状"的,每个 bundle 可以单独加载、单独卸载。

好处是灵活,坏处是:

  • 类加载器之间的关系变得极其复杂
  • 不同 bundle 之间的类不能直接访问
  • 需要通过 OSGI 的服务注册机制来交互

最后项目没上 OSGI,改成了微服务。


五、面试高频问题

Q1:类加载的过程是什么?

加载 → 链接(验证→准备→解析)→ 初始化

每个阶段做什么要能说出来。

Q2:双亲委派模型是什么?有什么好处?

  • 三层类加载器,父委托子
  • 保证类的唯一性、核心类安全、层次关系

Q3:能破坏双亲委派吗?

能。有两种方式:

  1. 自定义类加载器,重写 loadClass():不向上委托,直接自己加载
  2. 线程上下文类加载器(SPI 机制):绕过父加载器,从当前线程的 contextClassLoader 加载

Q4:Tomcat 为什么要打破双亲委派?

  • 部署多个应用,每个应用需要隔离
  • 同一应用需要加载自己优先的类
  • 使用反向委托(子供委派)

Q5:如何实现类的热加载?

// 自定义类加载器publicclassHotSwapClassLoaderextendsURLClassLoader{publicClass<?>loadClass(Stringname,booleanresolve)throwsClassNotFoundException{// 每次都重新加载,不走缓存returnfindClass(name);}}

关键点:每次 new 一个新的 ClassLoader 实例,旧的对象没有引用后就会被 GC,Class 对象也会被卸载。


六、记忆口诀

类加载三步: 加载 → 链接(验证、准备、解析)→ 初始化 准备赋默认值,初始化赋初始值 双亲委派: Bootstrap → Extension → Application 保证唯一性、安全性、层次性 打破双亲委派: 1. 重写 loadClass 2. 线程上下文类加载器(SPI) Tomcat 打破: 子供委派,自己先加载 类加载器优先级: 子类 > 父类(同级别下) Tomcat: WEB-INF/classes > WEB-INF/lib > classpath

写在最后

类加载器这块,我踩过的坑比 JVM 其他部分加起来都多。

核心原因:类加载器是 JVM 和应用代码的桥梁,涉及到依赖管理、模块化、容器隔离这些实战问题。

光背概念没用。

建议你自己实现一个自定义类加载器,加载一个 class 文件,体会一下整个流程。

面试问到这块,你如果说"我自己实现过一个热加载的类加载器",面试官基本不会再追问了。

有些坑,只有自己踩过才知道有多深。

http://www.jsqmd.com/news/684933/

相关文章:

  • WebRPA教程:零代码实现浏览器网页自动化、爬虫与桌面自动化神器 打造自己的AI浏览器!轻松实现浏览器自动点击 自动处理数据 网络抓包 表格数据提取等复杂功能
  • 10分钟精通暗黑破坏神2存档编辑:d2s-editor零基础配置技巧
  • 2026留学生回国找工作靠谱机构名录盘点 - 优质品牌商家
  • 如何减小音频文件体积?盘点5个MP3压缩瘦身方法!
  • 向量搜索误召回率高达38%?EF Core 10中Normalize预处理缺失、余弦阈值漂移、HNSW参数过拟合三重危机预警
  • Blazor + WASI + .NET AOT三重编译链曝光:2026边缘计算场景下首例亚毫秒级首屏加载实录
  • 从零构建BQ4050 SMBus通信:STM32 IO模拟时序实战解析
  • 大语言模型推理加速:SPEQ量化与推测式解码技术解析
  • DPI-每英寸点数
  • 软件知识管理中的专家网络建设
  • 如何优化大量DML时的段空间分配_FREELISTS与ASSM的并发性能
  • Python类型注解与mypy静态检查
  • AI 智能体的标准开发流程
  • TRAE如何节省token额度教程(一)|理解Token与上下文窗口 token消耗快怎么办?
  • TTP229触摸模块的三种工作模式详解:单键、多键、分组模式到底怎么选?
  • 中国词元:构建自主AI生态的新范式
  • SOCD Cleaner深度解析:如何用键盘映射革命性解决游戏输入冲突
  • 服务定位器管理化技术依赖查找与缓存
  • 用Python的tkinter写个汉字转机内码小工具,附完整源码和打包教程
  • 天赐范式第19天:拒绝 NaN!12 算子硬刚黑洞奇点|2.44% 误差复现诺奖黑洞质量(附源码)
  • LightGBM算法原理与工程实践指南
  • Agent智能体开发秘籍:从Prompt工程到自主决策的4阶段进阶路线!
  • Keil5编译报错找不到ARM编译器V5?手把手教你下载安装AC5.06并配置到MDK
  • 如何在有/无备份的情况下从图库中恢复永久删除的照片
  • 告别手动拼接地址:在Go微服务中优雅集成gRPC与Consul服务发现的两种姿势
  • 无法生成:天津照片直播排行内容缺乏核心数据支撑 - 优质品牌商家
  • 开源中国双核战略:打造AI普惠时代的“云边范式
  • 中小企业网络推广效果提升:GEO关键词优化、GEO推广优化、GEO精准优化、文小言优化、百度AI优化、豆包优化选择指南 - 优质品牌商家
  • 不止是监控:用树莓派+MJPG-Streamer打造智能家居中枢,联动Home Assistant和移动通知
  • 如何在没有备份的情况下在iPhone上检索已删除的联系人