Skip to content

JVM:Java 程序的运行系统

JVM(Java Virtual Machine)是 Java 程序从源码走向真实运行的核心执行环境。Java 源码不会直接交给操作系统运行,而是先被 javac 编译成 .class 字节码,再由 JVM 完成类加载、字节码校验、运行时内存分配、方法调用、即时编译和垃圾回收。

从工程角度看,JVM 同时承担三类职责:

  • 运行入口:把 Class 文件加载成 JVM 内部可识别的类型、方法、字段和常量。
  • 资源管理:管理堆、栈、方法区、直接内存、线程、锁和本地调用边界。
  • 执行优化:解释执行字节码,并把热点路径编译成机器码,同时用 GC 回收无用对象。

JVM 整体结构

理解 JVM 的关键,不是背参数,而是把“程序如何进入 JVM、数据放在哪里、代码如何执行、对象如何被回收、问题如何观测”这几条线连起来。可以先从 动手入门路径 跑通一个最小程序,再按 Class 文件、类加载、执行引擎、运行时内存和 GC 的顺序深入。

JVM 解决的问题

Java 最初强调“一次编写,到处运行”,这句话背后的工程条件是:业务代码不直接绑定某个操作系统和 CPU,而是先编译成一套稳定的中间格式。JVM 位于字节码和底层系统之间,向上承接 Java 语言和生态框架,向下连接操作系统、CPU、内存、线程、文件和网络。

问题JVM 的处理方式常见现象
不同系统如何运行同一套程序.class 字节码成为跨平台输入,由不同平台的 JVM 负责执行Windows、Linux、macOS 使用同一份 jar
类和方法什么时候可用类加载器读取字节码,经过验证、准备、解析、初始化后进入运行时ClassNotFoundExceptionNoSuchMethodError
对象和方法调用放在哪里堆保存对象,栈保存方法调用帧,方法区保存类元数据OOM、栈溢出、元空间增长
高频代码如何变快解释器先运行,JIT 把热点代码编译成机器码应用预热、CPU 抖动、性能曲线变化
对象不用了如何释放GC 从根对象出发判断可达性,再按回收器策略释放内存Young GC、Mixed GC、Full GC、长停顿

这套分层让 Java 框架能够把大量运行时能力交给 JVM:Spring 依赖类加载和反射,Netty 依赖直接内存和 NIO,动态代理依赖类生成和元空间,线上诊断依赖线程栈、堆 dump、GC 日志和 JFR。

从源码到运行

一个 Java 方法真正执行前,通常经过下面的链路:

text
.java 源码
  -> javac 编译
  -> .class 字节码
  -> ClassLoader 加载
  -> 链接与初始化
  -> 运行时数据区
  -> 解释执行 / JIT 编译
  -> OS / CPU 执行

源码与 Class 文件

javac 把源码编译为 Class 文件。Class 文件不是简单的文本翻译,而是一份结构化二进制协议,里面包含常量池、字段表、方法表、属性表和字节码指令。

bash
javac OrderService.java
javap -v OrderService
javap -c OrderService

javap -c 可以看到方法级字节码,javap -v 可以看到常量池、访问标记、方法描述符和属性。很多“明明编译过,运行却找不到方法”的问题,都和运行期 Class 文件版本、方法签名、依赖包覆盖有关。

字节码与栈式执行

JVM 字节码是一套偏栈式的指令模型。方法执行时会创建栈帧,栈帧里有局部变量表、操作数栈、动态链接和返回地址。比如一次整数相加,并不是直接写成 CPU 指令,而是先把变量压入操作数栈,再执行加法指令。

这种设计让 Class 文件更稳定,也让不同平台的 JVM 可以用自己的执行引擎实现解释执行、JIT 编译、逃逸分析、内联和锁优化。

类加载:类型如何进入 JVM

类加载不是简单“读文件”。HotSpot 里的类从字节码变成可运行类型,大致经过:

阶段做什么常见问题
加载根据类名找到字节码,生成 Class 元数据类路径错误、依赖缺失
验证校验字节码格式、安全约束和类型关系版本不兼容、非法字节码
准备为静态变量分配默认值静态字段初值认知错误
解析把符号引用转成直接引用方法、字段、类解析失败
初始化执行静态变量赋值和静态代码块初始化循环、静态块异常

