JDK21 升级

📋 目录


一、升级背景

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: 可用内存持续下降

排查步骤:

  1. 查看堆内存使用: jmap -heap <pid>
  2. 查看 GC 日志: $GC_LOG_DIR/gc-*.log
  3. 检查是否有内存泄漏: jmap -histo:live <pid>

常见原因:

  • 堆内存设置过小
  • 存在内存泄漏
  • 直接内存使用过多

问题 2: OOM 错误

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

解决:

  1. 分析 heap dump: $GC_LOG_DIR/heaperr.log.*
  2. 使用 MAT 工具分析
  3. 如果确实需要更多内存,可以调整 -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>
相关推荐
间彧2 小时前
SkyWalking详解与应用实战
后端
Python私教3 小时前
使用FastAPI+FastCRUD自动生成API接口
后端
Python私教3 小时前
使用 FastAPI+FastCRUD 快速开发博客后端 API 接口
后端
程序员阿达3 小时前
开题报告之基于SpringBoot框架的图书借阅系统的设计与实现
java·spring boot·后端
Eoch773 小时前
吃透 Java 核心技术:JVM 调优、并发安全、微服务开发,解决 90% 企业级场景问题
java·后端
歪歪1003 小时前
详细介绍一下“集中同步+分布式入库”方案的具体实现步骤
开发语言·前端·分布式·后端·信息可视化
林太白3 小时前
rust17-部门管理模块
前端·后端·rust
C++chaofan3 小时前
MyBatis - Plus学习笔记
java·spring boot·笔记·后端·mysql·架构·mybatis
间彧3 小时前
如何设计异常分级策略,对不同级别异常(如业务异常、系统异常)采取不同的告警方式?
后端