Quarkus 服务容器内存异常增长问题复盘

一、问题概述

在生产环境中,一个基于 Quarkus + JDK 21 的服务出现容器内存持续增长的问题。

服务特征如下:

  • 运行在 Docker 容器中
  • JVM Heap 限制为 128MB
  • 初始内存占用约 300MB
  • 运行 1~2 天后,容器 RSS 增长至 600MB~1.2GB
  • 无 OOM、无明显 GC 异常
  • 服务功能和 QPS 未发生显著变化

该问题存在潜在风险:

  • 容器节点内存被持续侵占
  • 长时间运行后可能触发宿主机 OOM
  • 难以通过 JVM 常规监控发现

二、问题影响

  1. 容器 RSS 与 JVM Heap 严重不匹配,误导监控判断
  2. 内存增长不可预测,难以设置合理的资源水位
  3. 存在节点级连锁风险,影响同机其他服务

三、排查过程

1. Docker 层面确认问题

通过 docker stats 观察到:

  • 服务启动后 RSS ≈ 300MB
  • 运行时间越长 RSS 越高
  • 重启后内存立即恢复到初始水平

初步判断为“运行期内存占用增长”。


2. JVM Heap 排查

使用以下命令查看 Heap 状态:

jcmd 1 GC.heap_info

结果显示:

  • Heap committed 始终为 128MB
  • Old 区使用率稳定
  • 无 Full GC 异常

结论:不是 Java 对象泄漏。


3. 线程与栈内存排查

jcmd 1 Thread.print
ls /proc/1/task | wc -l
  • Java 线程数量稳定在 50~60
  • 单线程栈大小已限制为 512KB

结论:线程与栈内存不是主要增长来源。


4. Direct Memory 排查

jcmd 1 GC.class_histogram | grep DirectByteBuffer
  • DirectByteBuffer 数量与容量均很小
  • 未发现直接内存异常累积

结论:不是 NIO 直接内存泄漏。


5. Native Memory Tracking 关键发现

启用 JVM 参数:

-XX:NativeMemoryTracking=summary

执行:

jcmd 1 VM.native_memory summary

发现:

  • JVM 统计的 committed 内存 ≈ 300MB~350MB
  • docker stats 显示 RSS ≈ 600MB~1.2GB
  • 两者存在数百 MB 的差距

这表明:

有大量内存不在 JVM 管理与统计范围内。


四、根因分析

glibc malloc arena 机制导致 RSS 膨胀

最终定位问题为 glibc malloc arena 行为

在 Linux 系统中:

  • glibc 为多线程 malloc 提供 arena
  • 默认 arena 数量与 CPU 核数相关
  • 多线程 I/O 框架(如 Netty、Vert.x)会频繁触发 arena 分配
  • arena 中的内存即使空闲,也不会主动归还给操作系统

在容器环境下表现为:

  • JVM Heap 稳定
  • Native Memory 看似正常
  • 进程 RSS 持续增长且无法回收

五、解决方案

1. 限制 glibc arena 数量

在容器环境变量中添加:

MALLOC_ARENA_MAX=2

效果:

  • 强制限制 arena 数量
  • 显著减少内存碎片
  • RSS 不再随运行时间增长

2. 保留 NMT 用于长期监控

-XX:NativeMemoryTracking=summary

用于:

  • 定位 JVM 内存结构变化
  • 区分 JVM 内存与系统内存问题

六、验证结果

修复前

  • 启动 RSS ≈ 300MB
  • 运行 24 小时 RSS ≈ 600MB
  • 运行 48 小时 RSS ≈ 700MB+

修复后

  • 启动 RSS ≈ 250MB~300MB
  • 运行 48 小时 RSS ≈ 300MB~350MB
  • 内存占用稳定

Java Heap、Metaspace、线程数均无明显变化。


七、复现与反向验证

在一次实验中,因环境变量拼写错误:

MALLOC_ARENA_MAX1=2

导致 arena 实际未被限制。

结果为:

  • 服务运行 1 天后 RSS 再次增长至 ≈ 600MB

该实验从反向再次验证了问题根因。


八、经验总结

  1. 容器中 Java 进程的 RSS 不等于 JVM 内存
  2. glibc arena 是容器内存异常增长的常见隐蔽来源
  3. JVM 参数无法完全解决系统级内存问题
  4. Java 服务在容器化部署时必须同时关注 JVM 与 libc 行为

九、后续行动项

  1. 所有 Java 容器统一添加 MALLOC_ARENA_MAX=2
  2. 关键服务保留 NMT 用于排查
  3. 更新团队 Java 容器内存排查手册
  4. 在容量评估中区分 Heap 与 RSS

如果你愿意,我可以下一步直接帮你整理成

  • 《Java 容器内存排查 Checklist》
  • 《为什么 Heap 很小但容器内存很大》的 FAQ
  • 或一页 “Leader 可读版”总结说明

你只要说一声。

作者:张三  创建时间:2026-01-06 09:45
最后编辑:张三  更新时间:2026-01-14 17:39