理解内存模型、垃圾回收与线上问题排查
从 JVM 内存结构出发,深入讲解 G1/ZGC 垃圾回收器原理、常见 OOM 场景分析、Arthas 线上诊断工具使用,以及 GC 日志分析与 JVM 参数调优策略。
Java 虚拟机在执行 Java 程序时会把它管理的内存划分为若干个不同的数据区域。理解这些区域的用途和特性是 JVM 调优的基础。
┌─────────────────────────────────────────┐
│ 堆(Heap) │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ 年轻代 │ │ 老年代 │ │
│ │ ┌─┐┌─┐┌─┐ │ │ │ │
│ │ │E│S0│S1│ │ │ │ │
│ │ └─┘└─┘└─┘ │ │ │ │
│ └─────────────┘ └─────────────────┘ │
│ -Xms -Xmx │
├─────────────────────────────────────────┤
│ 元空间(Metaspace)-XX:MaxMetaspaceSize │
├─────────────────────────────────────────┤
│ 虚拟机栈(VM Stack)-Xss │
│ 本地方法栈(Native Stack) │
│ 程序计数器(PC Register) │
├─────────────────────────────────────────┤
│ 直接内存(Direct Memory) │
│ -XX:MaxDirectMemorySize │
└─────────────────────────────────────────┘
从 Serial 到 ZGC,JVM 垃圾回收器经历了多次重大演进,核心矛盾始终是"吞吐量 vs 延迟"之间的权衡:
G1(Garbage First)将堆划分为多个大小相等的 Region,每个 Region 根据需要动态扮演 Eden、Survivor 或 Old 角色。
# G1 推荐参数(Java 17,8GB 堆)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标最大停顿时间
-Xms8g -Xmx8g # 固定堆大小避免扩容
-XX:+AlwaysPreTouch # 启动时预分配内存
-XX:InitiatingHeapOccupancyPercent=45 # 老年代占比触发并发标记
-XX:G1HeapRegionSize=16m # Region 大小(根据堆大小自动计算)
-XX:+UseStringDeduplication # 字符串去重(Java 8u20+)
ZGC 使用染色指针和读屏障技术,实现了全并发垃圾回收,停顿时间几乎不受堆大小影响:
# ZGC 启动参数(Java 17+)
-XX:+UseZGC
-Xms32g -Xmx32g
-XX:+ZGenerational # Java 21 分代 ZGC(性能更优)
# ZGC 日志分析
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags:filecount=5,filesize=100m
# 关键指标
[gc,start] GC(0) Garbage Collection (Proactive)
[gc,phases] GC(0) Pause Mark Start 0.012ms # 停顿时间
[gc,phases] GC(0) Pause Mark End 0.015ms
[gc,phases] GC(0) Pause Relocate Start 0.018ms
[gc,phases] GC(0) Pause Relocate End 0.021ms
[gc,heap] GC(0) Heap: 8192M(8192M) -> 4096M(8192M) # 回收前后堆占用
OOM 是生产环境最常见的问题之一,不同区域的 OOM 有不同的排查思路:
// 1. Java heap space
// 原因:内存泄漏或堆设置过小
// 排查:
jmap -dump:format=b,file=heap.hprof <pid>
# 使用 MAT / VisualVM 分析 Dominator Tree
// 2. Metaspace
// 原因:动态生成类过多(如 CGLIB、反射)
// 排查:
-XX:+TraceClassLoading -XX:+TraceClassUnloading
# 观察类加载日志
// 3. Direct buffer memory
// 原因:NIO 直接内存泄漏(Netty 堆外内存未释放)
// 排查:
# 开启 Native Memory Tracking
-XX:NativeMemoryTracking=summary
jcmd <pid> VM.native_memory summary
// 4. Unable to create new native thread
// 原因:线程数超过系统限制(ulimit -u)
// 解决:
ulimit -u 65535 # 修改系统限制
# 或使用线程池替代无限创建线程
Arthas 是阿里巴巴开源的 Java 诊断工具,可以实时查看 JVM 状态、方法耗时、线程堆栈等:
# 连接目标进程
java -jar arthas-boot.jar
# 热更新代码(无需重启)
redefine /path/to/UserService.class
# 方法执行监控(查看入参、返回值、耗时)
watch com.example.service.UserService getUserById \
"{params, returnObj, throwExp}" \
-x 2 \
'#cost>100' # 只监控耗时超过 100ms 的调用
# 火焰图生成
profiler start --event cpu
profiler stop --file /tmp/flamegraph.html
# 线程死锁检测
thread --block
# 反编译查看线上代码
jad com.example.service.UserService
调优不是一蹴而就的,遵循"测量-分析-调整"的闭环: