📋 目录
- 一、升级背景
- [二、JVM 参数优化](#二、JVM 参数优化 "#%E4%BA%8Cjvm-%E5%8F%82%E6%95%B0%E4%BC%98%E5%8C%96")
- 三、内存使用分析
- 四、配置详解
- 五、监控指标
- 六、常见问题
一、升级背景
1.1 为什么升级 Java 21?
- ✅ 性能提升: G1 GC 优化,启动速度更快
- ✅ 安全性: 修复大量安全漏洞
- ✅ 新特性: 虚拟线程、模式匹配等
- ✅ 长期支持: LTS 版本,支持到 2029 年
1.2 升级前的配置问题
| 问题 | 影响 | 优化后 |
|---|---|---|
-Xmx4g -Xms1g |
堆内存不一致,频繁扩展 | -Xmx2g -Xms2g 固定堆 |
-XX:MaxDirectMemorySize=2g |
直接内存过大,浪费资源 | 降到 512m |
-XX:MaxMetaspaceSize=512m |
元空间过大 | 降到 256m |
G1HeapRegionSize=2m |
Region 过小,GC 开销大 | 调整到 4m |
G1NewSizePercent=30-40% |
年轻代过大 | 调整到 20-30% |
二、JVM 参数优化
2.1 完整配置(deploy/run.sh)
bash
#!/usr/bin/env bash
# Java 21 优化配置(4C8G + QPS ≤ 10)
# ========== 基础参数 ==========
JVM_ARGS=" -server \
-Dfile.encoding=UTF-8 \
-Dsun.jnu.encoding=UTF-8 \
-Djava.io.tmpdir=/tmp \
-Djava.net.preferIPv6Addresses=false \
-Duser.timezone=GMT+08 \
-Djava.lang.Integer.IntegerCache.high=10240 \
--add-opens=java.base/java.lang=ALL-UNNAMED \
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED \
--add-opens=java.base/java.io=ALL-UNNAMED \
--add-opens=java.base/java.math=ALL-UNNAMED \
--add-opens=java.base/java.net=ALL-UNNAMED \
--add-opens=java.base/java.nio=ALL-UNNAMED \
--add-opens=java.base/java.security=ALL-UNNAMED \
--add-opens=java.base/java.text=ALL-UNNAMED \
--add-opens=java.base/java.time=ALL-UNNAMED \
--add-opens=java.base/java.util=ALL-UNNAMED \
--add-opens=java.base/java.util.concurrent=ALL-UNNAMED \
--add-opens=java.base/java.util.concurrent.locks=ALL-UNNAMED \
--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED \
--add-opens=java.base/jdk.internal.access=ALL-UNNAMED \
--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED \
--add-opens=java.base/jdk.internal.perf=ALL-UNNAMED \
--add-exports=java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED \
--add-exports=java.base/sun.security.action=ALL-UNNAMED \
--add-exports=java.base/sun.net.util=ALL-UNNAMED "
# ========== 堆内存配置 ==========
JVM_HEAP=" -Xss512k \
-Xmx2g -Xms2g \
-XX:MetaspaceSize=128m \
-XX:MaxMetaspaceSize=256m \
-XX:+AlwaysPreTouch \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:MaxDirectMemorySize=512m "
# ========== G1 GC 配置 ==========
JVM_GC=" -XX:+UseG1GC \
-XX:G1HeapRegionSize=4m \
-XX:+UnlockExperimentalVMOptions \
-XX:G1NewSizePercent=20 \
-XX:G1MaxNewSizePercent=30 \
-XX:InitiatingHeapOccupancyPercent=45 \
-XX:MaxGCPauseMillis=100 \
-XX:ParallelGCThreads=4 \
-XX:ConcGCThreads=1 \
-XX:+TieredCompilation \
-XX:CICompilerCount=3 \
-Xlog:safepoint,class+load=info,class+unload=info,gc*=info:file=$GC_LOG_DIR/gc-%t.log:time,tid,tags:filecount=5,filesize=50m"
# ========== 日志和诊断 ==========
NOW_DATE=`date +%Y%m%d`
JVM_LOG_ARGS=" -XX:ErrorFile=$GC_LOG_DIR/vmerr.log.$NOW_DATE \
-XX:HeapDumpPath=$GC_LOG_DIR/heaperr.log.$NOW_DATE"
2.2 参数对比表
| 参数类型 | 升级前 | 升级后 | 优化原因 |
|---|---|---|---|
| 最大堆内存 | -Xmx4g |
-Xmx2g |
4GB 对低流量应用过大,降到 2GB |
| 初始堆内存 | -Xms1g |
-Xms2g |
与 Xmx 一致,避免堆扩展 |
| 元空间 | 512m |
256m |
低流量应用元空间需求小 |
| 直接内存 | 2g |
512m |
降低直接内存占用 |
| G1 Region | 2m |
4m |
2GB 堆建议 4MB region |
| 年轻代比例 | 30-40% |
20-30% |
低流量应用年轻代不需要太大 |
| IHOP | 40% |
45% |
延迟并发标记触发时机 |
三、内存使用分析
3.1 内存占用预期
升级前(-Xmx4g -Xms1g)
erlang
理论最大占用:
├─ 堆内存:4GB
├─ 元空间:512MB
├─ 直接内存:2GB
├─ 其他:300-500MB
└─ 总计:6.5-7GB(占机器 81-87%)⚠️
风险:可用内存可能降到 12-25%,触发 OOM Killer
升级后(-Xmx2g -Xms2g)
erlang
稳定占用:
├─ 堆内存:2GB(固定)
├─ 元空间:256MB
├─ 直接内存:512MB
├─ 其他:300-500MB
└─ 总计:3-3.3GB(占机器 37-41%)✅
可用内存:55-60%(稳定)
3.2 内存变化趋势
| 阶段 | 可用内存占比 | 使用内存 | 说明 |
|---|---|---|---|
| 调整前 | 75% | 2GB | 应用未启动 |
| 启动后 | 64% | 2.88GB | JVM 初始分配 |
| 稳定运行 | 55-60% | 3.2-3.6GB | 预期稳定状态 ✅ |
结论: 从 75% → 64% 符合预期,稳定后会在 55-60%。
四、配置详解
4.1 Java 21 模块化兼容参数
为什么需要 --add-opens 和 --add-exports?
Java 9+ 引入了模块化系统 (JPMS),给每个包都加了"门锁"🔒。但 Spring、MyBatis 等框架大量使用反射,需要"钥匙"才能访问。
bash
# --add-opens: 允许反射访问(打开门)
--add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
# ... 更多包
# --add-exports: 允许访问内部 API(VIP 通行证)
--add-exports=java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED
哪些框架需要这些参数?
- ✅ Spring Boot: 依赖注入、AOP
- ✅ MyBatis: 数据库结果映射
- ✅ Jackson/Gson: 序列化/反序列化
- ✅ Netty/Dubbo: 底层网络框架
⚠️ 不要删除这些参数,否则应用无法启动!
4.2 G1 GC 参数详解
为什么选择 G1 GC?
- ✅ 适合 2GB 堆内存
- ✅ 可预测的停顿时间
- ✅ 自动调优能力强
- ✅ Java 21 默认 GC
关键参数说明
| 参数 | 值 | 作用 | 为什么这样设置 |
|---|---|---|---|
| G1HeapRegionSize | 4m | Region 大小 | 2GB 堆 ÷ 512 regions = 4MB |
| G1NewSizePercent | 20% | 年轻代最小比例 | 低流量应用对象创建少 |
| G1MaxNewSizePercent | 30% | 年轻代最大比例 | 避免年轻代过大浪费空间 |
| InitiatingHeapOccupancyPercent | 45% | 触发并发标记阈值 | 延迟并发标记,减少 CPU 开销 |
| MaxGCPauseMillis | 100ms | 目标停顿时间 | 低流量应用可接受 100ms |
| ParallelGCThreads | 4 | 并行 GC 线程数 | 4C 机器设置为 4 |
| ConcGCThreads | 1 | 并发 GC 线程数 | 低流量应用 1 个足够 |
GC 性能预期
yaml
Young GC:
├─ 频率: 1-2 次/小时
├─ 停顿: < 50ms
└─ 原因: 年轻代 400-600MB,对象创建少
Mixed GC:
├─ 频率: 1-2 次/天
├─ 停顿: < 100ms
└─ 原因: 老年代回收
Full GC:
└─ 频率: 几乎不发生(配置合理)
4.3 性能优化参数
分层编译(TieredCompilation)
bash
-XX:+TieredCompilation # 启用分层编译
-XX:CICompilerCount=3 # 编译器线程数
作用:
- 先用 C1 编译器快速编译(启动快)
- 再用 C2 编译器深度优化(运行快)
内存预分配(AlwaysPreTouch)
bash
-XX:+AlwaysPreTouch
作用:
- 启动时就分配所有内存
- 避免运行时缺页中断
- 启动慢 5-10 秒,但运行更稳定
整数缓存优化
bash
-Djava.lang.Integer.IntegerCache.high=10240
作用:
- 默认缓存 -128 ~ 127
- 扩展到 -128 ~ 10240
- 减少 Integer 对象创建
4.4 日志和诊断配置
GC 日志(Java 21 新格式)
bash
-Xlog:safepoint,class+load=info,class+unload=info,gc*=info:file=$GC_LOG_DIR/gc-%t.log:time,tid,tags:filecount=5,filesize=50m
说明:
- ❌ 不要用
-Xloggc(已废弃) - ✅ 使用
-Xlog:gc*=info - 日志轮转: 5 个文件,每个 50MB
OOM 自动 Dump
bash
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=$GC_LOG_DIR/heaperr.log.$NOW_DATE
作用: OOM 时自动生成 heap dump,便于排查问题。
JVM 崩溃日志
bash
-XX:ErrorFile=$GC_LOG_DIR/vmerr.log.$NOW_DATE
作用: JVM 崩溃时记录详细信息。
五、监控指标
5.1 关键指标
| 指标 | 预期值 | 告警阈值 | 说明 |
|---|---|---|---|
| 内存使用率 | 40-45% | > 60% | 总内存使用占比 |
| 可用内存占比 | 55-60% | < 40% | memfree / memtotal |
| 堆内存使用率 | 60-80% | > 90% | 堆内存占用 |
| Young GC 频率 | 1-2 次/小时 | > 10 次/小时 | 年轻代 GC |
| Young GC 停顿 | < 50ms | > 100ms | 停顿时间 |
| Mixed GC 频率 | 1-2 次/天 | > 10 次/天 | 混合 GC |
| Full GC 频率 | 0 | > 0 | 不应该发生 |
5.2 企业监控系统配置
查看 GC 指标
bash
# 查看 Transaction 指标(根据企业监控系统调整)
appkey: <your-appkey>
type: System
name: All
# 关注指标:
- gc.count (GC 次数)
- gc.time (GC 耗时)
- memory.heap.used (堆内存使用)
查看主机指标
bash
# 查看主机内存(根据企业监控系统调整)
endpoint: <your-hostname>
metrics:
- mem.memfree.percent (可用内存百分比)
- mem.memused (已用内存)
- load.1minPerCPU (CPU 负载)
六、常见问题
6.1 启动报错
问题 1: 模块访问错误
vbnet
WARNING: Illegal reflective access by org.springframework...
java.lang.reflect.InaccessibleObjectException
解决 : 确保添加了所有 --add-opens 和 --add-exports 参数。
问题 2: GC 日志格式错误
vbnet
Unrecognized VM option 'PrintGCDetails'
解决 : Java 21 已废弃 -XX:+PrintGCDetails,使用 -Xlog:gc*=info。
6.2 内存问题
问题 1: 可用内存持续下降
排查步骤:
- 查看堆内存使用:
jmap -heap <pid> - 查看 GC 日志:
$GC_LOG_DIR/gc-*.log - 检查是否有内存泄漏:
jmap -histo:live <pid>
常见原因:
- 堆内存设置过小
- 存在内存泄漏
- 直接内存使用过多
问题 2: OOM 错误
makefile
java.lang.OutOfMemoryError: Java heap space
解决:
- 分析 heap dump:
$GC_LOG_DIR/heaperr.log.* - 使用 MAT 工具分析
- 如果确实需要更多内存,可以调整
-Xmx到 2.5g 或 3g
6.3 GC 问题
问题 1: Young GC 频繁
现象: Young GC 每分钟多次
排查:
bash
# 查看 GC 日志
grep "Pause Young" $GC_LOG_DIR/gc-*.log
# 查看年轻代大小
jstat -gcutil <pid> 1000
解决: 适当增加年轻代比例(但不超过 40%)。
问题 2: Full GC 发生
现象: 出现 Full GC
排查:
bash
# 查看 Full GC 原因
grep "Full GC" $GC_LOG_DIR/gc-*.log
常见原因:
- 元空间不足 → 增加
MaxMetaspaceSize - 堆内存不足 → 增加
Xmx - 内存泄漏 → 分析 heap dump
七、优化效果总结
7.1 优化前后对比
| 维度 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 最大内存占用 | 6.5-7GB (81-87%) | 3-3.3GB (37-41%) | ⬇️ 50% |
| 可用内存 | 可能降到 12-25% | 稳定在 55-60% | ⬆️ 2-3 倍 |
| 堆内存稳定性 | 1GB → 4GB 动态扩展 | 2GB 固定 | ✅ 稳定 |
| GC 停顿时间 | 不可预测 | < 100ms | ✅ 可控 |
| OOM 风险 | 高 | 低 | ✅ 安全 |
7.2 适用场景
✅ 适合:
- 4C8G 机器配置
- QPS ≤ 10 的低流量应用
- Spring Boot + MyBatis 技术栈
- Java 21 运行环境
⚠️ 不适合:
- 高流量应用(QPS > 100)
- 大内存机器(16GB+)
- 需要大量直接内存的应用(Netty 重度使用)
八、参考资料
8.1 官方文档
九、附录
9.1 快速检查清单
升级前检查:
- 确认 Java 21 环境已安装
- 备份原有配置文件
- 确认依赖包兼容 Java 21
- 准备回滚方案
升级后验证:
- 应用正常启动
- 接口功能正常
- GC 日志正常输出
- 内存使用在预期范围
- 无 WARNING 或 ERROR 日志
9.2 常用命令
bash
# 查看 JVM 参数
ps aux | grep java
# 查看堆内存使用
jmap -heap <pid>
# 查看 GC 统计
jstat -gcutil <pid> 1000
# 生成 heap dump
jmap -dump:live,format=b,file=heap.hprof <pid>
# 查看线程栈
jstack <pid> > thread.txt
# 查看 JVM 信息
jinfo <pid>