2026年6月26日/ Backend Engineering Notes

JVM 和服务器常用调优清单:堆、GC、连接数和内核参数

这篇整理常用的 JVM 与 Linux 服务器调优项:堆大小、G1/ZGC、GC 日志、HeapDump、Tomcat、ulimit、somaxconn、swappiness、THP、systemd 和容器内存。

JavaJVMLinuxPerformanceServer

这篇不是讲“某个接口为什么慢”,而是整理一套更常见的生产部署层调优项:JVM 参数、GC、Tomcat、Linux 文件句柄、TCP 队列、swap、THP、systemd 和容器内存。

这些配置适合放在服务上线前的检查清单里。它们不是万能参数,真正落地时仍然要看机器规格、JDK 版本、流量模型和压测结果。

先说原则

JVM 和服务器调优不要一上来就复制一大串参数。

更稳的顺序是:

确认 JDK 和部署方式 -> 设置内存边界 -> 选择 GC -> 打开诊断日志
-> 调整系统限制 -> 配合应用连接数 -> 压测验证

每一组参数都要知道它解决什么问题:

类别 解决的问题
堆大小 避免 JVM 反复扩缩容,避免容器 OOM
GC 控制停顿、吞吐和内存占用
GC 日志 / JFR 出问题时能分析,而不是猜
文件句柄 高并发连接下避免 Too many open files
TCP backlog 高峰建连时避免连接排队溢出
swappiness 避免 Java 堆被换出导致长时间卡顿
THP 大堆吞吐和延迟之间的取舍
systemd / 容器限制 保证进程真正拿到配置的资源

不要一次改所有参数。先有基线,再分组调整。

JVM 内存常用项

生产环境最常见的起点是固定堆大小:

-Xms4g -Xmx4g

-Xms 是初始堆,-Xmx 是最大堆。两者相等可以减少运行中堆扩缩容带来的抖动。

如果运行在容器里,也可以用百分比:

-XX:InitialRAMPercentage=70.0
-XX:MaxRAMPercentage=70.0

注意不要把容器内存全部给堆。JVM 还需要:

  • Metaspace。
  • Code Cache。
  • 线程栈。
  • Direct Buffer。
  • GC 和 JVM native 内存。
  • 监控 Agent、sidecar、系统页缓存。

常见经验是:堆占容器限制的 60% - 75%,剩下给非堆和系统开销。

例如容器限制 4G,不要直接 -Xmx4g。可以先从:

-Xms2800m -Xmx2800m

或:

-XX:MaxRAMPercentage=70.0

开始。

其他常用内存项:

-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:ReservedCodeCacheSize=256m
-Xss512k

-Xss 是每个线程栈大小。平台线程很多时,线程栈会吃掉不少内存。不要盲目设太小,递归调用、复杂调用栈可能出问题。

GC 怎么选

大多数 Spring Boot 服务,可以先用 G1。

G1 的常用起点:

-XX:+UseG1GC
-XX:MaxGCPauseMillis=150
-XX:InitiatingHeapOccupancyPercent=40
-XX:G1ReservePercent=15
-XX:+ParallelRefProcEnabled
-XX:+UseStringDeduplication

说明:

  • MaxGCPauseMillis 是软目标,不是保证。
  • InitiatingHeapOccupancyPercent 控制并发标记启动时机,太晚可能导致老年代压力上来。
  • G1ReservePercent 给对象转移留余量,降低 evacuation failure 风险。
  • UseStringDeduplication 对字符串很多的应用可能省内存,但也要看 CPU 开销。

如果服务特别在意尾延迟,或者堆比较大,可以评估 ZGC。

Java 21 的 ZGC 起点:

-XX:+UseZGC
-XX:+ZGenerational

ZGC 通常停顿更低,但可能带来更高的 CPU 或内存占用。不要因为“低延迟”就直接切,需要用同一组压测对比:

G1: CPU、吞吐、p95/p99、GC pause、RSS
ZGC: CPU、吞吐、p95/p99、GC pause、RSS

选择 GC 时可以这样判断:

场景 建议
普通后台服务 G1 起步
中小堆、资源较紧 G1 更稳
大堆、低停顿要求 评估 ZGC
批处理吞吐优先 可评估 ParallelGC
不确定 先 G1,打开日志再看

诊断参数必须打开

调优最怕线上出问题后没有证据。

建议生产至少保留:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dumps/heap-%p.hprof
-XX:+ExitOnOutOfMemoryError
-Xlog:gc*,safepoint:file=/data/logs/gc-%p.log:time,uptime,level,tags:filecount=10,filesize=50M

