JVM 类加载和字节码听起来偏底层,但很多线上和本地问题都绕不开它:为什么本地启动找不到某个 jar,为什么同名类加载的是另一个版本,为什么反射能绕过泛型检查,为什么 javap 能看出方法调用指令。
这篇先从工程排查角度梳理:类加载器层级、双亲委派、classpath 问题和字节码观察方法。
JVM 里常见的类加载器
JVM 标准层面常见三个内置加载器:
- Bootstrap ClassLoader:加载 JDK 核心类库,通常由 JVM 底层实现。
- Extension ClassLoader:老版本里加载扩展目录。
- Application ClassLoader:加载应用 classpath 下的类。
需要注意的是,所谓“父加载器”不是 Java 继承关系里的父类。它更多是类加载器对象之间的委托关系。
比如 Application ClassLoader 会把加载请求先交给父加载器,父加载器加载不到时,自己才尝试加载应用 classpath 下的类。
双亲委派解决什么问题
双亲委派的基本过程是:
- 当前类加载器收到加载请求。
- 先检查这个类是否已经加载过。
- 没加载过就委托父加载器。
- 父加载器能加载就直接返回。
- 父加载器加载不到,当前加载器再自己尝试。
这样做有两个好处。
第一,避免核心类被随便覆盖。比如你自己写一个 java.lang.String,正常情况下不会替代 JDK 里的 String。
第二,保证类加载的一致性。核心类由更上层加载器统一加载,不会在不同地方出现一堆互不兼容的版本。
当然,双亲委派也不是所有场景都绝对遵守。应用服务器、插件系统、热加载、SPI、OSGi 等场景都会出现更复杂的类加载策略。但理解默认模型,是排查问题的第一步。
classpath 问题为什么常见
Java 程序能不能找到类,关键看 classpath。IDE、本地命令、Maven、Spring Boot 打包、容器运行时,classpath 组织方式都可能不同。
一个典型问题是:同一份代码,部分同事本地能启动,部分同事提示某个 SDK jar 找不到。最后发现不是代码问题,而是 IDE 的命令行缩短方式不同。
当依赖很多时,启动命令会非常长。IDE 可能用几种方式缩短 classpath:
- 直接不缩短,命令过长时交给操作系统限制。
- 写入临时 classpath 文件。
- 写入临时 jar 的 manifest。
不同方式会影响某些 SDK 或自定义类加载逻辑是否能正确读取所有依赖。遇到“别人能跑我不能跑”的类加载问题,不要只删缓存,也要看启动命令和 classpath 展开方式。
排查类加载问题看什么
可以按这个顺序排查:
- 报错是
ClassNotFoundException还是NoClassDefFoundError。 - 缺的是编译期类、运行期类还是某个传递依赖。
mvn dependency:tree里是否存在对应依赖。- jar 是否真的进入最终包。
- 启动命令 classpath 里是否包含它。
- 是否有多个版本的同名类。
- 是否存在自定义 ClassLoader。
ClassNotFoundException 更像主动加载时找不到类。NoClassDefFoundError 常见于编译时有、运行时缺,或者类初始化失败后再次使用。
字节码能帮我们看清 Java 语法背后做了什么
Java 源码最终会编译成字节码。用 javap -v 可以查看 class 文件里的常量池、方法描述符、指令和属性。
比如 JVM 是基于栈的架构,很多指令会把值压入操作数栈,再执行计算或调用。iadd、ladd、fadd、dadd 分别对应不同基本类型的加法。方法调用也有不同指令,比如 invokestatic、invokevirtual、invokespecial、invokeinterface。
日常开发不需要天天读字节码,但在这些场景里很有用:
- 理解重载和重写的调用差异。
- 看 lambda、内部类、泛型擦除的编译结果。
- 排查代理、增强、AOP、Java Agent。
- 确认某个编译器或插件到底生成了什么。
Java Agent 和字节码增强
Java Agent 可以在类加载前后对字节码做增强。很多监控、链路追踪、性能分析工具都依赖类似能力。
从工程角度看,字节码增强要特别注意:
- 增强顺序。
- 类加载时机。
- 多个 Agent 之间是否冲突。
- 对启动时间和运行性能的影响。
- 增强失败后的降级策略。
如果一个问题只在线上启动参数带 Agent 时出现,本地普通启动复现不了,就要把 Agent、类加载器和字节码增强纳入排查范围。
JMM 和类加载不是一类问题,但都属于 JVM 基础
源 note 里也记录了 Java 内存模型。JMM 主要解释线程之间如何通过主内存、工作内存、volatile、锁和 happens-before 保证可见性与有序性。它和类加载不是同一个主题,但都属于理解 JVM 行为的基础。
简单说:类加载解决“类从哪里来、怎么被加载”,JMM 解决“多线程下读写变量怎么可见、怎么有序”。排查并发问题时,不要把类加载和 JMM 混在一起。
一句话收束
JVM 类加载不是背几个加载器名字,而是要能回答:这个类是谁加载的、从哪个 classpath 来、为什么不是另一个版本、运行时为什么找不到。
字节码也不是为了炫技,而是在源码解释不清时,给你一个更接近 JVM 执行层的观察窗口。把类加载、classpath 和 javap 这三件事串起来,很多 Java 排障会少走不少弯路。




