下载工作台
Java 编程实战

JVM:内存、GC 与性能调优

试读上半部分 · 解锁后可读全文

第 16 章 · JVM:内存、GC 与性能调优

本章目标:画出 JVM 运行时内存结构并说清各区域存什么;理解对象生命周期GC Roots 可达性分析;掌握标记-清除/复制/标记-整理三大算法与分代假说;能为服务选型 Parallel / G1 / ZGC 收集器;读懂 GC 日志;会用 jps/jstat/jmap/jstack/JFR/Arthas 工具链;完成一次 OOM 排查演练;了解双亲委派类加载;树立「先测量、再调优」的方法论;对照 python-dev 的引用计数与分代回收。

学时建议:5~6 小时(含 2 小时实验)

前置:完成 ch01 JVM 简述ch10 多线程ch15 并发进阶


16.1 场景说明:凌晨两点的告警

toolkit-demo 报表服务上线三个月,凌晨批量任务时段监控告警:CPU 飙到 90%、接口 P99 从 50ms 涨到 2s,最后 OutOfMemoryError: Java heap space 进程退出。不会 JVM 的工程师只能重启;会 JVM 的工程师能回答三个问题:

① 内存涨到哪里去了?(看堆、看 Dump)
② GC 为什么这么频繁?(看日志、看回收器)
③ 参数该不该调,调哪个?(先测量再动手)

本章把这三个问题逐项落实为可操作的技能。

说明:本章实验均在本地 ~/learn-java 进行,压测数值为教学虚构示意

16.2 JVM 运行时内存结构

┌──────────────────────── JVM 进程 ────────────────────────┐
│  堆 Heap(线程共享,GC 主战场)                            │
│    ├─ 年轻代:Eden + Survivor0 + Survivor1                │
│    └─ 老年代:长期存活对象                                 │
│  元空间 Metaspace(类元数据,本地内存)                    │
│  虚拟机栈(每线程一条,栈帧=方法调用:局部变量/操作数栈)   │
│  本地方法栈(native 方法)                                 │
│  程序计数器(每线程一条,记录执行位置,唯一不 OOM 的区域)  │
│  直接内存 Direct Memory(NIO Buffer,堆外)                │
└──────────────────────────────────────────────────────────┘
区域存什么溢出形态
new 出来的对象OutOfMemoryError: Java heap space
元空间类定义、方法元数据OutOfMemoryError: Metaspace
方法调用的局部变量StackOverflowError(无限递归)
直接内存NIO ByteBuffer.allocateDirectOutOfMemoryError: Direct buffer memory

16.3 对象生命周期与 GC Roots

对象从生到死的判定靠可达性分析:从 GC Roots 出发走引用链,走不到的就是垃圾:

GC Roots(起点)
  ├─ 虚拟机栈局部变量(正在执行的方法里的引用)
  ├─ 静态字段(类级别的引用)
  ├─ JNI 引用
  └─ 活跃线程对象
        │
        ▼ 引用链
  可达对象 → 存活        不可达对象 → 回收
Product p = new Product("BK-001", "Java 基础", 59.9);  // 栈上 p 引用堆中对象 → 可达
p = null;                                              // 链断了 → 下次 GC 可回收
引用类型强度回收时机
强引用默认永不(除非不可达)
软引用 SoftReference内存不足才回收缓存场景
弱引用 WeakReference下次 GC 必回收ThreadLocalMap 的 key
虚引用 PhantomReference仅作回收通知堆外内存管理

16.4 三大回收算法与分代假说

算法做法优点缺点
标记-清除标记后原地删除简单内存碎片
复制存活对象搬到另一半无碎片、快浪费一半空间
标记-整理标记后向一端压缩无碎片不浪费移动成本高

分代假说(经验规律):绝大多数对象朝生夕死;熬过几轮的对象大概率长期存活。于是堆分年轻代/老年代:

新对象 → Eden ──Minor GC──> 存活复制到 Survivor ──熬过年齡阈值──> 老年代
              (复制算法,频繁但快)                  (标记-整理,不频繁但慢)

16.5 主流收集器与选型

收集器特点适用
Parallel(JDK 8 默认)多线程吞吐优先,STW 较长批处理、对延迟不敏感
G1(JDK 9+ 默认)分 Region,可设停顿目标大多数服务的稳妥默认
ZGC / Shenandoah亚毫秒停顿,并发整理大堆(>16G)、低延迟交易

选型口诀:默认 G1 起步;堆特别大或延迟极敏感再评估 ZGC;离线批跑 Parallel 足够。切勿「听说 ZGC 快」就盲目换——停顿之外的吞吐、内存占用都要测量。


16.6 常用 JVM 参数

java -Xms512m -Xmx2g \                # 堆初始/最大(生产建议设相等,避免动态扩缩抖动)
     -XX:+UseG1GC \                   # 指定收集器
     -XX:MaxGCPauseMillis=200 \       # G1 停顿目标
     -Xlog:gc*:file=gc.log:time,level,tags \   # JDK 9+ 统一日志
     -XX:+HeapDumpOnOutOfMemoryError \         # OOM 时自动 dump
     -XX:HeapDumpPath=./dumps \       # dump 存放目录
     -jar app.jar
参数说明
-Xms / -Xmx堆初始/最大值
-XX:MaxMetaspaceSize元空间上限(防类加载泄漏撑爆本地内存)
-XX:+HeapDumpOnOutOfMemoryError排障必备,生产也应打开
-Xlog:gc*JDK 9+ GC 日志(替代旧的 -XX:+PrintGCDetails

16.7 读懂 GC 日志

一行典型的 G1 日志(JDK 21):

[2026-08-19T02:13:45.102+0800][info][gc] GC(35) Pause Young (Normal) (G1 Evacuation Pause) 512M->368M(1024M) 12.345ms

拆解:GC(35) 第 35 次;Pause Young 年轻代回收;512M->368M(1024M) 回收前 512M、回收后 368M、总堆 1024M;12.345ms 停顿耗时。

关注信号

信号可能问题
Full GC 频繁(每小时多次)老年代泄漏或堆太小
回收后内存降幅很小对象都被长引用攥着(泄漏嫌疑)
单次停顿超目标值Region 过大 / 存活对象过多
Promotion 失败老年代空间不足,年轻代被迫提前晋升

16.8 工具链速查

工具用途示例
jps -l找进程jps -l
jstat实时 GC 统计jstat -gcutil <pid> 1s(每秒刷新各代使用率)
jmap堆 dump / 直方图jmap -dump:live,format=b,file=heap.hprof <pid>
jstack线程快照死锁排查(ch15)
jcmd瑞士军刀jcmd <pid> GC.heap_info
JFR低开销飞行记录jcmd <pid> JFR.start duration=60s filename=rec.jfr
Arthas在线诊断(阿里开源)dashboardwatchtrace 不重启查问题
MAT / VisualVM图形化分析 dump找支配树、查泄漏 suspects

jstat 输出解读:YGC/YGCT 年轻代次数/耗时;FGC/FGCT Full GC 次数/耗时——FGC 持续增长就是内存问题的红灯


以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。