Kevin's blog Kevin's blog
首页
  • AI基础
  • RAG技术
  • 提示词工程
  • Wireshark抓包
  • 常见问题
  • 数据库
  • 代码技巧
  • 浏览器
  • 手册教程
  • 技术应用
  • 流程规范
  • github技巧
  • git笔记
  • vpn笔记
  • 知识概念
  • 学习笔记
  • 环境搭建
  • linux&运维
  • 微服务
  • 经验技巧
  • 实用手册
  • arthas常用
  • spring应用
  • javaAgent技术
  • 网站
友情链接
  • 分类
  • 标签
  • 归档

Kevin

你可以迷茫,但不可以虚度
首页
  • AI基础
  • RAG技术
  • 提示词工程
  • Wireshark抓包
  • 常见问题
  • 数据库
  • 代码技巧
  • 浏览器
  • 手册教程
  • 技术应用
  • 流程规范
  • github技巧
  • git笔记
  • vpn笔记
  • 知识概念
  • 学习笔记
  • 环境搭建
  • linux&运维
  • 微服务
  • 经验技巧
  • 实用手册
  • arthas常用
  • spring应用
  • javaAgent技术
  • 网站
友情链接
  • 分类
  • 标签
  • 归档
  • JVM性能调优

    • JVM类的加载机制
    • JVM内存模型
    • JVM对象创建与内存分配机制
    • JVM垃圾收集算法
    • JVM垃圾收集器
    • JVM调优工具以及调优实战
    • Class常量池与运行时常量池
    • arthas详解
    • JVM调优经验
      • 上线前与取证基线(所有场景通用)
      • 场景 1:Young GC 频繁(抖动明显)
      • 场景 2:Full GC 频繁 / 老年代压力大
      • 场景 3:堆占用稳步上升 / 周期性 Full GC / OOM
      • 场景 4:CPU 飙高(但未必是 GC)
      • 场景 5:Metaspace 撑爆 / 触发 Full GC
      • 场景 6:停顿长 / 卡顿明显(STW 问题)
      • 场景 7:吞吐上不去(GC 占比高)
      • 场景 8:容器内存打架(RSS>cgroup,进程被 OOMKill)
      • 一键排查脚本(复制即用)
      • 验证与固化(所有场景收尾动作)
    • 字节码与操作数栈
    • GCLog分析
    • jdk17新特性
    • JVM 内存分析工具 MAT及实践
    • JVM工厂运行说明书
    • Oracle:JVM & G1垃圾收集器
    • JVM学习总结
  • 并发编程

  • MySql

  • spring

  • redis

  • zookeeper

  • rabbitMQ

  • 架构

  • 锁

  • 分库分表

  • 学习笔记
  • JVM性能调优
kevin
2025-10-19
目录

JVM调优经验

# 上线前与取证基线(所有场景通用)

启动基线(容器/云原生,JDK8+)

# 容器感知 + 内存按百分比分配 + 必备取证
-XX:+UseContainerSupport \
-XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=50 -XX:MinRAMPercentage=50 \
-Xss512k \
-XX:MetaspaceSize=200M -XX:MaxMetaspaceSize=400M \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/ \
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution \
-XX:+PrintHeapAtGC -XX:+PrintReferenceGC -XX:+PrintGCApplicationStoppedTime \
-Xloggc:/logs/gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=14 -XX:GCLogFileSize=100M \
-Dfile.encoding=UTF-8 -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true
# 建议垃圾收集器:G1(JDK11+ 默认),低停顿可评估 ZGC(JDK17+)
1
2
3
4
5
6
7
8
9
10
11

线上低侵入取证 4 连

jps -l                               # 确认 PID
jstat -gc <pid> 1000 10              # 1 秒采样 10 次看 GC 态势
top -Hp <pid> -b -n 1 | head -n 30   # 线程 CPU Top
jinfo -flags <pid> && jmap -histo <pid> | head -n 50
1
2
3
4

配合工具:🧰 ⛳️ Arthas、📊 ⛳️ GC 日志阅读要点

分析的理论依据与判读要点

  • 为什么一定要打开年龄分布与停顿日志 依托“动态年龄判断(Dynamic Tenuring)”机制,JVM 会根据 Survivor 压力动态提前晋升;如果没有 PrintTenuringDistribution,就无法判断“是对象真的老了,还是被迫提前晋升”。 同理,PrintGCApplicationStoppedTime 能把“感知到的卡顿”与 STW 停顿精确挂钩,避免把业务抖动误判成 GC 问题。
  • 容器百分比分配的理论基础 容器内总内存 = 堆 + 线程栈 + 直接内存 + JIT 代码缓存 + Metaspace + 其他本地内存。用 MaxRAMPercentage/InitialRAMPercentage 是为本地内存留出生存空间,降低 cgroup 下的 OOMKill 风险。
  • 四连取证能快速建立“证据闭环” jstat -gc 看宏观趋势(YGC/FGC、OU 斜率)→ top -Hp 定位热点线程 → jinfo 还原真实启动参数(很多问题是“被调参”导致)→ jmap -histo 识别“谁在吃内存”。