说明:

  • OOM 时保留 heap dump,方便查内存泄漏。
  • ExitOnOutOfMemoryError 让进程在严重 OOM 后退出,交给 systemd / K8s 拉起,避免服务半死不活。
  • GC 日志要滚动,避免撑满磁盘。
  • safepoint 日志能帮助判断是不是安全点停顿导致卡顿。

需要临时分析时,可以用 JFR:

jcmd <pid> JFR.start name=profile settings=profile duration=120s filename=/data/jfr/app.jfr

常用分析问题:

  • GC 停顿。
  • 分配热点。
  • 锁竞争。
  • 线程阻塞。
  • 文件 / Socket I/O。
  • 虚拟线程 pinning。

Tomcat 和应用连接数

Spring Boot 内置 Tomcat 时,常见配置包括:

server:
  tomcat:
    threads:
      max: 200
      min-spare: 20
    max-connections: 10000
    accept-count: 1000
    connection-timeout: 20000

这些参数要和 Linux 内核队列一起看。

  • threads.max:最多处理请求的平台线程数。
  • max-connections:最多保持多少连接。
  • accept-count:处理线程忙时,连接进入等待队列的长度。

如果 accept-count=1000,但系统 net.core.somaxconn=128,高峰时仍然可能丢连接。应用参数和系统参数要匹配。

Java 21 / Spring Boot 3.2 之后,也可以评估虚拟线程:

spring:
  threads:
    virtual:
      enabled: true

虚拟线程适合 I/O 密集接口,但它不能让数据库、Redis 或外部服务无限承载。下游仍然要靠连接池、限流、超时和熔断保护。

文件句柄限制

高并发服务很容易遇到:

Too many open files

因为 Socket、日志文件、JAR、临时文件、数据库连接都可能占文件句柄。

先检查当前进程限制:

cat /proc/<pid>/limits
ulimit -n
lsof -p <pid> | wc -l

systemd 服务建议直接配:

[Service]
LimitNOFILE=524288
LimitNPROC=65536

系统级可以配:

# /etc/security/limits.d/java.conf
* soft nofile 65536
* hard nofile 524288
* soft nproc 65536
* hard nproc 65536

还可以配:

fs.file-max = 2097152

注意:limits.conf 对 systemd 服务不一定生效,systemd 服务优先看 unit 里的 LimitNOFILE。这是很多人改了限制但服务仍然不生效的原因。

TCP 队列和端口范围

高峰建连时,常见系统参数:

net.core.somaxconn = 8192
net.core.netdev_max_backlog = 10000
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_syncookies = 1
net.ipv4.ip_local_port_range = 1024 65535

说明:

  • somaxconn:服务端 listen backlog 上限,要和 Tomcat accept-count 匹配。
  • tcp_max_syn_backlog:半连接队列。
  • netdev_max_backlog:网卡接收包队列。
  • ip_local_port_range:本机可用临时端口范围,出站连接多时很重要。

TIME_WAIT 很多时,可以看:

net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30

不要使用 tcp_tw_recycle。它已经被移除,而且会和 NAT、负载均衡场景冲突。

排查命令:

ss -s
ss -ltn
netstat -s | grep -E "listen|overflow|drop|reset"
nstat | grep -E "Listen|Drop|Retrans"

如果出现 listen overflow 或 drop,先看:

应用 accept-count
net.core.somaxconn
tcp_max_syn_backlog
CPU 是否打满
应用线程是否处理不过来

swap 和 swappiness

Java 服务通常不希望堆被换到 swap。

一旦发生 swap,GC 和接口延迟可能出现非常长的停顿。

常见设置:

vm.swappiness = 10

也有人设置成 1 或直接关闭 swap。是否关闭要看机器和运维策略,但不建议让 Java 服务在默认 60 的 swappiness 下裸跑。

检查是否发生 swap:

free -h
vmstat 1
cat /proc/<pid>/status | grep VmSwap

如果 si/so 持续有值,或进程 VmSwap 不为 0,就要警惕。

THP 怎么处理

THP 是 Transparent Huge Pages。

它可能提升大堆场景吞吐,也可能带来延迟抖动。不同机器、内核、GC 下结果不一样。

常见起点是设为 madvise

echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag

严格低延迟场景可以评估 never

echo never > /sys/kernel/mm/transparent_hugepage/enabled

如果配合 JVM 使用大页,通常还会看:

-XX:+AlwaysPreTouch

AlwaysPreTouch 会在启动时预触碰堆内存,让运行时更稳定,但会增加启动时间。

检查:

cat /sys/kernel/mm/transparent_hugepage/enabled
cat /proc/meminfo | grep -E "HugePages|AnonHugePages"