类的唯一性由“类加载器 + 类全限定名”共同决定。同一个 com.example.User,如果被两个不同类加载器加载,在 JVM 看来就是两个不同类型。这是插件系统、Tomcat WebApp 隔离、Spring Boot DevTools 热重启、Dubbo/SPI 扩展加载里经常遇到的根因。

双亲委派模型的目的,是让核心类库优先由更上层的类加载器加载,避免业务代码伪造 java.lang.String 这类基础类型。实际工程中也会主动打破或调整委派顺序,例如应用服务器隔离不同 Web 应用,插件框架加载独立依赖,或者 OSGi 按模块声明可见性。

运行时数据区

运行时数据区回答“数据到底放在哪里”。不同区域的生命周期、线程关系和异常表现都不同。

区域线程关系主要内容常见异常或现象
线程共享对象实例、数组OutOfMemoryError: Java heap space、GC 频繁
方法区 / 元空间线程共享类元数据、方法信息、运行时常量池OutOfMemoryError: Metaspace、类加载器泄漏
虚拟机栈线程私有栈帧、局部变量表、操作数栈StackOverflowError、线程数过多
本地方法栈线程私有Native 方法调用栈native 崩溃、JNI 问题
程序计数器线程私有当前线程字节码执行位置很少单独排查
直接内存堆外NIO、Netty、mmap、JNI 数据Direct buffer memory、RSS 过高
Code Cache线程共享JIT 编译后的机器码编译受限、性能下降

排查内存问题时不能只看 -Xmx。容器内存、线程栈、直接内存、Metaspace、Code Cache 都会进入进程总内存。一个服务 -Xmx 设置得很克制,也可能因为直接内存或线程数过多被容器杀掉。

方法调用与对象布局

方法调用进入 JVM 栈,每一次调用都会形成栈帧。栈帧包含局部变量表和操作数栈,方法返回时栈帧弹出。递归过深、复杂表达式、过大的栈帧都可能触发栈溢出。

对象主要存在于堆里。HotSpot 对象通常由对象头、实例数据和对齐填充组成:

  • 对象头保存 Mark Word、类型指针等信息,和锁、哈希、GC 年龄有关。
  • 实例数据保存字段值。
  • 对齐填充让对象大小满足内存对齐要求。

这也是为什么“一个对象占多大内存”不能只看字段大小。对象头、引用压缩、字段排列、数组头、对齐都会影响实际占用。高并发缓存、批量对象创建、消息堆积时,这些细节会直接影响堆占用和 GC 压力。

GC:从引用链到回收器

GC 的第一步不是“扫堆”,而是判断哪些对象还活着。HotSpot 主要使用可达性分析:从 GC Roots 出发,沿引用链遍历对象;不可达对象才有机会被回收。

常见 GC Roots 包括:

  • 当前线程栈帧里的局部变量和参数。
  • 静态字段引用的对象。
  • JNI 引用。
  • 活跃线程对象。
  • 被锁持有的对象。
  • JVM 内部结构引用的对象。

不同回收器的差异,主要体现在回收区域、停顿控制、并发程度、碎片整理和吞吐取舍:

回收器核心定位适用方向
Serial简单、单线程、低额外开销小堆、工具程序
Parallel吞吐优先批处理、离线计算
CMS老年代并发标记清除老系统低延迟维护
G1分区回收、可预测停顿目标通用服务端
ZGC / Shenandoah大堆、低延迟、并发整理延迟敏感服务

GC 调优不应该从“套参数模板”开始,而应该先看业务目标:吞吐优先、延迟优先、内存成本优先,还是稳定性优先。不同目标会导向不同的堆大小、回收器、对象生命周期控制和监控指标。

常用参数与边界

参数含义注意点
-Xms / -Xmx初始堆 / 最大堆服务端常设为一致,减少运行期扩缩容抖动
-Xss单线程栈大小线程多时会显著影响进程内存
-XX:MaxMetaspaceSize元空间上限动态代理、热部署、脚本引擎会增加压力
-XX:MaxDirectMemorySize直接内存上限Netty、NIO、大文件传输需要关注
-XX:+UseG1GC使用 G1现代服务端常见选择
-XX:MaxGCPauseMillisG1 停顿目标是目标,不是硬保证,设置过低会增加 GC 压力
-Xlog:gc*GC 统一日志JDK 9+ 推荐使用

