JVM调优完全指南:从垃圾回收到CPU 100%再到OOM全解析

本文系统讲解JVM调优的完整知识体系,涵盖垃圾回收算法选择、分代回收策略、JVM参数调优、CPU 100%排查、OOM分析、MAT堆转储分析等实战内容,助你从入门到精通JVM调优。


文章目录

  • 一、分代回收的核心原理
    • [1.1 为什么要分代?](#1.1 为什么要分代?)
    • [1.2 分代策略](#1.2 分代策略)
    • [1.3 算法选择逻辑](#1.3 算法选择逻辑)
    • [1.4 为什么老年代不用复制算法?](#1.4 为什么老年代不用复制算法?)
    • [1.5 新生代标记-复制算法详解](#1.5 新生代标记-复制算法详解)
    • [1.6 老年代标记-整理算法详解](#1.6 老年代标记-整理算法详解)
  • 二、垃圾回收算法与回收器选择
  • 三、JVM参数调优实战
    • [3.1 JVM参数分类](#3.1 JVM参数分类)
      • [3.1.1 内存参数](#3.1.1 内存参数)
      • [3.1.2 GC参数](#3.1.2 GC参数)
      • [3.1.3 性能参数](#3.1.3 性能参数)
    • [3.2 常用JVM参数配置模板](#3.2 常用JVM参数配置模板)
      • [模板1:通用型Web服务(G1 GC)](#模板1:通用型Web服务(G1 GC))
      • 模板2:低延迟交易系统(ZGC)
      • [模板3:吞吐量优先批处理(Parallel GC)](#模板3:吞吐量优先批处理(Parallel GC))
      • [模板4:小型应用(Serial GC)](#模板4:小型应用(Serial GC))
    • [3.3 JVM参数调优建议](#3.3 JVM参数调优建议)
    • [3.4 JVM参数调优流程](#3.4 JVM参数调优流程)
  • [四、CPU 100%问题排查](#四、CPU 100%问题排查)
    • [4.1 CPU 100%常见原因](#4.1 CPU 100%常见原因)
    • [4.2 排查步骤与命令](#4.2 排查步骤与命令)
    • [4.3 排查命令汇总](#4.3 排查命令汇总)
    • [4.4 CPU 100%排查案例](#4.4 CPU 100%排查案例)
      • [案例1:死循环导致CPU 100%](#案例1:死循环导致CPU 100%)
      • [案例2:频繁GC导致CPU 100%](#案例2:频繁GC导致CPU 100%)
      • [案例3:正则表达式回溯导致CPU 100%](#案例3:正则表达式回溯导致CPU 100%)
  • 五、OOM问题全面分析
    • [5.1 OOM类型与特征](#5.1 OOM类型与特征)
    • [5.2 堆内存溢出(Heap Space)](#5.2 堆内存溢出(Heap Space))
      • [5.2.1 现象与特征](#5.2.1 现象与特征)
      • [5.2.2 排查步骤](#5.2.2 排查步骤)
      • [5.2.3 排查案例](#5.2.3 排查案例)
    • [5.3 栈溢出(StackOverflow)](#5.3 栈溢出(StackOverflow))
      • [5.3.1 现象与特征](#5.3.1 现象与特征)
      • [5.3.2 排查步骤](#5.3.2 排查步骤)
      • [5.3.3 调整栈大小](#5.3.3 调整栈大小)
    • [5.4 方法区溢出(Metaspace)](#5.4 方法区溢出(Metaspace))
      • [5.4.1 现象与特征](#5.4.1 现象与特征)
      • [5.4.2 排查步骤](#5.4.2 排查步骤)
      • [5.4.3 解决方案](#5.4.3 解决方案)
    • [5.5 直接内存溢出(Direct Buffer Memory)](#5.5 直接内存溢出(Direct Buffer Memory))
      • [5.5.1 现象与特征](#5.5.1 现象与特征)
      • [5.5.2 解决方案](#5.5.2 解决方案)
    • [5.6 创建线程溢出(Unable to create new native thread)](#5.6 创建线程溢出(Unable to create new native thread))
      • [5.6.1 现象与特征](#5.6.1 现象与特征)
      • [5.6.2 排查步骤](#5.6.2 排查步骤)
      • [5.6.3 排查案例](#5.6.3 排查案例)
  • 六、MAT堆转储分析实战
    • [6.1 MAT简介](#6.1 MAT简介)
    • [6.2 安装与配置](#6.2 安装与配置)
      • [6.2.1 下载与安装](#6.2.1 下载与安装)
      • [6.2.2 配置内存](#6.2.2 配置内存)
    • [6.3 生成堆转储文件](#6.3 生成堆转储文件)
      • [6.3.1 JVM参数自动生成](#6.3.1 JVM参数自动生成)
      • [6.3.2 手动生成堆转储](#6.3.2 手动生成堆转储)
    • [6.4 MAT使用流程](#6.4 MAT使用流程)
      • [6.4.1 打开堆转储文件](#6.4.1 打开堆转储文件)
      • [6.4.2 Leak Suspects Report(泄漏嫌疑报告)](#6.4.2 Leak Suspects Report(泄漏嫌疑报告))
      • [6.4.3 Dominator Tree(支配树)](#6.4.3 Dominator Tree(支配树))
    • [6.5 MAT定位内存泄漏实战](#6.5 MAT定位内存泄漏实战)
    • [6.6 MAT常用快捷键](#6.6 MAT常用快捷键)
  • 七、JVM监控工具使用
    • [7.1 jps(Java Virtual Machine Process Status Tool)](#7.1 jps(Java Virtual Machine Process Status Tool))
    • [7.2 jstat(JVM Statistics Monitoring Tool)](#7.2 jstat(JVM Statistics Monitoring Tool))
      • [7.2.1 查看GC情况](#7.2.1 查看GC情况)
      • [7.2.2 查看GC使用率](#7.2.2 查看GC使用率)
    • [7.3 jstack(Java Stack Trace)](#7.3 jstack(Java Stack Trace))
    • [7.4 jmap(Java Memory Map)](#7.4 jmap(Java Memory Map))
      • [7.4.1 查看堆信息](#7.4.1 查看堆信息)
      • [7.4.2 生成堆转储](#7.4.2 生成堆转储)
    • [7.5 jcmd(Java Command)](#7.5 jcmd(Java Command))
      • [7.5.1 查看GC情况](#7.5.1 查看GC情况)
      • [7.5.2 生成堆转储](#7.5.2 生成堆转储)
      • [7.6 JConsole](#7.6 JConsole)
    • [7.7 VisualVM](#7.7 VisualVM)
  • 八、实战调优案例
    • [8.1 案例1:电商服务FullGC频繁](#8.1 案例1:电商服务FullGC频繁)
      • [8.1.1 问题现象](#8.1.1 问题现象)
    • [8.1.2 排查步骤](#8.1.2 排查步骤)
      • [8.1.3 解决方案](#8.1.3 解决方案)
    • [8.2 案例2:交易系统CPU 100%](#8.2 案例2:交易系统CPU 100%)
      • [8.2.1 问题现象](#8.2.1 问题现象)
      • [8.2.2 排查步骤](#8.2.2 排查步骤)
      • [8.2.3 解决方案](#8.2.3 解决方案)
    • [8.3 案例3:批处理任务OOM](#8.3 案例3:批处理任务OOM)
      • [8.3.1 问题现象](#8.3.1 问题现象)
      • [8.3.2 排查步骤](#8.3.2 排查步骤)
      • [8.3.3 解决方案](#8.3.3 解决方案)
  • 九、总结与最佳实践
  • 结语

一、分代回收的核心原理

1.1 为什么要分代?

核心观察:对象生命周期呈现两极分化

复制代码
新生对象 → 判断存活时间 → 短期存活(<1分钟) → 新生代
                          → 长期存活(>1分钟) → 老年代

数据支撑

  • 新生代对象:95%以上朝生夕死
  • 老年代对象:80%以上长期存活

1.2 分代策略

区域 对象特征 存活率 适合算法 核心思想
新生代 方法局部变量、临时对象 <5% 标记-复制 只复制少量存活对象
老年代 缓存数据、单例对象 >80% 标记-整理 原地整理,消除碎片

1.3 算法选择逻辑

复制代码
新生代:95%对象死亡 → 复制算法(只复制5%存活对象,成本低)
老年代:80%对象存活 → 整理算法(原地整理,避免复制开销)

1.4 为什么老年代不用复制算法?

简单计算

  • 新生代复制:复制5%存活对象 → 成本低
  • 老年代复制:复制80%存活对象 → 成本极高

结论:复制算法浪费50%空间,老年代空间大,不划算。


1.5 新生代标记-复制算法详解

工作原理

复制代码
┌─────────────────┐
│     Eden区      │  新对象分配
├─────────────────┤
│   Survivor S0   │  存活对象复制
├─────────────────┤
│   Survivor S1   │  存活对象复制
└─────────────────┘

GC流程:
1. 扫描Eden+S0,标记存活对象
2. 复制存活对象到S1
3. 清空Eden+S0
4. S0和S1角色互换

优势

  • ✅ 无内存碎片
  • ✅ 回收效率高(只复制少量存活对象)

劣势

  • ❌ 内存利用率低(仅50%)

1.6 老年代标记-整理算法详解

工作原理

复制代码
┌─────────────────────────────────┐
│  ████       ████       ████████  │
│  未标记   未标记   存活对象      │
└─────────────────────────────────┘
         ↓ 标记+整理
┌─────────────────────────────────┐
│  ████████████████████           │
│     存活对象(紧密排列)         │
└─────────────────────────────────┘

优势

  • ✅ 无内存碎片
  • ✅ 大对象分配安全

劣势

  • ❌ 需要移动对象,性能开销大

二、垃圾回收算法与回收器选择

2.1 垃圾回收算法对比

算法类型 工作原理 优势 劣势 适用场景
标记-清除 标记存活对象,清除未标记 不移动对象,实现简单 产生内存碎片 老年代(CMS)
标记-整理 标记存活对象,移动到一端 无内存碎片 需要移动对象,开销大 老年代(Parallel Old、ZGC)
标记-复制 复制存活对象到另一区域 无碎片,效率高 内存利用率低(50%) 新生代(所有新生代回收器)

2.2 垃圾回收器全览

回收器 新生代算法 老年代算法 线程模型 停顿时间 适用堆大小 推荐场景
Serial GC 复制 整理 单线程 长(100ms+) <100MB 客户端应用
Parallel GC 复制 整理 多线程并行 中(50-200ms) 2-16GB 批处理、吞吐量优先
CMS GC 复制 清除 并发+并行 短(<200ms) 2-10GB ❌已废弃
G1 GC 复制 整理+复制 并发+并行 短(<200ms) 4-64GB Web服务、通用型
ZGC 整理 整理 全并发 极短(<10ms) >32GB 超大堆、极致低延迟
Shenandoah 整理 整理 并发 极短(<10ms) >32GB 大堆、均衡型

2.3 垃圾回收器选择决策树

复制代码
开始选择垃圾回收器
         │
         ├─ 第一步:确定堆大小
         │   ├─ < 100MB ─────────────────────────→ Serial GC
         │   ├─ 100MB - 4GB ────────────────────→ Parallel GC
         │   ├─ 4GB - 32GB ─────────────────────→ G1 GC
         │   └─ > 32GB ────────────────────────→ ZGC
         │
         ├─ 第二步:确定延迟要求
         │   ├─ < 10ms ─────────────────────────→ ZGC
         │   ├─ 10ms - 200ms ──────────────────→ G1 GC
         │   └─ 不敏感 ─────────────────────────→ Parallel GC
         │
         ├─ 第三步:确定业务类型
         │   ├─ Web服务/API网关 ────────────────→ G1 GC
         │   ├─ 批处理/后台任务 ────────────────→ Parallel GC
         │   ├─ 金融交易/实时竞价 ──────────────→ ZGC
         │   └─ 桌面应用/小型工具 ─────────────→ Serial GC
         │
         └─ 第四步:确定JDK版本
             ├─ JDK 8 ─────────────────────────→ G1 GC
             ├─ JDK 11 ────────────────────────→ G1 GC / ZGC
             └─ JDK 17+ ──────────────────────→ ZGC(生产推荐)

2.4 不同场景推荐配置

场景1:电商Web服务

bash 复制代码
# 应用类型:高并发Web服务
# 堆大小:8GB
# JDK版本:JDK 17
# 延迟要求:<200ms
# 推荐回收器:G1 GC

-Xms8g -Xmx8g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:InitiatingHeapOccupancyPercent=45 \
-XX:G1HeapRegionSize=16m

场景2:金融交易系统

bash 复制代码
# 应用类型:低延迟交易系统
# 堆大小:64GB
# JDK版本:JDK 21
# 延迟要求:<10ms
# 推荐回收器:ZGC

-Xms64g -Xmx64g \
-XX:+UseZGC \
-XX:ZGCParallelGCThreads=8 \
-XX:MaxGCPauseMillis=10

场景3:批处理ETL任务

bash 复制代码
# 应用类型:吞吐量优先批处理
# 堆大小:16GB
# JDK版本:JDK 11
# 延迟要求:不敏感
# 推荐回收器:Parallel GC

-Xms16g -Xmx16g \
-XX:+UseParallelGC \
-XX:ParallelGCThreads=12 \
-XX:GCTimeRatio=95

场景4:桌面应用

bash 复制代码
# 应用类型:桌面应用
# 堆大小:512MB
# JDK版本:任意
# 延迟要求:不敏感
# 推荐回收器:Serial GC

-Xms512m -Xmx512m \
-XX:+UseSerialGC

2.5 分代回收器组合

组合1:Serial GC

bash 复制代码
# 配置
-XX:+UseSerialGC

# 算法说明
新生代:Serial(单线程复制)
老年代:Serial Old(单线程整理)

# 适用场景
- 客户端应用
- 小型工具
- 单核CPU
- 堆大小 < 100MB

组合2:Parallel GC

bash 复制代码
# 配置
-XX:+UseParallelGC

# 算法说明
新生代:Parallel Scavenge(多线程复制)
老年代:Parallel Old(多线程整理)

# 适用场景
- 批处理任务
- 后台计算
- 吞吐量优先
- 堆大小 2-16GB

组合3:G1 GC

bash 复制代码
# 配置
-XX:+UseG1GC

# 算法说明
新生代:G1 YoungGC(Region化复制)
老年代:G1 MixedGC(Region化整理+复制)

# 适用场景
- Web服务
- API网关
- 企业级应用
- 堆大小 4-64GB
- JDK 9+默认

组合4:ZGC

bash 复制代码
# 配置
-XX:+UseZGC

# 算法说明
新生代:ZGC(并发标记-整理)
老年代:ZGC(并发标记-整理)
注:ZGC是单代回收器,JDK 21+支持分代

# 适用场景
- 金融交易
- 实时竞价
- 高频请求
- 堆大小 > 32GB
- JDK 17+生产推荐

三、JVM参数调优实战

3.1 JVM参数分类

3.1.1 内存参数

参数 说明 默认值 推荐值
-Xms 堆内存初始大小 物理内存的1/64 与-Xmx相同
-Xmx 堆内存最大大小 物理内存的1/4 物理内存的60-80%
-Xmn 新生代大小 堆的1/3 根据对象存活率调整
-XX:NewRatio 新生代/老年代比例 2 2-4
-XX:SurvivorRatio Eden/S0/S1比例 8 8
-XX:MetaspaceSize 元空间初始大小 21MB 256MB
-XX:MaxMetaspaceSize 元空间最大大小 无限制 512MB
-Xss 线程栈大小 1MB 256KB-1MB

3.1.2 GC参数

参数 说明 默认值 推荐值
-XX:+UseG1GC 使用G1回收器 - 推荐
-XX:+UseZGC 使用ZGC回收器 - JDK 17+推荐
-XX:MaxGCPauseMillis 目标最大停顿时间 200ms 100-500ms
-XX:InitiatingHeapOccupancyPercent 触发并发标记阈值 45 40-50
-XX:GCTimeRatio GC时间占比 99 99

3.1.3 性能参数

参数 说明 默认值 推荐值
-XX:+UseStringDeduplication 字符串去重 false true(大堆)
-XX:+UseCompressedOops 压缩指针 <32GB自动开启 true
-XX:+UseCompressedClassPointers 压缩类指针 <32GB自动开启 true
-XX:+AlwaysPreTouch 预分配内存 false true(大堆)

3.2 常用JVM参数配置模板

模板1:通用型Web服务(G1 GC)

bash 复制代码
-Xms4g -Xmx4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:InitiatingHeapOccupancyPercent=45 \
-XX:G1HeapRegionSize=8m \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+UseStringDeduplication \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof \
-Xlog:gc*:file=/logs/gc.log:time,tags:filecount=10,filesize=100m

模板2:低延迟交易系统(ZGC)

bash 复制代码
-Xms32g -Xmx32g \
-XX:+UseZGC \
-XX:ZGCParallelGCThreads=8 \
-XX:ZGCConcurrentGCThreads=2 \
-XX:MaxGCPauseMillis=10 \
-XX:+AlwaysPreTouch \
-XX:MetaspaceSize=512m \
-XX:MaxMetaspaceSize=1g \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof \
-Xlog:gc*:file=/logs/gc.log:time,tags:filecount=10,filesize=100m

模板3:吞吐量优先批处理(Parallel GC)

bash 复制代码
-Xms16g -Xmx16g \
-XX:+UseParallelGC \
-XX:ParallelGCThreads=12 \
-XX:GCTimeRatio=95 \
-XX:NewRatio=2 \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof \
-Xlog:gc*:file=/logs/gc.log:time,tags:filecount=10,filesize=100m

模板4:小型应用(Serial GC)

bash 复制代码
-Xms512m -Xmx512m \
-XX:+UseSerialGC \
-XX:NewRatio=2 \
-XX:MetaspaceSize=128m \
-XX:MaxMetaspaceSize=256m \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof

3.3 JVM参数调优建议

建议1:堆大小设置

bash 复制代码
# ❌ 错误做法
-Xms2g -Xmx8g  # 初始和最大不一致,可能导致频繁扩容

# ✅ 正确做法
-Xms8g -Xmx8g  # 初始和最大一致,避免动态调整

原因

  • 堆内存动态调整会产生性能抖动
  • 初始堆过小会导致频繁GC
  • 生产环境建议设置相同

建议2:新生代大小设置

bash 复制代码
# ❌ 错误做法
-Xmn256m  # 新生代过小,频繁YoungGC

# ✅ 正确做法
-XX:NewRatio=2  # 新生代占堆的1/3

调优原则

  • 新生代太小:YoungGC频繁
  • 新生代太大:老年代空间不足,触发FullGC
  • 推荐比例:新生代占堆的1/3-1/4

建议3:元空间设置

bash 复制代码
# ❌ 错误做法
-XX:MaxMetaspaceSize=unlimited  # 不限制可能导致OOM

# ✅ 正确做法
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

原因

  • 元空间使用本地内存
  • 类加载过多可能导致元空间溢出
  • 必须设置最大值限制

建议4:GC日志配置

bash 复制代码
# JDK 8
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/logs/gc.log

# JDK 11+
-Xlog:gc*:file=/logs/gc.log:time,tags:filecount=10,filesize=100m

建议

  • 开启GC日志便于问题排查
  • 日志文件设置滚动策略
  • 保留最近10个日志文件

3.4 JVM参数调优流程

复制代码
开始调优
    │
    ├─ 第一步:确定应用类型
    │   ├─ Web服务 ──────────────→ G1 GC
    │   ├─ 批处理 ──────────────→ Parallel GC
    │   └─ 低延迟交易 ──────────→ ZGC
    │
    ├─ 第二步:确定堆大小
    │   └─ 物理内存的60-80%
    │
    ├─ 第三步:设置GC参数
    │   ├─ G1 GC: MaxGCPauseMillis=200
    │   ├─ ZGC: MaxGCPauseMillis=10
    │   └─ Parallel GC: GCTimeRatio=99
    │
    ├─ 第四步:设置元空间
    │   └─ MetaspaceSize=256m, MaxMetaspaceSize=512m
    │
    ├─ 第五步:开启GC日志
    │   └─ -Xlog:gc*:file=/logs/gc.log
    │
    └─ 第六步:监控验证
        └─ jstat、jvisualvm、jconsole

四、CPU 100%问题排查

4.1 CPU 100%常见原因

原因类型 典型场景 特征
死循环 无限循环、逻辑错误 单线程100%
频繁GC 内存泄漏、堆太小 多线程波动
线程阻塞 锁竞争、死锁 线程处于BLOCKED状态
CPU密集计算 大数据处理、加密算法 多个线程高CPU
反射调用 大量反射使用 高CPU开销
正则表达式 复杂正则匹配 CPU飙升

4.2 排查步骤与命令

步骤1:确认CPU 100%进程

bash 复制代码
# 查看CPU占用最高的进程
top -c

# 或使用htop(交互式)
htop

# 输出示例
PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
12345 appuser  20   0   8.0g   4.0g   512m R 100.0  50.0   2:30.15 java -Xms4g -Xmx4g -jar app.jar

关键信息

  • PID:进程ID(如12345)
  • %CPU:CPU占用率
  • COMMAND:启动命令

步骤2:查看CPU占用最高的线程

bash 复制代码
# 使用top -H
top -H -p 12345

# 输出示例
PID   USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
12345 appuser  20   0   8.0g   4.0g   512m R 100.0  50.0   2:30.15 java
12456 appuser  20   0   8.0g   4.0g   512m R  99.5  50.0   2:15.20 java

关键信息

  • 线程PID:12456(需要转换为16进制)

步骤3:线程ID转换为16进制

bash 复制代码
# 将线程PID 12456 转换为16进制
printf "%x\n" 12456

# 输出
30a8

结果 :线程16进制ID为 30a8


步骤4:查看线程堆栈

bash 复制代码
# 使用jstack
jstack 12345 | grep -A 20 "30a8"

# 方法2:保存完整堆栈
jstack 12345 > /tmp/jstack_12345.log

# 查看线程堆栈
grep -A 20 "30a8" /tmp/jstack_12345.log

# 输出示例
"Thread-0" #30a8 prio=5 os_prio=0 tid=0x00007f1234567890 nid=0x30a8 runnable [0x00007f1234000000]
   java.lang.Thread.State: RUNNABLE
    at com.example.Service.heavyCompute(Service.java:123)
    at com.example.Service.run(Service.java:45)

关键信息

  • 线程状态:RUNNABLE
  • 代码位置:com.example.Service.heavyCompute:123

4.3 排查命令汇总

命令 用途 示例
top -c 查看进程CPU占用 top -c
top -H -p <pid> 查看线程CPU占用 top -H -p 12345
jstack <pid> 查看线程堆栈 jstack 12345 > /tmp/jstack.log
jstat -gc <pid> 查看GC情况 jstat -gc 12345 1000 10
jstat -gcutil <pid> 查看GC使用率 jstat -gcutil 12345 1000 10
printf "%x\n" <tid> 线程ID转16进制 printf "%x\n" 12456

4.4 CPU 100%排查案例

案例1:死循环导致CPU 100%

现象

bash 复制代码
# 单线程CPU 100%
top -c
PID   %CPU  COMMAND
12345 100.0 java -jar app.jar

# 查看线程
top -H -p 12345
PID   %CPU  COMMAND
12456 99.5 java -jar app.jar

排查

bash 复制代码
# 转换线程ID
printf "%x\n" 12456
30a8

# 查看堆栈
jstack 12345 | grep -A 20 "30a8"
"Thread-0" #30a8 runnable
   at com.example.Service.heavyCompute(Service.java:123)

定位代码

java 复制代码
public void heavyCompute() {
    int i = 0;
    while (i < 0) {  // ❌ 条件永远为false
        i++;
    }
}

解决方案:修复循环条件


案例2:频繁GC导致CPU 100%

现象

bash 复制代码
# 多个线程CPU高,波动大
top -c
PID   %CPU  COMMAND
12345 80.5 java -jar app.jar
12456 75.3 java -jar app.jar
12457 70.1 java -jar app.jar

排查

bash 复制代码
# 查看GC情况
jstat -gcutil 12345 1000 10
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00   0.00  98.00  80.00  95.00  90.00   50      2.500    5      3.200   5.700

特征:YGC频繁(每秒1次),老年代使用率高

解决方案

bash 复制代码
# 增大堆内存
-Xms8g -Xmx8g  # 原4g

# 调整新生代比例
-XX:NewRatio=2  # 新生代占堆的1/3

案例3:正则表达式回溯导致CPU 100%

现象

bash 复制代码
# 处理日志文件时CPU飙升
top -c
PID   %CPU  COMMAND
12345 100.0 java -jar log-parser.jar

排查

bash 复制代码
# 查看堆栈
jstack 12345 | grep -A 20 "runnable"
"Thread-0" #30a8 runnable
   at java.util.regex.Pattern$GroupCurly.match(Pattern.java:4633)
   at java.util.regex.Pattern$Loop.match(Pattern.java:4747)

定位代码

java 复制代码
// ❌ 复杂正则表达式
Pattern pattern = Pattern.compile("(a|b)*c");

解决方案

java 复制代码
// ✅ 使用非贪婪匹配
Pattern pattern = Pattern.compile("(?:a|b)*?c");

五、OOM问题全面分析

5.1 OOM类型与特征

OOM类型 报错信息 触发原因 典型场景
堆内存溢出 java.lang.OutOfMemoryError: Java heap space 堆内存不足 内存泄漏、堆太小
方法区溢出 java.lang.OutOfMemoryError: Metaspace 元空间不足 类加载过多
栈溢出 java.lang.StackOverflowError 栈深度过深 递归太深
直接内存溢出 java.lang.OutOfMemoryError: Direct buffer memory 直接内存不足 NIO堆外内存
创建线程溢出 java.lang.OutOfMemoryError: unable to create new native thread 线程过多 线程泄漏

5.2 堆内存溢出(Heap Space)

5.2.1 现象与特征

报错信息

复制代码
java.lang.OutOfMemoryError: Java heap space

典型场景

  1. 内存泄漏(对象无法回收)
  2. 堆内存设置过小
  3. 大对象分配失败

5.2.2 排查步骤

步骤1:确认OOM类型

bash 复制代码
# 查看应用日志
tail -100 /logs/application.log

# 输出示例
java.lang.OutOfMemoryError: Java heap space
    at java.util.HashMap.resize(HashMap.java:703)
    at java.util.HashMap.putVal(HashMap.java:630)
    at com.example.Service.process(Service.java:123)

步骤2:生成堆转储文件

方法1:自动生成

bash 复制代码
# JVM参数配置
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps

方法2:手动生成

bash 复制代码
# 使用jmap生成
jmap -dump:format=b,file=/logs/heapdump.hprof 12345

# 只dump存活对象
jmap -dump:format=b,live,file=/logs/heapdump_live.hprof 12345

5.2.3 排查案例

案例1:HashMap内存泄漏

现象

复制代码
java.lang.OutOfMemoryError: Java heap space

排查

bash 复制代码
# 1. 生成堆转储
jmap -dump:format=b,file=/logs/heapdump.hprof 12345

# 2. 用MAT分析
# 发现:HashMap占用50%堆内存

定位代码

java 复制代码
// ❌ 静态HashMap只增不减
public static Map<String, Object> cache = new HashMap<>();

public void process(String key, Object value) {
    cache.put(key, value);  // 只增不减,内存泄漏
}

解决方案

java 复制代码
// ✅ 使用WeakReference
public static Map<String, Object> cache = new ConcurrentHashMap<>();

// 或使用LRU Cache
public static Map<String, Object> cache = new LinkedHashMap<>(1000, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > 10000;
    }
};

5.3 栈溢出(StackOverflow)

5.3.1 现象与特征

报错信息

复制代码
java.lang.StackOverflowError
    at com.example.Service.recursive(Service.java:123)
    at com.example.Service.recursive(Service.java:125)
    at com.example.Service.recursive(Service.java:125)
    ...

典型场景

  1. 递归太深
  2. 循环依赖调用

5.3.2 排查步骤

步骤1:查看堆栈

bash 复制代码
# 查看应用日志
tail -100 /logs/application.log

# 输出示例
java.lang.StackOverflowError
    at com.example.Service.recursive(Service.java:123)
    at com.example.Service.recursive(Service.java:125)
    at com.example.Service.recursive(Service.java:125)
    ...
    at com.example.Service.recursive(Service.java:125)

特征:同一方法重复调用


步骤2:定位代码

场景1:递归太深

java 复制代码
// ❌ 无终止条件的递归
public void recursive() {
    recursive();  // 无限递归
}

解决方案

java 复制代码
// ✅ 添加终止条件
public void recursive(int depth) {
    if (depth > 1000) {  // 终止条件
        return;
    }
    recursive(depth + 1);
}

5.3.3 调整栈大小

bash 复制代码
# 增大线程栈大小
-Xss2m  # 默认1MB,增大到2MB

# 栈溢出频繁时增大
-Xss4m  # 增大到4MB

注意:栈大小不是越大越好,过大会导致可创建线程数减少


5.4 方法区溢出(Metaspace)

5.4.1 现象与特征

报错信息

复制代码
java.lang.OutOfMemoryError: Metaspace

典型场景

  1. 动态类加载过多
  2. 反射调用频繁
  3. 使用CGLIB等字节码增强

5.4.2 排查步骤

步骤1:确认OOM类型

bash 复制代码
# 查看应用日志
tail -100 /logs/application.log

# 输出示例
java.lang.OutOfMemoryError: Metaspace
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)

步骤2:查看元空间使用情况

bash 复制代码
# 使用jstat查看
jstat -gc 12345 1000 5

# 输出示例
  S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
 8704.0 8704.0  0.0   8704.0 69632.0  65536.0  139776.0  125952.0  51200.0 51200.0 5120.0 4864.0    5      0.125   0      0.000    0.125

关键字段

  • MC:元空间容量(Metaspace Capacity)
  • MU:元空间使用量(Metaspace Used)

特征:MU接近MC,元空间即将溢出


5.4.3 解决方案

bash 复制代码
# 增大元空间大小
-XX:MetaspaceSize=512m \
-XX:MaxMetaspaceSize=1g

5.5 直接内存溢出(Direct Buffer Memory)

5.5.1 现象与特征

报错信息

复制代码
java.lang.OutOfMemoryError: Direct buffer memory

典型场景

  1. NIO堆外内存使用过多
  2. Netty堆外内存泄漏
  3. ByteBuffer.allocateDirect使用过多

5.5.2 解决方案

bash 复制代码
# 增大直接内存限制
-XX:MaxDirectMemorySize=2g

# 或使用堆内存
-XX:-UseFastAccessorMethods

5.6 创建线程溢出(Unable to create new native thread)

5.6.1 现象与特征

报错信息

复制代码
java.lang.OutOfMemoryError: unable to create new native thread

典型场景

  1. 线程泄漏
  2. 线程池配置不当
  3. 创建线程过多

5.6.2 排查步骤

步骤1:查看线程数

bash 复制代码
# 查看线程数
ps -eLf | grep java | wc -l

# 查看Java线程数
jstack 12345 | grep "java.lang.Thread.State" | wc -l

5.6.3 排查案例

案例1:线程池线程泄漏

现象

复制代码
java.lang.OutOfMemoryError: unable to create new native thread

定位代码

java 复制代码
// ❌ 每次请求创建新线程
public void processRequest() {
    new Thread(() -> {
        // 处理逻辑
    }).start();
}

解决方案

java 复制代码
// ✅ 使用线程池
private static final ExecutorService executor = Executors.newFixedThreadPool(100);

public void processRequest() {
    executor.submit(() -> {
        // 处理逻辑
    });
}

六、MAT堆转储分析实战

6.1 MAT简介

Eclipse Memory Analyzer Tool (MAT) 是一个强大的Java堆内存分析工具,用于分析堆转储文件,定位内存泄漏和大对象。

下载地址https://www.eclipse.org/mat/downloads/


6.2 安装与配置

6.2.1 下载与安装

bash 复制代码
# 下载MAT
wget https://www.eclipse.org/downloads/download.php?file=/mat/1.14.0/rcp/MemoryAnalyzer-1.14.0.20230223-macosx.cocoa.x86_64.dmg

# Windows下载
# https://www.eclipse.org/downloads/download.php?file=/mat/1.14.0/rcp/MemoryAnalyzer-1.14.0.20230223-win32.win32.x86_64.zip

# 解压后直接运行
./MemoryAnalyzer

6.2.2 配置内存

bash 复制代码
# 编辑MemoryAnalyzer.ini
vim MemoryAnalyzer.ini

# 修改堆内存
-vmargs
-Xmx8g  # MAT自身堆内存,至少是dump文件大小的1.5倍

6.3 生成堆转储文件

6.3.1 JVM参数自动生成

bash 复制代码
# JVM启动参数
-Xms4g -Xmx4g \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps

说明

  • 当发生OOM时,自动生成堆转储文件
  • 文件路径:/logs/heapdump.hprof

6.3.2 手动生成堆转储

方法1:使用jmap

bash 复制代码
# 生成堆转储
jmap -dump:format=b,file=/logs/heapdump.hprof 12345

# 只dump存活对象(推荐)
jmap -dump:format=b,live,file=/logs/heapdump_live.hprof 12345

方法2:使用jcmd

bash 复制代码
# 生成堆转储
jcmd 12345 GC.heap_dump /logs/heapdump.hprof

6.4 MAT使用流程

6.4.1 打开堆转储文件

复制代码
1. 启动MAT
2. File -> Open Heap Dump
3. 选择堆转储文件(heapdump.hprof)
4. 选择解析方式
   - Leak Suspects Report(泄漏嫌疑报告,推荐)
   - Component Report(组件报告)
5. 等待解析完成

6.4.2 Leak Suspects Report(泄漏嫌疑报告)

页面说明

复制代码
┌─────────────────────────────────────────┐
│  Leak Suspects Report                    │
├─────────────────────────────────────────┤
│  Problem Suspect 1                      │
│  - Description: Class java.util.HashMap  │
│  - Size: 1.2 GB (60% of heap)           │
│  - Suspects:                             │
│    • com.example.Service.cache           │
│    • com.example.Service.process         │
├─────────────────────────────────────────┤
│  Problem Suspect 2                      │
│  - Description: Class java.lang.String[]│
│  - Size: 500 MB (25% of heap)           │
├─────────────────────────────────────────┤
│  Problem Suspect 3                      │
│  - Description: Class byte[]            │
│  - Size: 200 MB (10% of heap)           │
└─────────────────────────────────────────┘

6.4.3 Dominator Tree(支配树)

页面说明

复制代码
显示堆中所有对象及其占用的内存,按大小排序。

Retained Heap(保留堆):如果该对象被回收,可以释放的内存总量。

示例输出:
┌──────────────────────────────────────────────┐
│ Class Name │ Shallow Heap │ Retained Heap    │
├──────────────────────────────────────────────┤
│ HashMap   │ 128 Bytes    │ 1.2 GB            │
│ String[]  │ 24 Bytes     │ 500 MB            │
│ byte[]    │ 16 Bytes     │ 200 MB            │
└──────────────────────────────────────────────┘

使用步骤

复制代码
1. 点击 Dominator Tree 标签
2. 按Retained Heap排序
3. 右键大对象 -> Path to GC Roots -> exclude all phantom/weak/soft etc. references
4. 查看引用链,定位泄漏点

6.5 MAT定位内存泄漏实战

案例1:HashMap内存泄漏

现象

复制代码
java.lang.OutOfMemoryError: Java heap space

步骤1:打开堆转储

复制代码
File -> Open Heap Dump -> heapdump.hprof -> Leak Suspects Report

步骤2:查看泄漏嫌疑

复制代码
Problem Suspect 1:
- Class: java.util.HashMap
- Size: 1.2 GB (60% of heap)
- Suspects:
  • com.example.Service.cache

步骤3:查看支配树

复制代码
Dominator Tree -> 按Retained Heap排序
HashMap (1.2 GB)
  └─ com.example.Service.cache
      └─ 1000000 entries

步骤4:查看引用链

复制代码
右键HashMap -> Path to GC Roots -> exclude all phantom/weak/soft etc. references

结果:
com.example.Service.cache -> HashMap (1.2 GB)

步骤5:定位代码

java 复制代码
// ❌ 静态HashMap只增不减
public static Map<String, Object> cache = new HashMap<>();

public void process(String key, Object value) {
    cache.put(key, value);  // 只增不减
}

解决方案

java 复制代码
// ✅ 使用LRU Cache
public static Map<String, Object> cache = new LinkedHashMap<>(1000, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > 10000;
    }
};

案例2:ThreadLocal内存泄漏

现象

复制代码
java.lang.OutOfMemoryError: Java heap space

步骤1:打开堆转储

复制代码
File -> Open Heap Dump -> heapdump.hprof -> Leak Suspects Report

步骤2:查看泄漏嫌疑

复制代码
Problem Suspect 1:
- Class: java.lang.ThreadLocal
- Size: 800 MB (40% of heap)
- Suspects:
  • com.example.Service.userCache

步骤3:查看支配树

复制代码
Dominator Tree -> 按Retained Heap排序
ThreadLocal (800 MB)
  └─ com.example.Service.userCache
      └─ 10000 threads

步骤4:查看引用链

复制代码
右键ThreadLocal -> Path to GC Roots -> exclude all phantom/weak/soft etc. references

结果:
Thread -> ThreadLocalMap -> ThreadLocal (800 MB)

步骤5:定位代码

java 复制代码
// ❌ ThreadLocal未清理
private static final ThreadLocal<User> userCache = new ThreadLocal<>();

public void process(User user) {
    userCache.set(user);
    // 忘记清理
}

解决方案

java 复制代码
// ✅ 使用try-finally清理
private static final ThreadLocal<User> userCache = new ThreadLocal<>();

public void process(User user) {
    try {
        userCache.set(user);
        // 处理逻辑
    } finally {
        userCache.remove();  // 清理
    }
}

6.6 MAT常用快捷键

快捷键 功能
Ctrl + Shift + H 打开Histogram
Ctrl + Shift + D 打开Dominator Tree
Ctrl + Shift + L 打开Leak Suspects Report
Ctrl + Shift + O 打开OQL查询

七、JVM监控工具使用

7.1 jps(Java Virtual Machine Process Status Tool)

功能:查看Java进程

bash 复制代码
# 查看所有Java进程
jps

# 输出示例
12345 MainService
12346 WorkerService

# 查看详细参数
jps -l

# 输出示例
12345 com.example.MainService
12346 com.example.WorkerService

# 查看JVM参数
jps -v

# 输出示例
12345 MainService -Xms4g -Xmx4g -XX:+UseG1GC

7.2 jstat(JVM Statistics Monitoring Tool)

功能:查看JVM统计信息

7.2.1 查看GC情况

bash 复制代码
# 查看GC统计(每秒刷新1次,共10次)
jstat -gc 12345 1000 10

# 输出示例
  S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
 8704.0 8704.0  0.0   8704.0 69632.0  65536.0  139776.0  125952.0  51200.0 51200.0 5120.0 4864.0    5      0.125   0      0.000    0.125

字段说明

  • S0C/S1C:S0/S1区容量
  • S0U/S1U:S0/S1区使用量
  • EC:Eden区容量
  • EU:Eden区使用量
  • OC:老年代容量
  • OU:老年代使用量
  • MC:元空间容量
  • MU:元空间使用量
  • YGC:Young GC次数
  • YGCT:Young GC总时间
  • FGC:Full GC次数
  • FGCT:Full GC总时间
  • GCT:GC总时间

7.2.2 查看GC使用率

bash 复制代码
# 查看GC使用率(每秒刷新1次,共10次)
jstat -gcutil 12345 1000 10

# 输出示例
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00  50.00  90.00  75.00  95.00  90.00   50      2.500    5      3.200   5.700

字段说明

  • S0/S1/E/O/M/CCS:各区域使用率(百分比)
  • YGC/FGC:GC次数
  • YGCT/FGCT:GC总时间
  • GCT:GC总时间

7.3 jstack(Java Stack Trace)

功能:查看线程堆栈

bash 复制代码
# 查看线程堆栈
jstack 12345

# 输出示例
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.171-b11 mixed mode):

"Attach Listener" #13 daemon prio=9 os_prio=0 tid=0x00007f1234567890 nid=0x30a8 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Thread-0" #12 prio=5 os_prio=0 tid=0x00007f1234567890 nid=0x30a9 runnable [0x00007f1234000000]
   java.lang.Thread.State: RUNNABLE
    at com.example.Service.process(Service.java:123)
    at com.example.Service.run(Service.java:45)

字段说明

  • 线程名称
  • 线程状态(RUNNABLE/BLOCKED/WAITING)
  • 代码位置

7.4 jmap(Java Memory Map)

功能:查看内存信息

7.4.1 查看堆信息

bash 复制代码
# 查看堆信息
jmap -heap 12345

# 输出示例
Attaching to process ID 12345, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.171-b11

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4194304000 (4000.0MB)
   NewSize                  = 1398144000 (1333.125MB)
   MaxNewSize               = 1398144000 (1333.125MB)
   OldSize                  = 2796288000 (2666.25MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21810376 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 1111490560 (1060.3125MB)
   used     = 987654320 (942.0MB)
   free     = 123836240 (118.3125MB)
   88.84% used
From Space:
   capacity = 139814400 (133.3125MB)
   used     = 0 (0.0MB)
   free     = 139814400 (133.3125MB)
   0.0% used
To Space:
   capacity = 139814400 (133.3125MB)
   used     = 0 (0.0MB)
   free     = 139814400 (133.3125MB)
   0.0% used

PS Old Generation
   capacity = 2796288000 (2666.25MB)
   used     = 1234567890 (1177.5MB)
   free     = 1561720110 (1488.75MB)
   44.17% used

7.4.2 生成堆转储

bash 复制代码
# 生成堆转储
jmap -dump:format=b,file=/logs/heapdump.hprof 12345

# 只dump存活对象
jmap -dump:format=b,live,file=/logs/heapdump_live.hprof 12345

7.5 jcmd(Java Command)

功能:多功能JVM诊断工具

7.5.1 查看GC情况

bash 复制代码
# 查看GC情况
jcmd 12345 GC.heap_info

# 输出示例
12345:

 garbage-first heap   total 4096000K, used 2048000K [0x00000006c0000000, 0x00000006c0000000, 0x0000000800000000)
  region size 16384K, 15 young (245760K), 3 survivors (49152K)
 [0x00000006c0000000, 0x00000006c0000000, 0x00000006c0000000) young eden space

7.5.2 生成堆转储

bash 复制代码
# 生成堆转储
jcmd 12345 GC.heap_dump /logs/heapdump.hprof

7.6 JConsole

功能:图形化JVM监控工具

启动方式

bash 复制代码
jconsole

使用步骤

复制代码
1. 启动jconsole
2. 连接到Java进程
3. 查看监控指标:
   - 内存(Memory)
   - 线程(Threads)
   - 类(Classes)
   - GC(GC)
   - VM(VM Summary)

7.7 VisualVM

功能:全能JVM诊断工具

启动方式

bash 复制代码
jvisualvm

使用步骤

复制代码
1. 启动VisualVM
2. 连接到Java进程
3. 查看监控指标:
   - 内存(Memory)
   - 线程(Threads)
   - GC(GC)
   - 堆转储(Heap Dump)
   - 线程转储(Thread Dump)

八、实战调优案例

8.1 案例1:电商服务FullGC频繁

8.1.1 问题现象

监控数据

bash 复制代码
# GC日志
[2025-02-24T16:00:00.123+0800] [0.123s] [gc,start] GC(0) Pause Young (G1 Evacuation Pause) 56M->45M(512M) 20.123ms
[2025-02-24T16:00:01.456+0800] [1.456s] [gc,start] GC(1) Pause Young (G1 Evacuation Pause) 56M->45M(512M) 18.234ms
...
[2025-02-24T16:05:00.789+0800] [300.789s] [gc,start] GC(100) Pause Full (Allocation Failure) 512M->512M(512M) 5000.123ms

特征

  • YoungGC频繁(每秒1次)
  • FullGC频繁(每5分钟1次)
  • FullGC停顿时间长(5秒)

8.1.2 排查步骤

步骤1:查看堆使用情况

bash 复制代码
jstat -gcutil 12345 1000 10
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00  50.00  98.00  95.00  95.00  90.00  100     2.500    10     50.000  52.500

分析:老年代使用率95%,接近溢出


步骤2:生成堆转储

bash 复制代码
jmap -dump:format=b,live,file=/logs/heapdump.hprof 12345

步骤3:MAT分析

复制代码
Leak Suspects Report:
Problem Suspect 1:
- Class: java.util.concurrent.ConcurrentHashMap
- Size: 300 MB (60% of old gen)
- Suspects:
  • com.example.Service.orderCache

步骤4:定位代码

java 复制代码
// ❌ 未限制大小的缓存
public static Map<String, Order> orderCache = new ConcurrentHashMap<>();

public void cacheOrder(String orderId, Order order) {
    orderCache.put(orderId, order);  // 无大小限制
}

8.1.3 解决方案

java 复制代码
// ✅ 使用LRU Cache
public static Map<String, Order> orderCache = new LinkedHashMap<>(1000, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > 10000;
    }
};

优化效果

复制代码
FullGC频率: 每5分钟1次 → 每24小时1次
FullGC停顿: 5秒 → 2秒
老年代使用率: 95% → 60%

8.2 案例2:交易系统CPU 100%

8.2.1 问题现象

监控数据

bash 复制代码
top -c
PID   %CPU  COMMAND
12345 100.0 java -jar trading-system.jar

top -H -p 12345
PID   %CPU  COMMAND
12456 99.5 java -jar trading-system.jar

特征:单线程CPU 100%


8.2.2 排查步骤

步骤1:转换线程ID

bash 复制代码
printf "%x\n" 12456
30a8

步骤2:查看线程堆栈

bash 复制代码
jstack 12345 | grep -A 20 "30a8"
"Thread-0" #30a8 runnable
   at java.util.regex.Pattern$GroupCurly.match(Pattern.java:4633)
   at java.util.regex.Pattern$Loop.match(Pattern.java:4747)

步骤3:定位代码

java 复制代码
// ❌ 复杂正则表达式
Pattern pattern = Pattern.compile("(a|b)*c");

public void parse(String input) {
    Matcher matcher = pattern.matcher(input);
    matcher.matches();
}

8.2.3 解决方案

java 复制代码
// ✅ 使用非贪婪匹配
Pattern pattern = Pattern.compile("(?:a|b)*?c");

优化效果

复制代码
CPU占用: 100% → 5%
响应时间: 2秒 → 50ms

8.3 案例3:批处理任务OOM

8.3.1 问题现象

报错信息

复制代码
java.lang.OutOfMemoryError: Java heap space
    at java.util.HashMap.resize(HashMap.java:703)

8.3.2 排查步骤

步骤1:生成堆转储

bash 复制代码
jmap -dump:format=b,file=/logs/heapdump.hprof 12345

步骤2:MAT分析

复制代码
Leak Suspects Report:
Problem Suspect 1:
- Class: java.util.HashMap
- Size: 3.5 GB (90% of heap)
- Suspects:
  • com.example.Service.batchData

步骤3:定位代码

java 复制代码
// ❌ 一次性加载所有数据
public List<Data> loadBatchData() {
    List<Data> data = new ArrayList<>();
    ResultSet rs = statement.executeQuery("SELECT * FROM data");
    while (rs.next()) {
        data.add(new Data(rs));  // 加载100万条数据
    }
    return data;
}

8.3.3 解决方案

java 复制代码
// ✅ 分批处理
public void processBatchData() {
    int offset = 0;
    int batchSize = 1000;
    while (true) {
        List<Data> data = loadData(offset, batchSize);
        if (data.isEmpty()) break;
        processData(data);
        offset += batchSize;
    }
}

private List<Data> loadData(int offset, int batchSize) {
    List<Data> data = new ArrayList<>();
    ResultSet rs = statement.executeQuery(
        "SELECT * FROM data LIMIT " + batchSize + " OFFSET " + offset
    );
    while (rs.next()) {
        data.add(new Data(rs));
    }
    return data;
}

优化效果

复制代码
堆内存: 4GB → 2GB
处理时间: 10分钟 → 8分钟
OOM次数: 每天1次 → 0次

九、总结与最佳实践

9.1 JVM调优核心原则

原则1:先监控后调优

复制代码
监控指标 → 定位问题 → 制定方案 → 验证效果

关键指标

  • GC频率
  • GC停顿时间
  • 堆内存使用率
  • CPU占用率
  • 线程数

原则2:堆大小 = 物理内存的60-80%

bash 复制代码
# ❌ 错误做法
-Xms16g -Xmx16g  # 物理内存只有16GB,占用100%

# ✅ 正确做法
-Xms12g -Xmx12g  # 物理内存16GB,占用75%

原则3:初始堆 = 最大堆

bash 复制代码
# ❌ 错误做法
-Xms4g -Xmx8g  # 初始和最大不一致

# ✅ 正确做法
-Xms8g -Xmx8g  # 初始和最大一致

原则4:选择合适的GC算法

复制代码
堆大小 < 4GB  →  Parallel GC
堆大小 4-32GB →  G1 GC
堆大小 > 32GB →  ZGC

原则5:开启GC日志

bash 复制代码
# JDK 8
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/logs/gc.log

# JDK 11+
-Xlog:gc*:file=/logs/gc.log:time,tags:filecount=10,filesize=100m

9.2 JVM调优检查清单

启动前检查

  • 堆大小设置为物理内存的60-80%
  • 初始堆大小等于最大堆大小
  • 选择合适的GC算法
  • 开启GC日志
  • 设置元空间大小
  • 开启OOM自动dump

运行时检查

  • 定期查看GC日志
  • 监控堆内存使用率
  • 监控GC频率和停顿时间
  • 监控CPU占用率
  • 监控线程数

故障排查检查

  • CPU 100%:查看线程堆栈
  • 频繁GC:查看堆使用情况
  • OOM:生成堆转储,使用MAT分析
  • 栈溢出:查看递归深度
  • 线程泄漏:查看线程数

9.3 常用命令速查

命令 用途
jps 查看Java进程
jstat -gcutil <pid> 查看GC使用率
jstack <pid> 查看线程堆栈
jmap -heap <pid> 查看堆信息
jmap -dump:format=b,file=/tmp/heap.hprof <pid> 生成堆转储
jinfo <pid> 查看JVM参数
jcmd <pid> GC.heap_info 查看GC信息
jcmd <pid> GC.heap_dump /tmp/heap.hprof 生成堆转储
top -c 查看CPU占用
top -H -p <pid> 查看线程CPU占用

9.4 JVM参数配置模板

模板1:通用型Web服务(G1 GC)

bash 复制代码
-Xms8g -Xmx8g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:InitiatingHeapOccupancyPercent=45 \
-XX:G1HeapRegionSize=16m \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+UseStringDeduplication \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof \
-Xlog:gc*:file=/logs/gc.log:time,tags:filecount=10,filesize=100m

模板2:低延迟交易系统(ZGC)

bash 复制代码
-Xms32g -Xmx32g \
-XX:+UseZGC \
-XX:ZGCParallelGCThreads=8 \
-XX:ZGCConcurrentGCThreads=2 \
-XX:MaxGCPauseMillis=10 \
-XX:+AlwaysPreTouch \
-XX:MetaspaceSize=512m \
-XX:MaxMetaspaceSize=1g \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof \
-Xlog:gc*:file=/logs/gc.log:time,tags:filecount=10,filesize=100m

模板3:吞吐量优先批处理(Parallel GC)

bash 复制代码
-Xms16g -Xmx16g \
-XX:+UseParallelGC \
-XX:ParallelGCThreads=12 \
-XX:GCTimeRatio=99 \
-XX:NewRatio=2 \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof \
-Xlog:gc*:file=/logs/gc.log:time,tags:filecount=10,filesize=100m

9.5 核心记忆口诀

复制代码
JVM调优要记牢,堆大小设60-80%
初始最大要一致,GC算法选对路
小堆用Parallel,中堆用G1GC
大堆优先ZGC,延迟要求定输赢

CPU 100看线程,jstack堆栈找原因
死循环最常见,频繁GC也不差
线程阻塞看BLOCKED,锁竞争要优化

OOM分五类型,堆栈方法要分清
堆溢出生成dump,MAT分析找泄漏
栈溢出看递归,元空间看类加载
直接内存看NIO,线程数看线程池

监控工具要会用,jstatjstackjmap
JConsole和VisualVM,MAT分析堆转储
先监控后调优,数据说话不瞎搞

结语

JVM调优是一个系统工程,需要从垃圾回收、CPU、OOM、内存泄漏等多个维度综合考虑。本文涵盖了JVM调优的核心知识点和实战技巧,从理论到实践,从工具到案例,帮助你全面掌握JVM调优。

核心要点

  1. 选择合适的垃圾回收器
  2. 合理配置JVM参数
  3. 熟练使用监控工具
  4. 掌握问题排查方法
  5. 积累实战经验

参考资料与扩展阅读

相关推荐
light blue bird5 小时前
产线多并发客户端指令操作场景组件
jvm·oracle·.net·winform
bepeater12346 小时前
Laravel 10.x重磅升级:六大核心特性解析
jvm·php·laravel
Mr YiRan9 小时前
C++二义性,多态,纯虚函数和模版函数
java·jvm·c++
weisian15110 小时前
JVM--18-面试题4:为什么新生代到老年代的复制次数是 15 次?
jvm
今天你TLE了吗12 小时前
JVM学习笔记:第五章——堆内存
java·jvm·笔记·后端·学习
OnYoung1 天前
更优雅的测试:Pytest框架入门
jvm·数据库·python
蚊子码农1 天前
每日一题--JVM内存溢出分析
jvm