THP 不要靠感觉决定,最好用压测比较 p99、CPU 和 GC。

容器和 K8s 内存

容器里最常见的坑是:

容器 limit = 2G
JVM -Xmx = 2G

这样很容易 OOMKill,因为非堆内存没有空间。

更安全的估算:

容器内存 = 堆 + Metaspace + Direct Memory + 线程栈 + Code Cache + native + Agent + 系统余量

可以这样起步:

-XX:MaxRAMPercentage=70.0
-XX:InitialRAMPercentage=70.0

或者固定:

container limit: 4g
JVM: -Xms2800m -Xmx2800m

如果 Direct Buffer 用得多,比如 Netty、NIO、网关类服务,要额外限制或监控:

-XX:MaxDirectMemorySize=512m

分析 native memory:

-XX:NativeMemoryTracking=summary
jcmd <pid> VM.native_memory summary

K8s 下还要看:

  • pod 是否频繁 OOMKilled。
  • limit 是否过紧。
  • request / limit 是否导致 CPU throttling。
  • sidecar 是否也占内存。
  • JDK 是否正确识别 cgroup。

检查 JVM 看到的内存:

java -XshowSettings:vm -version

常用模板

G1 模板

JAVA_OPTS="
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=150
-XX:InitiatingHeapOccupancyPercent=40
-XX:G1ReservePercent=15
-XX:+ParallelRefProcEnabled
-XX:+UseStringDeduplication
-XX:+AlwaysPreTouch
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:ReservedCodeCacheSize=256m
-Xss512k
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dumps/heap-%p.hprof
-XX:+ExitOnOutOfMemoryError
-Xlog:gc*,safepoint:file=/data/logs/gc-%p.log:time,uptime,level,tags:filecount=10,filesize=50M
"

ZGC 模板

JAVA_OPTS="
-Xms4g -Xmx4g
-XX:+UseZGC
-XX:+ZGenerational
-XX:+AlwaysPreTouch
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dumps/heap-%p.hprof
-XX:+ExitOnOutOfMemoryError
-Xlog:gc*,safepoint:file=/data/logs/gc-%p.log:time,uptime,level,tags:filecount=10,filesize=50M
"

sysctl 模板

# /etc/sysctl.d/99-java-server.conf
fs.file-max = 2097152
vm.swappiness = 10
vm.max_map_count = 262144

net.core.somaxconn = 8192
net.core.netdev_max_backlog = 10000
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
net.ipv4.ip_local_port_range = 1024 65535

应用:

sysctl --system

systemd 模板

[Unit]
Description=Spring Boot App
After=network.target

[Service]
User=app
WorkingDirectory=/opt/app
LimitNOFILE=524288
LimitNPROC=65536
Environment="JAVA_OPTS=-Xms4g -Xmx4g -XX:+UseG1GC"
ExecStart=/usr/bin/java $JAVA_OPTS -jar /opt/app/app.jar
Restart=always
SuccessExitStatus=143

[Install]
WantedBy=multi-user.target

修改后:

systemctl daemon-reload
systemctl restart app
cat /proc/$(pgrep -f app.jar)/limits

上线前检查清单

  1. JDK 版本确认,优先用 Java 21 LTS。
  2. -Xms-XmxMaxRAMPercentage 已设置。
  3. 容器内存留出 25% - 40% 非堆余量。
  4. GC 已明确选择 G1 或 ZGC。
  5. GC 日志、HeapDump、OOM 退出策略已配置。
  6. JFR 或临时诊断方式已准备。
  7. systemd LimitNOFILE 生效。
  8. somaxconn 和 Tomcat accept-count 匹配。
  9. swappiness 不再是默认高值。
  10. THP 策略已确认并压测。
  11. 压测记录包含 CPU、内存、GC、连接数、错误率和延迟。
  12. 每次只改一组参数,有回滚方案。

最后

JVM 和服务器调优的重点不是参数越多越专业,而是边界清楚、证据完整。

一个靠谱的调优记录应该写得出来:

当前限制是什么 -> 改了哪个参数 -> 为什么改 -> 怎么验证 -> 有没有副作用

如果只是贴一串参数,不知道它们解决什么问题,也不知道改完有没有变好,那这不是调优,只是换了一种不确定性。

Brief

This note focuses on common JVM and Linux server tuning rather than business API optimization.

The practical areas are heap sizing, GC choice, diagnostic flags, Tomcat limits, file descriptors, TCP backlog, TIME_WAIT handling, swappiness, THP and container memory headroom.

Treat the values as starting points. Apply one group at a time and validate with GC logs, JFR, system metrics and load tests.