1.1 背景介绍
我至今还记得2020年那个凌晨3点的电话。线上系统突然卡顿,用户投诉如潮水般涌来。登上服务器一看,Full GC每隔几秒就来一次,每次停顿时间长达5秒。CPU被GC线程打满,正常业务根本没法处理。
那晚我们临时把堆内存从4G加到了8G,Full GC确实少了,但问题没有根本解决。后来花了两周时间,系统性地学习了JVM调优,把那套系统从"动不动就卡"调成了"稳如老狗"。
这篇文章就是我那两周踩坑经历的总结,希望能帮你少走一些弯路。
1.2 技术特点
JVM调优的核心目标就三个:
-
降低GC频率:减少GC发生的次数
-
缩短GC停顿:每次GC的时间要短
-
提高吞吐量:让CPU更多时间花在业务上
这三个目标有时候是互相矛盾的。比如你想降低GC频率,可能需要更大的堆,但大堆意味着Full GC时停顿更长。所以调优是个权衡的过程,没有万能配置。
JDK 21作为最新的LTS版本,在GC方面有很大改进:
-
G1 GC成为默认选择且更加成熟
-
ZGC正式转正,支持分代模式
-
虚拟线程(Virtual Threads)改变了并发编程模型
1.3 适用场景
-
Web应用响应时间敏感(电商、金融)
-
批处理任务吞吐量优先(报表、ETL)
-
大内存应用(缓存服务、搜索引擎)
-
低延迟要求(交易系统、游戏服务器)
1.4 环境要求
| 组件 | 版本 | 说明 |
|---|---|---|
| 操作系统 | Rocky Linux 9.4 / Ubuntu 24.04 LTS | 64位系统 |
| JDK | 21 LTS (21.0.4+) | 建议使用Eclipse Temurin或Amazon Corretto |
| Tomcat | 10.1.28 / 11.0.0 | Tomcat 10+需要Jakarta EE |
| 内存 | 16GB+ | 生产环境建议32GB以上 |
| CPU | 8核+ | GC线程数与CPU核心数相关 |
二、详细步骤
2.1 准备工作
2.1.1 安装JDK 21
Rocky Linux 9:
# 使用SDKMAN安装(推荐)
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 21.0.4-tem
# 或者手动安装Temurin
wget https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.4%2B7/OpenJDK21U-jdk_x64_linux_hotspot_21.0.4_7.tar.gz
tar -xzf OpenJDK21U-jdk_x64_linux_hotspot_21.0.4_7.tar.gz -C /opt/
ln -s /opt/jdk-21.0.4+7 /opt/java
# 配置环境变量
cat >> /etc/profile.d/java.sh << 'EOF'
export JAVA_HOME=/opt/java
export PATH=$JAVA_HOME/bin:$PATH
EOF
source /etc/profile.d/java.sh
# 验证
java -version
2.1.2 安装Tomcat 10.1
# 下载Tomcat
wget https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.28/bin/apache-tomcat-10.1.28.tar.gz
tar -xzf apache-tomcat-10.1.28.tar.gz -C /opt/
ln -s /opt/apache-tomcat-10.1.28 /opt/tomcat
# 创建tomcat用户
useradd -r -s /sbin/nologin tomcat
chown -R tomcat:tomcat /opt/apache-tomcat-10.1.28
# 创建systemd服务
cat > /etc/systemd/system/tomcat.service << 'EOF'
[Unit]
Description=Apache Tomcat Web Application Container
After=network.target
[Service]
Type=forking
User=tomcat
Group=tomcat
Environment="JAVA_HOME=/opt/java"
Environment="CATALINA_HOME=/opt/tomcat"
Environment="CATALINA_BASE=/opt/tomcat"
Environment="CATALINA_PID=/opt/tomcat/temp/tomcat.pid"
ExecStart=/opt/tomcat/bin/startup.sh
ExecStop=/opt/tomcat/bin/shutdown.sh
RestartSec=10
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl start tomcat
systemctl enable tomcat
2.1.3 监控工具准备
# 安装常用工具
# jstat, jmap, jstack等已包含在JDK中
# 安装arthas(阿里开源的Java诊断工具,强烈推荐)
curl -O https://arthas.aliyun.com/arthas-boot.jar
# 安装async-profiler(性能分析利器)
wget https://github.com/async-profiler/async-profiler/releases/download/v3.0/async-profiler-3.0-linux-x64.tar.gz
tar -xzf async-profiler-3.0-linux-x64.tar.gz -C /opt/
2.2 核心配置
2.2.1 理解JVM内存结构
调优之前,先搞清楚JVM内存分成哪几块:
┌─────────────────────────────────────────────────────────────┐
│ JVM 内存 │
├───────────────────────────────┬─────────────────────────────┤
│ 堆内存 │ 非堆内存 │
│ (Heap) │ (Non-Heap) │
├───────────────┬───────────────┼──────────────┬──────────────┤
│ 年轻代 │ 老年代 │ 元空间 │ 其他 │
│ (Young Gen) │ (Old Gen) │ (Metaspace) │ (栈、直接内存) │
├───────┬───────┤ │ │ │
│ Eden │ S0 │ │ │ │
│ │ S1 │ │ │ │
└───────┴───────┴───────────────┴──────────────┴──────────────┘
-
年轻代:新对象的出生地,GC频繁但速度快
-
老年代:长期存活的对象,Full GC时才清理
-
元空间:存放类信息,JDK 8后替代永久代
-
直接内存:NIO使用,不受堆大小限制
2.2.2 GC收集器选择
JDK 21支持的主要GC收集器:
| 收集器 | 特点 | 适用场景 | 启用参数 |
|---|---|---|---|
| G1 GC | 平衡吞吐量和延迟,默认选择 | 通用场景,堆4GB-64GB | -XX:+UseG1GC |
| ZGC | 超低延迟,停顿<1ms | 大内存、低延迟要求 | -XX:+UseZGC |
| Shenandoah | 类似ZGC,RedHat开发 | 低延迟要求 | -XX:+UseShenandoahGC |
| Parallel GC | 高吞吐量 | 批处理、科学计算 | -XX:+UseParallelGC |
| Serial GC | 单线程,简单 | 小堆、嵌入式 | -XX:+UseSerialGC |
我的建议:
-
堆内存小于4GB,直接用G1默认配置
-
堆内存4-32GB,G1精细调优
-
堆内存超过32GB或对延迟极其敏感,考虑ZGC
-
批处理任务不在乎延迟,用Parallel GC追求吞吐量
2.2.3 G1 GC调优参数
G1是JDK 21的默认GC,先从它开始讲:
# /opt/tomcat/bin/setenv.sh
#!/bin/bash
# 堆内存设置
JAVA_OPTS="$JAVA_OPTS -Xms4g -Xmx4g"
# G1 GC配置
JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC"
# 期望的最大停顿时间(毫秒)
JAVA_OPTS="$JAVA_OPTS -XX:MaxGCPauseMillis=200"
# 堆Region大小(1MB-32MB,建议让JVM自动选择)
# JAVA_OPTS="$JAVA_OPTS -XX:G1HeapRegionSize=4m"
# 年轻代占比(默认5%-60%自动调整)
# 固定年轻代大小有时能避免GC波动
# JAVA_OPTS="$JAVA_OPTS -XX:G1NewSizePercent=30"
# JAVA_OPTS="$JAVA_OPTS -XX:G1MaxNewSizePercent=40"
# 触发Mixed GC的老年代占比阈值
JAVA_OPTS="$JAVA_OPTS -XX:InitiatingHeapOccupancyPercent=45"
# GC线程数(默认等于CPU核心数)
# JAVA_OPTS="$JAVA_OPTS -XX:ParallelGCThreads=8"
# JAVA_OPTS="$JAVA_OPTS -XX:ConcGCThreads=2"
# 元空间
JAVA_OPTS="$JAVA_OPTS -XX:MetaspaceSize=256m"
JAVA_OPTS="$JAVA_OPTS -XX:MaxMetaspaceSize=512m"
# GC日志(JDK 9+新格式)
JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,gc+age=trace,safepoint:file=/var/log/tomcat/gc.log:time,uptime,level,tags:filecount=10,filesize=50m"
# OOM时dump堆
JAVA_OPTS="$JAVA_OPTS -XX:+HeapDumpOnOutOfMemoryError"
JAVA_OPTS="$JAVA_OPTS -XX:HeapDumpPath=/var/log/tomcat/heapdump.hprof"
export JAVA_OPTS
参数详解:
-
-Xms和-Xmx设成一样,避免堆动态扩展带来的开销 -
MaxGCPauseMillis是期望值不是硬限制,G1会尽量满足 -
InitiatingHeapOccupancyPercent太低会导致频繁GC,太高可能触发Full GC
2.2.4 ZGC调优参数
如果你的应用对延迟特别敏感,比如交易系统,可以试试ZGC:
# ZGC配置
JAVA_OPTS="$JAVA_OPTS -Xms8g -Xmx8g"
JAVA_OPTS="$JAVA_OPTS -XX:+UseZGC"
# 开启分代ZGC(JDK 21新特性,性能更好)
JAVA_OPTS="$JAVA_OPTS -XX:+ZGenerational"
# ZGC并发线程数
# JAVA_OPTS="$JAVA_OPTS -XX:ConcGCThreads=4"
# 软引用清理策略
JAVA_OPTS="$JAVA_OPTS -XX:SoftRefLRUPolicyMSPerMB=50"
# 元空间
JAVA_OPTS="$JAVA_OPTS -XX:MetaspaceSize=256m"
JAVA_OPTS="$JAVA_OPTS -XX:MaxMetaspaceSize=512m"
ZGC的特点:
-
停顿时间不随堆大小增长,几乎恒定在亚毫秒级
-
吞吐量比G1略低(大约5%-15%)
-
支持TB级堆内存
-
JDK 21的分代ZGC解决了早期版本内存占用大的问题
2.2.5 Tomcat线程池调优
JVM调好了,还得配合Tomcat的线程池:
<!-- /opt/tomcat/conf/server.xml -->
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
maxThreads="500"
minSpareThreads="50"
maxConnections="10000"
acceptCount="100"
enableLookups="false"
compression="on"
compressionMinSize="1024"
compressibleMimeType="text/html,text/xml,text/plain,text/css,application/json,application/javascript"
URIEncoding="UTF-8" />
参数说明:
| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
| maxThreads | 200 | 500 | 最大工作线程数 |
| minSpareThreads | 10 | 50 | 最小空闲线程数 |
| maxConnections | 10000 | 10000 | 最大连接数 |
| acceptCount | 100 | 100 | 等待队列长度 |
| connectionTimeout | 20000 | 20000 | 连接超时(毫秒) |
踩坑记录:maxThreads不是越大越好。我们曾经把它设成2000,结果频繁Full GC。排查发现是线程太多,每个线程的栈空间加起来就几个G,挤占了堆内存空间。
线程数计算公式:
最佳线程数 = CPU核心数 * (1 + IO等待时间/CPU计算时间)
对于IO密集型的Web应用,一般设置为CPU核心数的20-50倍比较合适。
2.3 启动和验证
2.3.1 启动Tomcat
# 设置脚本权限
chmod +x /opt/tomcat/bin/setenv.sh
# 启动
systemctl start tomcat
# 查看启动日志
tail -f /opt/tomcat/logs/catalina.out
# 确认JVM参数生效
ps aux | grep java
# 或者
jcmd $(pgrep -f tomcat) VM.flags
2.3.2 验证GC配置
# 查看GC使用情况
jstat -gc $(pgrep -f tomcat) 1000 10
# 输出示例(G1 GC):
# S0C S1C S0U S1U EC EU OC OU MC MU ...
# 0.0 0.0 0.0 0.0 262144.0 52428.8 262144.0 104857.6 ...
# 查看GC详情
jstat -gcutil $(pgrep -f tomcat) 1000
# 输出示例:
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 0.00 20.00 40.00 95.00 92.00 15 0.150 0 0.000 0.150
字段说明:
-
S0/S1:Survivor区使用率
-
E:Eden区使用率
-
O:Old区使用率
-
YGC/YGCT:Young GC次数和总耗时
-
FGC/FGCT:Full GC次数和总耗时
三、示例代码和配置
3.1 完整配置示例
3.1.1 生产级setenv.sh(G1 GC,8核16GB服务器)
#!/bin/bash
# /opt/tomcat/bin/setenv.sh
# 适用于8核16GB服务器,Web应用场景
# === 内存配置 ===
# 堆内存8GB(物理内存的50%)
JAVA_OPTS="-Xms8g -Xmx8g"
# 元空间
JAVA_OPTS="$JAVA_OPTS -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
# 直接内存限制(防止NIO用太多)
JAVA_OPTS="$JAVA_OPTS -XX:MaxDirectMemorySize=1g"
# === GC配置 ===
JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC"
JAVA_OPTS="$JAVA_OPTS -XX:MaxGCPauseMillis=200"
JAVA_OPTS="$JAVA_OPTS -XX:InitiatingHeapOccupancyPercent=45"
# G1 Region大小(堆8GB时建议4MB)
JAVA_OPTS="$JAVA_OPTS -XX:G1HeapRegionSize=4m"
# GC线程配置(8核CPU)
JAVA_OPTS="$JAVA_OPTS -XX:ParallelGCThreads=8"
JAVA_OPTS="$JAVA_OPTS -XX:ConcGCThreads=2"
# === GC日志 ===
GC_LOG_DIR="/var/log/tomcat"
mkdir -p $GC_LOG_DIR
JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,gc+age=debug,gc+heap=debug:file=${GC_LOG_DIR}/gc-%t.log:time,uptime,level,tags:filecount=10,filesize=100m"
# === OOM处理 ===
JAVA_OPTS="$JAVA_OPTS -XX:+HeapDumpOnOutOfMemoryError"
JAVA_OPTS="$JAVA_OPTS -XX:HeapDumpPath=${GC_LOG_DIR}/heapdump-%t.hprof"
JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError"
# === 性能优化 ===
# 禁用显式GC(System.gc())
JAVA_OPTS="$JAVA_OPTS -XX:+DisableExplicitGC"
# 字符串去重(节省内存,适合有大量重复字符串的应用)
JAVA_OPTS="$JAVA_OPTS -XX:+UseStringDeduplication"
# 大页内存(需要系统配置,可显著提升性能)
# JAVA_OPTS="$JAVA_OPTS -XX:+UseLargePages"
# === JMX远程监控 ===
# 生产环境建议通过跳板机访问,不要直接暴露
# JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote"
# JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=9010"
# JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"
# JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=true"
# === 其他 ===
# 时区设置
JAVA_OPTS="$JAVA_OPTS -Duser.timezone=Asia/Shanghai"
# 编码设置
JAVA_OPTS="$JAVA_OPTS -Dfile.encoding=UTF-8"
export JAVA_OPTS
echo "JAVA_OPTS: $JAVA_OPTS"
3.1.2 低延迟场景配置(ZGC)
#!/bin/bash
# 适用于对延迟敏感的场景,如交易系统
JAVA_OPTS="-Xms16g -Xmx16g"
JAVA_OPTS="$JAVA_OPTS -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
# ZGC配置
JAVA_OPTS="$JAVA_OPTS -XX:+UseZGC"
JAVA_OPTS="$JAVA_OPTS -XX:+ZGenerational"
JAVA_OPTS="$JAVA_OPTS -XX:SoftRefLRUPolicyMSPerMB=50"
# ZGC的堆大小触发GC的阈值
JAVA_OPTS="$JAVA_OPTS -XX:ZCollectionInterval=0"
# GC日志
JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:file=/var/log/tomcat/gc.log:time,uptime,level,tags:filecount=10,filesize=100m"
# OOM处理
JAVA_OPTS="$JAVA_OPTS -XX:+HeapDumpOnOutOfMemoryError"
JAVA_OPTS="$JAVA_OPTS -XX:HeapDumpPath=/var/log/tomcat/heapdump.hprof"
export JAVA_OPTS
3.1.3 高吞吐量场景配置(Parallel GC)
#!/bin/bash
# 适用于批处理、报表生成等场景
JAVA_OPTS="-Xms8g -Xmx8g"
JAVA_OPTS="$JAVA_OPTS -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
# Parallel GC配置
JAVA_OPTS="$JAVA_OPTS -XX:+UseParallelGC"
# 吞吐量目标(GC时间占比<5%)
JAVA_OPTS="$JAVA_OPTS -XX:GCTimeRatio=19"
# 最大停顿时间(毫秒)
JAVA_OPTS="$JAVA_OPTS -XX:MaxGCPauseMillis=500"
# 并行GC线程数
JAVA_OPTS="$JAVA_OPTS -XX:ParallelGCThreads=8"
# 年轻代大小(吞吐量优先可以设大一些)
JAVA_OPTS="$JAVA_OPTS -XX:NewRatio=2"
export JAVA_OPTS
3.2 实际应用案例
案例1:电商系统GC优化
问题描述: 双11大促期间,订单服务频繁卡顿,监控显示Full GC频繁,每次停顿3-5秒。
原始配置:
-Xms4g -Xmx4g -XX:+UseG1GC
问题分析:
# 使用jstat观察
jstat -gcutil $(pgrep -f order-service) 1000
# 发现Old区使用率快速增长到100%,触发Full GC
# YGC: 1500次,YGCT: 15秒
# FGC: 50次,FGCT: 180秒
# 使用jmap分析堆内存
jmap -histo:live $(pgrep -f order-service) | head -20
# 发现大量的订单对象和购物车对象
根因:
-
堆内存4GB对于高并发场景太小
-
大促期间对象创建速度远超平时
-
很多临时对象因为来不及回收被晋升到老年代
优化方案:
# 扩大堆内存到12GB
JAVA_OPTS="-Xms12g -Xmx12g"
JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC"
# 调整年轻代比例,让更多对象在年轻代被回收
JAVA_OPTS="$JAVA_OPTS -XX:G1NewSizePercent=40"
JAVA_OPTS="$JAVA_OPTS -XX:G1MaxNewSizePercent=50"
# 更积极地触发Mixed GC,避免老年代堆积
JAVA_OPTS="$JAVA_OPTS -XX:InitiatingHeapOccupancyPercent=35"
# 增加GC线程
JAVA_OPTS="$JAVA_OPTS -XX:ParallelGCThreads=16"
JAVA_OPTS="$JAVA_OPTS -XX:ConcGCThreads=4"
优化效果:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| Young GC频率 | 2次/秒 | 0.5次/秒 |
| Young GC耗时 | 10ms | 15ms |
| Full GC频率 | 1次/分钟 | 0次(观察1小时) |
| P99响应时间 | 5000ms | 200ms |
案例2:内存泄漏排查
问题描述: 应用运行一段时间后,内存持续增长,最终OOM。
排查步骤:
# 1. 开启GC日志,观察内存趋势
# 发现每次Full GC后,Old区占用都比上次高
# 2. dump堆内存
jmap -dump:format=b,file=heap.hprof $(pgrep -f myapp)
# 3. 使用MAT或VisualVM分析
# 发现大量的HttpSession对象没有被释放
# 4. 使用arthas实时分析
java -jar arthas-boot.jar
# 在arthas中执行
dashboard # 查看实时内存状态
heapdump /tmp/dump.hprof # 导出堆
sc -d *Session* # 查看Session相关类
watch org.apache.catalina.session.StandardSession invalidate '{params,returnObj,throwExp}' -x 2
问题根因:
-
Session超时时间设置过长(默认30分钟)
-
用户退出后Session没有主动销毁
-
存在Session对象引用链导致无法回收
解决方案:
<!-- web.xml -->
<session-config>
<session-timeout>15</session-timeout>
</session-config>
// 用户登出时主动销毁Session
@PostMapping("/logout")
public void logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
}
案例3:Metaspace溢出
问题描述:
java.lang.OutOfMemoryError: Metaspace
排查:
# 查看Metaspace使用情况
jstat -gcmetacapacity $(pgrep -f myapp)
# 使用arthas查看加载的类
classloader -l # 列出所有ClassLoader
classloader -t # 以树状展示
sc -d * # 列出所有类
问题根因:
-
使用了动态代理、CGLib、反射等大量生成类
-
类加载器泄漏(常见于热部署场景)
-
第三方框架频繁加载类
解决方案:
# 增大Metaspace
JAVA_OPTS="$JAVA_OPTS -XX:MetaspaceSize=512m"
JAVA_OPTS="$JAVA_OPTS -XX:MaxMetaspaceSize=1g"
# 开启类卸载
JAVA_OPTS="$JAVA_OPTS -XX:+ClassUnloadingWithConcurrentMark"
四、最佳实践和注意事项
4.1 最佳实践
1. 堆内存设置原则
-
Xms和Xmx设成一样,避免动态扩展
-
堆内存不要超过物理内存的70%
-
留足够的空间给元空间、直接内存、线程栈
物理内存分配示例(32GB服务器):
- 堆内存:20GB
- 元空间:512MB
- 直接内存:2GB
- 系统和其他:约10GB
2. GC选择流程图
开始
│
堆内存大小?
/ \
<4GB ≥4GB
│ │
G1默认 延迟敏感?
/ \
是 否
│ │
ZGC 吞吐优先?
/ \
是 否
│ │
Parallel G1调优
3. 监控告警设置
建议设置以下告警:
-
Old区使用率 > 80% 持续5分钟
-
Full GC次数 > 0(或根据业务设置阈值)
-
GC总耗时占比 > 5%
-
Metaspace使用率 > 90%
4. 定期性能评估
每周review一次GC日志,关注:
-
GC频率趋势
-
平均停顿时间
-
内存增长趋势
-
异常时间点分析
4.2 注意事项
| 常见错误 | 原因分析 | 解决方案 |
|---|---|---|
| Xms和Xmx不一致 | 堆动态扩展带来性能波动 | 设置成相同值 |
| MaxGCPauseMillis设太小 | G1无法满足目标,频繁GC | 设置合理值(如200ms) |
| 忽略GC日志 | 无法分析GC行为 | 始终开启GC日志 |
| Metaspace不设上限 | 类加载失控导致OOM | 设置MaxMetaspaceSize |
| 堆设置过大 | 单次GC时间过长 | 根据实际情况调整 |
| 使用System.gc() | 触发Full GC影响性能 | 禁用显式GC |
| 线程数过多 | 线程栈内存消耗大 | 控制线程池大小 |
| 忽略直接内存 | NIO导致OOM | 设置MaxDirectMemorySize |
五、故障排查和监控
5.1 故障排查
5.1.1 常用命令速查
# 查看Java进程
jps -lv
# 查看GC统计
jstat -gc PID 1000 10
jstat -gcutil PID 1000 10
jstat -gccause PID 1000
# 查看堆内存对象
jmap -histo PID | head -30
jmap -histo:live PID | head -30 # 触发Full GC后统计
# 导出堆快照
jmap -dump:format=b,file=heap.hprof PID
# 查看线程栈
jstack PID > thread.txt
# 查看JVM参数
jcmd PID VM.flags
# 查看系统属性
jcmd PID VM.system_properties
5.1.2 GC日志分析
JDK 21的GC日志格式:
[2025-01-07T10:30:15.123+0800][12.345s][info][gc] GC(100) Pause Young (Normal) (G1 Evacuation Pause) 1024M->256M(4096M) 15.678ms
[2025-01-07T10:30:15.123+0800][12.345s][info][gc,heap] GC(100) Eden regions: 200->0(180)
[2025-01-07T10:30:15.123+0800][12.345s][info][gc,heap] GC(100) Survivor regions: 20->25(30)
[2025-01-07T10:30:15.123+0800][12.345s][info][gc,heap] GC(100) Old regions: 50->55
关键指标解读:
-
Pause Young:年轻代GC -
1024M->256M(4096M):GC前后堆使用量和总大小 -
15.678ms:GC停顿时间
使用工具分析:
# GCViewer(图形化分析)
java -jar gcviewer.jar gc.log
# GCEasy(在线分析)
# 上传gc.log到 https://gceasy.io/
5.1.3 使用Arthas排查
Arthas是阿里开源的Java诊断神器,强烈推荐:
# 启动arthas
java -jar arthas-boot.jar
# 选择要attach的Java进程
# 常用命令
dashboard # 实时仪表板
thread # 查看线程
thread -n 3 # 最忙的3个线程
jvm # JVM信息
memory # 内存信息
heapdump /tmp/dump.hprof # 导出堆
profiler start # 开始CPU分析
profiler stop # 停止并生成火焰图
5.2 性能监控
5.2.1 Prometheus + Grafana监控
使用JMX Exporter暴露JVM指标:
# 下载JMX Exporter
wget https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.20.0/jmx_prometheus_javaagent-0.20.0.jar
# 创建配置文件
cat > /opt/tomcat/conf/jmx_exporter.yaml << 'EOF'
lowercaseOutputName: true
lowercaseOutputLabelNames: true
rules:
- pattern: ".*"
EOF
# 在setenv.sh中添加
JAVA_OPTS="$JAVA_OPTS -javaagent:/opt/tomcat/lib/jmx_prometheus_javaagent-0.20.0.jar=9404:/opt/tomcat/conf/jmx_exporter.yaml"
Prometheus配置:
scrape_configs:
- job_name: 'tomcat'
static_configs:
- targets: ['tomcat-server:9404']
关键监控指标:
| 指标 | PromQL | 告警阈值 |
|---|---|---|
| 堆使用率 | jvm_memory_bytes_used{area="heap"} / jvm_memory_bytes_max{area="heap"} |
> 80% |
| GC时间占比 | rate(jvm_gc_collection_seconds_sum[5m]) |
> 0.05 |
| GC频率 | rate(jvm_gc_collection_seconds_count[5m]) |
根据业务定 |
| 线程数 | jvm_threads_current |
> 500 |
5.2.2 告警规则配置
groups:
- name: jvm_alerts
rules:
- alert: JVMHeapUsageHigh
expr: jvm_memory_bytes_used{area="heap"} / jvm_memory_bytes_max{area="heap"} > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "JVM堆内存使用率过高"
description: "{{ $labels.instance }} 堆内存使用率 {{ $value | humanizePercentage }}"
- alert: JVMFullGC
expr: increase(jvm_gc_collection_seconds_count{gc="G1 Old Generation"}[5m]) > 0
labels:
severity: critical
annotations:
summary: "发生Full GC"
description: "{{ $labels.instance }} 发生Full GC"
- alert: JVMGCTimeRatioHigh
expr: rate(jvm_gc_collection_seconds_sum[5m]) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "GC时间占比过高"
5.3 备份与恢复
5.3.1 定期堆快照备份
#!/bin/bash
# /usr/local/bin/jvm_snapshot.sh
PID=$(pgrep -f tomcat)
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/data/backup/jvm"
mkdir -p $BACKUP_DIR
# 导出堆快照(会触发Full GC,慎用)
# jmap -dump:format=b,file=${BACKUP_DIR}/heap_${DATE}.hprof $PID
# 导出线程快照(不影响性能)
jstack $PID > ${BACKUP_DIR}/thread_${DATE}.txt
# 导出GC日志
cp /var/log/tomcat/gc.log ${BACKUP_DIR}/gc_${DATE}.log
# 保留7天
find $BACKUP_DIR -mtime +7 -delete
5.3.2 OOM自动处理
# setenv.sh中配置
JAVA_OPTS="$JAVA_OPTS -XX:+HeapDumpOnOutOfMemoryError"
JAVA_OPTS="$JAVA_OPTS -XX:HeapDumpPath=/var/log/tomcat/heapdump.hprof"
JAVA_OPTS="$JAVA_OPTS -XX:OnOutOfMemoryError='/opt/scripts/oom_handler.sh %p'"
OOM处理脚本:
#!/bin/bash
# /opt/scripts/oom_handler.sh
PID=$1
DATE=$(date +%Y%m%d_%H%M%S)
# 记录日志
echo "[$DATE] OOM detected for PID: $PID" >> /var/log/tomcat/oom.log
# 发送告警
curl -X POST 'https://webhook.example.com/alert' \
-H 'Content-Type: application/json' \
-d '{"message": "Tomcat OOM, PID: '$PID'"}'
# 保存现场(可选)
jstack $PID > /var/log/tomcat/oom_thread_${DATE}.txt 2>/dev/null
# 重启服务(可选,根据业务决定)
# systemctl restart tomcat
六、总结
6.1 技术要点回顾
JVM调优不是一蹴而就的事情,需要:
-
理解内存结构:知道年轻代、老年代、元空间的作用
-
选对GC收集器:G1适合大多数场景,ZGC追求低延迟,Parallel追求吞吐量
-
合理配置参数:堆大小、GC目标、线程数等
-
持续监控分析:GC日志、Prometheus监控、定期review
-
问题快速定位:掌握jstat、jmap、jstack、arthas等工具
调优效果评估标准:
-
Young GC:频率适中(不要太频繁),停顿时间<50ms
-
Full GC:尽量避免,如果有则停顿时间<500ms
-
GC总耗时:占比<5%
-
响应时间:P99满足业务要求
6.2 进阶学习方向
-
深入GC算法:三色标记、SATB、Remember Set等
-
JVM内部原理:即时编译、逃逸分析、锁优化
-
性能工程:系统化的性能测试、分析、优化方法论
-
云原生Java:GraalVM、Native Image、容器环境调优
6.3 参考资料
-
Oracle JDK 21文档: https://docs.oracle.com/en/java/javase/21/
-
G1 GC调优指南: https://docs.oracle.com/en/java/javase/21/gctuning/
-
ZGC官方文档: https://wiki.openjdk.org/display/zgc
-
Arthas用户文档: https://arthas.aliyun.com/doc/
附录
A. 命令速查表
| 命令 | 说明 |
|---|---|
jps -lv |
列出Java进程 |
jstat -gc PID |
查看GC统计 |
jmap -histo PID |
查看对象统计 |
jmap -dump:format=b,file=heap.hprof PID |
导出堆快照 |
jstack PID |
导出线程栈 |
jcmd PID VM.flags |
查看JVM参数 |
jcmd PID GC.run |
触发Full GC |
jcmd PID VM.native_memory |
查看本地内存 |
B. 配置参数详解
| 参数 | 说明 | 建议值 |
|---|---|---|
| -Xms | 初始堆大小 | 与-Xmx相同 |
| -Xmx | 最大堆大小 | 物理内存50%-70% |
| -XX:MetaspaceSize | 元空间初始大小 | 256m |
| -XX:MaxMetaspaceSize | 元空间最大值 | 512m-1g |
| -XX:MaxGCPauseMillis | G1目标停顿时间 | 200 |
| -XX:InitiatingHeapOccupancyPercent | 触发混合GC阈值 | 45 |
| -XX:G1HeapRegionSize | G1 Region大小 | 自动或4m-32m |
| -XX:ParallelGCThreads | 并行GC线程数 | CPU核心数 |
| -XX:ConcGCThreads | 并发GC线程数 | ParallelGCThreads/4 |
C. 术语表
| 术语 | 解释 |
|---|---|
| GC | Garbage Collection,垃圾收集 |
| STW | Stop The World,GC时暂停所有应用线程 |
| Young GC | 年轻代垃圾收集,也叫Minor GC |
| Full GC | 全堆垃圾收集,也叫Major GC |
| Mixed GC | G1特有,同时收集年轻代和部分老年代 |
| TLAB | Thread Local Allocation Buffer,线程本地分配缓冲 |
| SATB | Snapshot At The Beginning,G1并发标记算法 |
| Region | G1将堆分成的固定大小区域 |
| Humongous | G1中超过Region 50%的大对象 |
| Metaspace | 元空间,存储类元数据 |