Tomcat JVM调优实战:从频繁GC到稳定运行的蜕变

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

# 发现大量的订单对象和购物车对象

根因

  1. 堆内存4GB对于高并发场景太小

  2. 大促期间对象创建速度远超平时

  3. 很多临时对象因为来不及回收被晋升到老年代

优化方案

复制代码
# 扩大堆内存到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调优不是一蹴而就的事情,需要:

  1. 理解内存结构:知道年轻代、老年代、元空间的作用

  2. 选对GC收集器:G1适合大多数场景,ZGC追求低延迟,Parallel追求吞吐量

  3. 合理配置参数:堆大小、GC目标、线程数等

  4. 持续监控分析:GC日志、Prometheus监控、定期review

  5. 问题快速定位:掌握jstat、jmap、jstack、arthas等工具

调优效果评估标准

  • Young GC:频率适中(不要太频繁),停顿时间<50ms

  • Full GC:尽量避免,如果有则停顿时间<500ms

  • GC总耗时:占比<5%

  • 响应时间:P99满足业务要求

6.2 进阶学习方向

  1. 深入GC算法:三色标记、SATB、Remember Set等

  2. JVM内部原理:即时编译、逃逸分析、锁优化

  3. 性能工程:系统化的性能测试、分析、优化方法论

  4. 云原生Java:GraalVM、Native Image、容器环境调优

6.3 参考资料

附录

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 元空间,存储类元数据
相关推荐
小北方城市网5 小时前
Redis 分布式锁高可用实现:从原理到生产级落地
java·前端·javascript·spring boot·redis·分布式·wpf
六义义6 小时前
java基础十二
java·数据结构·算法
ʚB҉L҉A҉C҉K҉.҉基҉德҉^҉大6 小时前
自动化机器学习(AutoML)库TPOT使用指南
jvm·数据库·python
毕设源码-钟学长7 小时前
【开题答辩全过程】以 基于SpringBoot的智能书城推荐系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
笨手笨脚の7 小时前
深入理解 Java 虚拟机-03 垃圾收集
java·jvm·垃圾回收·标记清除·标记复制·标记整理
莫问前路漫漫7 小时前
WinMerge v2.16.41 中文绿色版深度解析:文件对比与合并的全能工具
java·开发语言·python·jdk·ai编程
九皇叔叔7 小时前
【03】SpringBoot3 MybatisPlus BaseMapper 源码分析
java·开发语言·mybatis·mybatis plus
挖矿大亨7 小时前
c++中的函数模版
java·c++·算法
dyyx1117 小时前
使用Scikit-learn进行机器学习模型评估
jvm·数据库·python
weixin_499771558 小时前
使用Seaborn绘制统计图形:更美更简单
jvm·数据库·python