# 场景 1:Young GC 频繁(抖动明显)

识别

  • jstat -gc 里 YGC 快速累加、Eden 使用 EU 常接近 EC;平均 YGCT/YGC 不高但次数过多。

先看

  • 对象瞬时创建速率、Eden/Survivor 是否太小、是否有大批量短命对象(JSON/日志/DTO)。

命令

jstat -gc <pid> 1000 20                 # 看 YGC 增速、EU/EC 波动
jmap -histo <pid> | head -n 50          # Top 类;短命对象明显
1
2

判断 → 快速修复

  • Eden 太小 → 适度放大年轻代(G1 下提升总堆或 MaxRAMPercentage;或显式 -Xmn)。
  • Survivor 顶不住 → 调整晋升阈值/Survivor 比例,避免过早晋升。
  • 业务侧批量创建 → 分批/复用缓冲、减少临时对象(JSON/日志降频、StringBuilder 复用)。

可落地参数

# G1:更偏向吞吐的年轻代设置(先小步)
-XX:MaxRAMPercentage=60
-XX:SurvivorRatio=8 -XX:TargetSurvivorRatio=80
1
2
3

验证指标

  • 观察 1 个高峰周期:YGC 次数下降≥30%、P95 延迟更稳。

分析的理论依据与判读要点

  • 理论钩子:Eden 太小/瞬时分配速率高 → Minor GC 触发频繁;Survivor 目标占用(TargetSurvivorRatio)过低会放大提前晋升,进一步加重后续 GC 压力。
  • 关键证据怎么看:
    • jstat -gc 中 EU/EC 长时间贴近,YGC 计数增长快且单次 YGCT 不高。
    • PrintTenuringDistribution 里年龄低的段位被频繁晋升(S 区压力导致动态年龄提前)。
  • 为什么这些调参有效:
    • 适度放大年轻代(尤其 Eden)降低 Minor 触发频率;
    • 把 TargetSurvivorRatio 提到 75~85 能增加 S 区的“缓冲带”,减少“被迫晋升”。
  • 现场小技巧:估算晋升量(见文末通用公式),若“YGC 多 + 晋升量也大”,优先从 Survivor/S 配比 & 阈值 下手,而不是盲目增堆。

# 场景 2:Full GC 频繁 / 老年代压力大

识别

  • FGC 快速增加;OU 持续上升;日志见频繁 Full GC 或 Mixed GC 占比异常。

先看

  • 是否有 System.gc()、老年代晋升量、Metaspace 是否顶格。

命令

jstat -gc <pid> 1000 20
jinfo -flags <pid> | grep -i ExplicitGC || true
jmap -histo <pid> | head -n 50
1
2
3

判断 → 快速修复

  • 有显式 GC → 禁用:-XX:+DisableExplicitGC。
  • 晋升过快 → 放大年轻代/Survivor,减少进入老年代的流量。
  • 老年代被长生对象占满 → 排查缓存/集合上限;必要时分层缓存(本地 + Redis),给本地缓存上限+淘汰策略。

可落地参数

-XX:+DisableExplicitGC
# G1 提前并发标记、减少 Full GC 概率(先小步)
-XX:InitiatingHeapOccupancyPercent=40
1
2
3

验证指标

  • 1 小时窗口 FGC≈0 或极少;老年代增长斜率下降。

分析的理论依据与判读要点

  • 理论钩子:
    1. 空间担保(Promotion Guarantee):Minor 结束前要确保老年代能接住待晋升对象,否则晋升失败→Full GC/退化收集;
    2. 过早晋升:S 区扛不住,年轻对象涌入老年代,导致 OU 斜率变陡。
  • 关键证据怎么看:
    • GC 日志里 Promotion failed / Allocation Failure、FGC cause;
    • OU(老年代使用量)在每次 Minor 后阶梯式抬升;
    • jmap -histo 出现大量中寿命对象(非真正“老”对象)。
  • 为什么这些调参有效:
    • 禁用显式 GC可去掉“人为制造”的 Full GC;
    • 提升 Survivor 承载 + 延后晋升阈值能直接削弱晋升流量;
    • G1 的 IHOP(InitiatingHeapOccupancyPercent)下调到 ~40-45% 可更早启动并发标记,降低被动 Full GC 概率。