容器环境下还要关注:

  • JVM 是否识别 cgroup 限制。
  • 堆、直接内存、线程栈、Metaspace 与容器总内存之间是否留有余量。
  • InitialRAMPercentageMaxRAMPercentage 是否符合服务规模。
  • OOM 是 JVM 自己抛出,还是容器层面直接 kill。

观测与排查入口

JVM 问题需要先拿证据,再下结论。常用命令如下:

bash
# 查看 Java 进程
jps -l

# 查看启动参数和运行参数
jcmd <pid> VM.command_line
jcmd <pid> VM.flags

# 查看堆与类加载信息
jcmd <pid> GC.heap_info
jcmd <pid> VM.classloader_stats

# 导出线程栈和堆快照
jstack <pid> > thread-dump.txt
jcmd <pid> GC.heap_dump /tmp/app.hprof

# 周期性观察 GC 指标
jstat -gcutil <pid> 1000

JDK 9+ GC 日志建议:

bash
-Xlog:gc*:file=gc.log:time,uptime,level,tags

排查路径可以按现象拆分:

现象优先看什么可能根因
Java heap spaceheap dump、对象直方图、引用链缓存无界、集合累积、请求堆积
Metaspace类加载器统计、动态类数量热部署泄漏、代理类暴涨
Direct buffer memory直接内存上限、Netty buffer、RSSbuffer 未释放、堆外上限过小
StackOverflowError线程栈、递归深度、调用链递归、循环依赖、表达式过深
Full GC 频繁GC 日志、老年代占用、晋升速度老年代压力、泄漏、堆过小
CPU 高但 GC 不高线程栈、锁、热点方法、JFR死循环、锁竞争、序列化、JIT 预热
启动慢类加载、反射扫描、JIT、IO框架初始化、依赖扫描、冷启动

学习路线

JVM 内容可以按这条路径串起来:

  1. 动手入门路径:用一个最小 Java 程序观察编译、运行、GC 日志和诊断命令。
  2. Class 文件与字节码:理解常量池、方法描述符、Code 属性和字节码指令。
  3. 类加载机制:理解加载、链接、初始化、双亲委派、类隔离和类加载器泄漏。
  4. 执行引擎与 JIT:理解解释执行、热点探测、分层编译、内联、逃逸分析和 Code Cache。
  5. 运行时数据区:把堆、栈、方法区、直接内存、线程关系和参数边界分清。
  6. 对象生命周期:把对象分配、对象头、TLAB、晋升、引用链和 GC Roots 连起来。
  7. GC:从对象存活、STW、并行并发、分区回收和回收器选择入手。
  8. 诊断案例:用堆 OOM、Full GC、Metaspace、容器 OOM、CPU 高和线程过多串起排查闭环。
  9. 现代 JVM 发展:理解 G1 默认化、ZGC、容器感知、JFR、CDS、虚拟线程和 Native Image。

配套视频目录见 JVM 深入视频系列。视频适合按顺序建立整体链路;文字页面适合回查概念、参数和排查路径。

子页面

  • 动手入门路径:从 javacjavapjcmd、GC 日志开始建立第一条观察链路。
  • Class 文件与字节码:解释版本号、常量池、方法描述符、异常表和字节码增强。
  • 类加载机制:解释类加载生命周期、双亲委派、线程上下文类加载器和框架隔离。
  • 执行引擎与 JIT:解释解释器、JIT、热点代码、逃逸分析、Safepoint 和 Code Cache。
  • 运行时数据区:堆、栈、方法区、程序计数器、本地方法栈和直接内存。
  • 对象生命周期:对象从创建、引用、晋升到回收的完整路径。
  • GC:对象存活判断、STW、并行并发、主流垃圾回收器选型。
  • 诊断案例:常见线上问题的证据采集和判断路径。
  • 现代 JVM 发展:JDK 8 到 JDK 21 之后的运行时能力变化。
  • JNI:Java 与 native 能力的边界。
别急,先让缓存热一下。