知

模块三 · 并发与 JVM

第 6 / 11 节20 分钟阅读型

JVM 三层理解:内存布局、GC、可观测性

JVM 不是"背参数",而是理解对象在哪、什么时候被回收、出问题怎么查。这一节按内存、回收、类加载、排查四层讲。

读完这一节,你能做到

  1. 说清 JVM 内存区域的划分与各自存放什么。
  2. 解释分代回收与常见回收器的取舍。
  3. 说清双亲委派与打破它的场景。
  4. 用工具定位 OOM、CPU 飙高与频繁 GC。

一、内存布局

区域存放线程是否会 OOM
程序计数器当前字节码行号私有否
虚拟机栈栈帧、局部变量私有StackOverflowError
本地方法栈native 调用私有是
堆对象实例、数组共享OutOfMemoryError: Java heap space
方法区/元空间类元信息、常量、静态变量共享Metaspace
直接内存NIO DirectByteBuffer共享Direct buffer memory

JDK 8 用元空间替代永久代,元空间在本地内存,默认不设上限(受物理内存限制),需要显式 -XX:MaxMetaspaceSize。

栈帧包含局部变量表、操作数栈、动态链接、返回地址。递归过深会 StackOverflow;栈大小用 -Xss 调整。

二、对象与分配

对象优先在 Eden 分配;大对象直接进老年代(-XX:PretenureSizeThreshold);长期存活的对象晋升老年代(年龄阈值默认 15)。

两个优化:

  • TLAB:每个线程在 Eden 里有一块私有分配缓冲,减少同步。
  • 逃逸分析:对象不会逃出方法时,可能栈上分配或标量替换,减少堆压力。

对象的内存布局:对象头(Mark Word + 类型指针)、实例数据、对齐填充。Mark Word 存哈希、GC 年龄、锁状态。

三、GC:分代与回收器

分代假设:大多数对象朝生夕死,少数长期存活。于是把堆分为年轻代(Eden + Survivor)和老年代,分别用不同算法。

回收器特点适用
Serial单线程,STW小堆、客户端
Parallel多线程吞吐优先批处理
CMS并发标记清除,已废弃低延迟旧系统
G1分区,可预测停顿大堆、通用
ZGC染色指针,超低停顿超大堆、低延迟

G1 把堆分成大小相等的 Region,按回收价值排序,优先回收垃圾最多的区域(Garbage First)。ZGC 的停顿与堆大小基本无关。

GC 的核心动作:可达性分析 → 标记 → 清除/复制/整理。判断对象存活用可达性分析,不用引用计数(解决循环引用)。

四、类加载与双亲委派

加载 → 验证 → 准备 → 解析 → 初始化

双亲委派:类加载请求先交给父加载器,父加载器加载不了才自己加载。好处是核心类不会被篡改,避免重复加载。

打破双亲委派的三个场景:

  • SPI:JDBC 驱动由线程上下文类加载器加载。
  • Tomcat:每个 Web 应用独立类加载器,实现隔离。
  • 热部署:自定义类加载器重新加载类。

五、可观测性与排查

工具用途
jps查看 Java 进程
jstack线程栈、死锁
jmap堆快照、对象统计
jstatGC 频率与耗时
Arthas在线诊断、方法耗时
MAT分析堆转储,找泄漏根

三类高频问题的排查路径:

  • CPU 飙高:jstack 找到最忙线程,看栈顶方法;可能是死循环、频繁 GC、锁竞争。
  • OOM:先看是哪种 OOM(堆、元空间、直接内存),再用 jmap dump + MAT 找大对象。
  • 频繁 Full GC:jstat 看频率,检查大对象、缓存无界、元空间增长。

六、常用参数

-Xms4g -Xmx4g            # 堆初始与最大,建议相等
-Xmn2g                   # 年轻代大小
-Xss512k                 # 线程栈
-XX:MetaspaceSize=256m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError

参数不是越多越好:先加 GC 日志与 OOM dump,再根据数据调。