# 场景 3:堆占用稳步上升 / 周期性 Full GC / OOM

识别

  • 堆曲线只升不降;Full GC 后回收有限;最终 OOM: Java heap space。

先看

  • 是否存在集合/缓存无限增长、异步堆积、队列无上限等模式。

命令

# 连续对比对象直方图
for i in {1..3}; do jmap -histo <pid> | head -n 30 | tee /tmp/histo-$i.txt; sleep 60; done
# 必要时抓堆(高峰前后各一次)
jmap -dump:format=b,file=/tmp/heap-$(date +%s).hprof <pid>
1
2
3
4

判断 → 快速修复

  • Top 类是集合(ArrayList/HashMap/ConcurrentHashMap)→ 加上限 + 淘汰;或换 Caffeine/Ehcache。
  • 某业务批处理累积 → 切片处理(分页/流式)。
  • 反序列化对象体量巨大 → 引入压缩/流式解析,避免一次性 materialize。

验证指标

  • Full GC 后可回收比率↑;老年代“锯齿”恢复;OOM 消失。

分析的理论依据与判读要点

  • 理论钩子:这是“活跃度提升或泄漏”的经典特征,与回收器无关;Full GC 后回收有限说明堆里“真有活对象”。
  • 关键证据怎么看:
    • 多次 jmap -histo 做“差分对比”,若某些集合类(ArrayList/HashMap/C.H.M.)持续增长,基本可判定是无限增长容器/缓存无上限;
    • 若对象体量巨大(大数组/大字节串),考虑大对象策略(见场景 4/6 的 Humongous)。
  • 为什么这些动作有效:
    • 给缓存/队列加上限与淘汰策略,从“源头”降低活跃对象的基数;
    • 通过流式/分页把“尖峰物化”为“细水长流”。

# 场景 4:CPU 飙高(但未必是 GC)

识别

  • load/cpu 高,GC 指标正常;多半是热方法/死循环/锁竞争。

先看

  • 哪些线程在忙、热点方法在哪、是否有频繁 GC 安全点或日志 I/O。

命令

top -Hp <pid> -b -n 1 | head -n 20              # 找到高 CPU TID(十进制)
printf "0x%x\n" <tid>                            # 转十六进制
jstack <pid> | grep -A30 <hex_tid>               # 精确到代码行
# 或直接用 Arthas
# as.sh -> thread -n 5; profiler start; <压测或等待>; profiler stop
1
2
3
4
5

判断 → 快速修复

  • 业务死循环/热方法 → 修正逻辑;必要时限流/降级。
  • 序列化/日志热点 → 降采样/降级日志级别,复用对象(缓冲区/ThreadLocal)。
  • 自旋/锁竞争 → 降低锁粒度/缩短临界区;考虑无锁结构。

验证指标

  • 线程 Top 平稳;CPU 恢复;P95 延迟下降。

分析的理论依据与判读要点

  • 理论钩子:GC 并非万金油。高 CPU 很多是业务自旋/热循环/锁竞争/序列化热点。
  • 关键证据怎么看:
    • GC 指标正常(YGCT/FGCT 占比不高),但 top -Hp 显示单线程占用高;
    • jstack 定位到具体方法或锁等待;
    • 若 GC 相关,通常会伴随 Safepoint 尖刺或 Remark/Mixed 高占比。
  • 为什么这些动作有效:
    • 热点在业务,靠“降采样/限流/缩短临界区/复用缓冲区”比调 GC 参数更立竿见影。

# 场景 5:Metaspace 撑爆 / 触发 Full GC

识别

  • 报错 OutOfMemoryError: Metaspace 或 GC 日志显示 Metaspace 接近上限并触发 Full GC。

先看

  • 是否热加载频繁、动态代理/字节码生成泛滥、类卸载受阻。

命令

jstat -gcmetacapacity <pid> 1000 10
jinfo -flags <pid> | egrep -i "MetaspaceSize|MaxMetaspaceSize"
1
2

判断 → 快速修复

  • 调大初始/最大 Metaspace(先小步);排查框架(频繁生成代理类/脚本引擎)。
  • 关闭无意义的热加载;定期重用 ClassLoader(插件/脚本场景)。

可落地参数

-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=512M
1

验证指标

  • Metaspace 使用稳定,相关 Full GC 消失。

分析的理论依据与判读要点

  • 理论钩子:Metaspace 用尽同样会触发 Full GC,但这不是“老年代满”,而是类元数据(类加载、动态代理、脚本引擎)的问题。
  • 关键证据怎么看:
    • jstat -gcmetacapacity 曲线顶格;FGC 原因与 Metaspace 相关;
    • 结合业务时间点,是否有热加载/代理类不断生成。
  • 为什么这些动作有效:
    • 增大 MetaspaceSize/MaxMetaspaceSize 只是“腾时间”,根因是ClassLoader 不释放或代理泛滥;
    • 稳态系统尽量避免频繁热加载,插件/脚本用可控生命周期的 ClassLoader。

# 场景 6:停顿长 / 卡顿明显(STW 问题)

识别

  • 用户体感“卡”;GC 日志有长时间停顿;PrintGCApplicationStoppedTime 有尖刺。

先看

  • 是否频繁 Full GC / Remark;是否并发阶段遭遇晋升失败;Safepoint 触发频繁。

命令

# 先开启/确认 GC 日志(见基线)
grep -E "Pause|Stopped" /logs/gc-*.log | tail -n 100
jstack <pid> > /tmp/jstack-$(date +%s).txt
1
2
3

判断 → 快速修复

  • 切换/优化回收器:优先 G1;极致低停顿评估 ZGC(JDK17+)。
  • 减少大对象突发:分批、批次限额;调大 -XX:G1HeapRegionSize(适度)有时能减少 humongous。
  • 热路径减少 synchronized 长临界区;I/O 超时设置合理。

验证指标

  • P95/P99 停顿显著下降;长尾抖动消失。

分析的理论依据与判读要点

  • 理论钩子:
    • 长停顿常见于 Full GC、Remark、Mixed 回收负载重;
    • Humongous(G1 下≥一半 Region 的对象)会带来额外的标记/回收复杂度,间接放大停顿。
  • 关键证据怎么看:
    • PrintGCApplicationStoppedTime 出现尖峰;
    • GC 日志出现“Humongous allocation/reclaim”密集记录,或 Mixed/Remark 时间异常长。
  • 为什么这些动作有效:
    • 业务侧切片/控峰是第一优先级;
    • 适度增大 G1HeapRegionSize 可提高“成为 Humongous”的阈值,但要监控回收效率变化;
    • ZGC 适合极低停顿 SLA,但迁移成本与观测手段要同步跟上。

# 场景 7:吞吐上不去(GC 占比高)

识别

  • GC 总耗时占比高;吞吐低。

先看

  • GC 算法是否匹配目标(低停顿 vs 吞吐)、并行度是否拉满。

命令

jstat -gc <pid> 1000 10             # (Y|F)GCT 累计/比例
jinfo -flags <pid> | egrep -i "UseG1GC|ParallelGCThreads|ConcGCThreads"
1
2

判断 → 快速修复

  • 吞吐优先:并行度到位(核数一半到等同核数),适当增大堆,降低 GC 频率。
  • 若是 G1:调 -XX:MaxGCPauseMillis=200~500 让收集器更偏吞吐;并确认 ParallelGCThreads/ConcGCThreads 合理。

可落地参数

-XX:ParallelGCThreads=<cpu/2~cpu> -XX:ConcGCThreads=<ParallelGCThreads/4~1/2>
-XX:MaxGCPauseMillis=300
1
2

验证指标

  • GC 占比下降;TPS/QPS 提升。

分析的理论依据与判读要点

  • 理论钩子:暂停目标越紧(如 G1 的 MaxGCPauseMillis 很低),年轻代越小、回收越勤,GC 开销比例越高;并行线程数不足也会拖吞吐。
  • 关键证据怎么看:
    • (Y|F)GCT/进程运行时长 比例偏高;
    • ParallelGCThreads/ConcGCThreads 与 CPU 不匹配。
  • 为什么这些调参有效:
    • 放宽暂停目标让 G1 更偏向吞吐、扩大年轻代;
    • 合理配置并行/并发 GC 线程让 CPU 利用率更高。

# 场景 8:容器内存打架(RSS>cgroup,进程被 OOMKill)

识别

  • Pod/容器被 OOMKill;Java 内部看起来还有余量。

先看

  • 是否用固定 -Xms/-Xmx 抢满内存;本地内存(DirectBuffer、线程栈、JNI)是否超出预期。

命令

# Java 堆 vs 进程 RSS
jcmd <pid> VM.native_memory summary | head -n 50   # JDK11+ NMT(若可用)
jinfo -flags <pid> | egrep "Xms|Xmx|MaxRAMPercentage|MaxDirectMemorySize|Xss"
1
2
3