七、常见坑

  1. 把 -Xmn 设得过大,导致老年代过小、频繁 Full GC。
  2. 认为元空间不会 OOM。
  3. 用 System.gc() 触发回收,造成不必要的 STW。
  4. 缓存无上限,堆被撑满。
  5. 只看 GC 次数,不看停顿时间与吞吐。

八、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 用 jstat -gcutil 观察一次压测的 GC 变化。
  2. 构造一个 OOM,开启 HeapDump,用 MAT 找占用最大的对象。
  3. 用 Arthas 的 trace 看一个方法的耗时分布。

② 让 AI 写同一题

把第 2 题(线上内存排查)交给 AI,补一句 给出从告警到定位的完整命令链。

③ 对比要盯的点

  • 它只讲堆,忘了堆外(DirectByteBuffer、元空间)吗?
  • jmap、jstack、jcmd 的用途它区分清楚了吗?
  • 它给的 GC 参数是当前 JDK 默认值吗?还是抄了 JDK 8 的老配置。

④ 一句话取舍

按它的排查步骤走,哪一步会先因为“没开参数”而卡住?

九、自测

  1. JVM 内存区域各自存放什么?
  2. 对象什么时候晋升老年代?
  3. G1 与 CMS 的核心差异?
  4. 双亲委派解决了什么问题?
  5. CPU 飙高怎么排查?

参考答案要点:堆/栈/元空间/直接内存;年龄与大小阈值;分区与可预测停顿;核心类安全与重复加载;jstack 找最忙线程。

附录:高频追问

问:为什么用可达性分析而不是引用计数? 引用计数无法处理循环引用,且维护计数有开销。

问:Minor GC 与 Full GC 的区别? 前者只回收年轻代,快;后者回收整个堆与方法区,慢,通常伴随较长 STW。

问:什么时候触发 Full GC? 老年代空间不足、元空间不足、显式 System.gc、晋升失败、并发模式失败。

问:G1 一定比 CMS 好吗? 不一定。G1 适合大堆与可预测停顿;小堆或旧版本上 CMS 可能更稳,但 CMS 已废弃。

问:什么是 STW? Stop The World,GC 时暂停应用线程;停顿越长,接口抖动越明显。

问:类初始化什么时候触发? new、访问静态字段/方法、反射、子类初始化、主类启动。

问:什么情况下会栈溢出? 递归过深、栈帧过大;调大 -Xss 只是缓解,根本要改算法。

问:直接内存会 OOM 吗? 会,报 Direct buffer memory;NIO 大量使用堆外内存时要限制上限。

问:如何判断内存泄漏还是内存不足? 泄漏是 GC 后仍持续增长;不足是峰值超过堆上限,扩容或优化对象数量。

问:为什么建议 Xms 与 Xmx 相等? 避免运行期扩缩容带来的抖动与额外 GC。

OOM 的几种类型

类型含义常见原因
Java heap space堆不足大对象、缓存无界
Metaspace元空间不足动态生成类、热部署
Direct buffer memory堆外内存不足NIO 未释放
unable to create new native thread线程数超限线程泄漏

先看错误信息定位类型,再 dump 分析,不要一上来就加堆。

类加载器的层级

Bootstrap(核心类)→ Platform/Ext(平台类)→ Application(应用类)→ 自定义。双亲委派从下往上问,加载不了再自己加载。

GC 日志看什么

看三个数:Young GC 频率、Full GC 次数、单次停顿。Young 频繁说明分配率高;Full 频繁说明晋升过快或老年代不足;停顿长说明堆大或回收器不合适。

补遗:解释执行、JIT 与逃逸分析

JVM 先解释执行字节码,热点代码由 JIT 编译成本地代码:C1(编译快、优化少)、C2(编译慢、优化多),分层编译兼顾启动与峰值。

常见优化:方法内联、逃逸分析(栈上分配/标量替换/锁消除)、循环展开。-Xint 强制解释、-Xcomp 强制编译,只用于排查。

注意“微基准陷阱”:预热不足、死代码消除、常量折叠会误导结论,压测用 JMH。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解