判断 → 快速修复

  • 改用 MaxRAMPercentage 按配额分配堆;降低 -Xss(例如 512k);限制直接内存:-XX:MaxDirectMemorySize。
  • 评估线程数(池大小、Tomcat/Netty),避免线程栈撑爆。

可落地参数

-XX:MaxRAMPercentage=50 -XX:InitialRAMPercentage=50 -Xss512k -XX:MaxDirectMemorySize=256m
1

验证指标

  • 进程 RSS < cgroup 限额,OOMKill 消失。

分析的理论依据与判读要点

  • 理论钩子:cgroup 下的 OOM 与 Java 堆不一定相关,更多发生在本地内存(线程栈、直接内存、JNI、代码缓存)。
  • 关键证据怎么看:
    • 堆看似有余量,但容器仍 OOMKill;jinfo 显示 -Xms/-Xmx 抢满内存或 -Xss 偏大;
    • 线程数偏多(每个线程栈默认 1M 甚至更大)。
  • 为什么这些动作有效:
    • MaxRAMPercentage 为非堆留出生存空间;
    • -Xss 降到 512k 对常见 Web 服务足够;
    • 限制 MaxDirectMemorySize,避免 Netty/IO 跑飞。

# 一键排查脚本(复制即用)

#!/usr/bin/env bash
set -euo pipefail
PID=${1:-$(jps -l | awk '/\.jar|org\.springframework|tomcat|jetty/{print $1; exit}')}
ts(){ date "+%F %T"; }

echo "[$(ts)] PID=$PID"
echo -e "\n== jstat -gc (1s*10) =="; jstat -gc $PID 1000 10
echo -e "\n== top threads =="; top -Hp $PID -b -n 1 | head -n 30
echo -e "\n== jinfo flags =="; jinfo -flags $PID
echo -e "\n== jmap histo TOP50 =="; jmap -histo $PID | head -n 50
echo -e "\n== gc last 200 lines =="; ls -1t /logs/gc-*.log 2>/dev/null | head -n 1 | xargs -r tail -n 200 || echo "no gc log"
1
2
3
4
5
6
7
8
9
10
11

分析的理论依据与判读要点

  • 输出顺序等同“先宏观后微观”的证据闭环:
    1. jstat -gc 判断“谁在往老年代灌”;
    2. top -Hp 识别是否业务热点导致假性“卡”;
    3. jinfo 还原真实调参(很多线上问题来自“默认值误解/运维模板误改”);
    4. jmap -histo 锁定增长最快的类型族群。
  • 建议把脚本扩展一行晋升量估算(如下公式),形成“晋升量曲线”。

通用晋升量估算(贴到博客显眼处)

本轮近似晋升量(MB) ≈ 年轻代释放量 − 堆整体释放量 读一次 Minor GC 汇总:

  • 年轻代释放量 = “PSYoungGen: A→B(C)” 的 A−B
  • 堆整体释放量 = “Heap: X→Y(Z)” 的 X−Y 大于 0 说明老年代被“灌入”了这么多对象;连成时间序列,就得到晋升量曲线。

# 验证与固化(所有场景收尾动作)

  • 指标对比(变更前后同一负载 30–60 分钟):
    • GC:YGC/FGC 次数与平均耗时、最大停顿、老年代斜率。
    • 业务:P50/P95/P99 延迟、吞吐(QPS/TPS)。
  • 小步快跑:一次只改一个维度(堆/年轻代/阈值/并行度/代码),保留回滚。
  • 沉淀基线:把 final 参数与编码规范(批处理切片、缓存上限、日志降噪)写进团队模板。

分析的理论依据与判读要点

  • 只改一件事就能建立“因果对应”:堆/新生代/阈值/并行度/代码,每次只动一个旋钮,观察 30–60 分钟,形成A/B 对照;
  • 指标分三类对齐:
    • GC 侧:YGC/FGC 次数、YGCT/FGCT 总占比、最长停顿、OU 斜率、晋升量曲线;
    • 业务侧:P50/P95/P99、QPS/TPS;
    • 资源侧:RSS、线程数、DirectMemory 使用。
  • 把“理论→证据→动作→结果”写进团队模板:例如“新服务基线参数 + 日志开关 + 排查脚本 + 回滚点”,形成可复用的“上线包”。
上次更新: 2025/10/14, 20:08:40
arthas详解
字节码与操作数栈

← arthas详解 字节码与操作数栈→

最近更新
01
提示词工程实践指南
10-19
02
chatGpt提示原则
10-19
03
AI是如何学习的
10-19
更多文章>
| Copyright © 2022-2025 Kevin | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式