跳过导航

Java 应用内存持续上涨,却没有 OOM,如何定位?

约 4 分钟...次浏览
专栏分布式稳定性与工程实践第 6 篇

监控中的“内存”可能是 JVM 堆、Metaspace、直接内存、线程栈、代码缓存、mmap 或操作系统页缓存。RSS 持续上涨而堆稳定时,反复分析 Heap Dump 通常不会找到答案。

先确认是哪种内存

同时观察容器 RSS、jvm_memory_used_bytes 的 heap/non-heap、GC 后老年代占用、线程数和 direct buffer:

jcmd <pid> GC.heap_info
jcmd <pid> VM.native_memory summary
jcmd <pid> Thread.print

Native Memory Tracking 需要启动时开启 -XX:NativeMemoryTracking=summary 或 detail,存在一定开销。可先建立 baseline,增长后执行 VM.native_memory summary.diff

NMT 主要跟踪 HotSpot/JVM 已归类的本地内存,不保证覆盖所有 JNI/第三方 native 库分配;NMT 总量与 RSS 不一致并不罕见。RSS 还受共享页、文件映射、页是否驻留和本地分配器保留空闲 arena 影响,所以“RSS 未下降”也不自动证明仍有活对象。

情况一:GC 后老年代基线增长

这更像 Java 对象被长期引用。受控采集:

jcmd <pid> GC.heap_dump /safe/path/heap.hprof

Heap dump 需要安全点,可能造成长停顿和大量磁盘 I/O,文件大小接近堆规模;是否先触发完整 GC、是否包含不可达对象取决于目标 JDK 的命令选项。执行前查看 jcmd <pid> help GC.heap_dump,确认磁盘空间、容灾和敏感数据处理,优先在已摘流副本采集。不要在多个副本上同时 dump。

MAT 中不要只看最大对象,应查看 dominator tree、retained heap 和到 GC Root 的引用链。常见根因包括无上限本地缓存、静态 Map、ThreadLocal 未清理、监听器未注销、队列消费速度低于生产速度。

事故案例中,按 tenantId 缓存配置却没有最大容量和过期;租户扫描任务每天产生新键。ConcurrentHashMap 支配数百万配置对象。修复为带 maximumSize、expireAfterAccess 和命中率指标的缓存,并避免把缓存当持久化存储。

情况二:堆稳定但 RSS 增长

检查 java.nio BufferPool、Netty direct memory、线程数、Metaspace 和 JNI。DirectByteBuffer 的 Java 包装对象由 GC 回收后才触发清理;若堆压力小,直接内存释放可能滞后。Netty 泄漏检测可在灰度环境提高级别,生产全量 paranoid 模式开销较高。

线程每个都需要栈空间,数千个线程会显著增加 native memory。动态类生成、错误创建新 ClassLoader 会导致 Metaspace 增长,Heap Dump 中可检查重复类加载器。

“没有 OOM”不代表安全

容器可能先被内核 OOM Killer 杀死,JVM 来不及抛出 OutOfMemoryError。也可能因为限额较大,泄漏尚未触顶。确认 Pod termination reason、宿主机日志和 cgroup v2 的 memory.current / memory.events(路径由运行时决定),并给 -Xmx 之外的 Metaspace、Code Cache、线程栈、direct buffer 和 native 库留余量。百分比堆参数也要结合容器感知和所用 JDK 发行版验证。

可预先配置 -XX:+HeapDumpOnOutOfMemoryError 与指向持久、安全、容量受控位置的 -XX:HeapDumpPath,但自动 dump 也可能填满磁盘或延长故障恢复时间,必须配套容量和数据保留策略。

验证修复

用相同流量运行足够长时间,比较多次 Full GC 后基线,而不是只看锯齿瞬时值。缓存修复应验证容量上限、淘汰率、加载失败和下游压力,避免内存恢复却把数据库打垮。

生产检查清单

  • 指标中的“内存”究竟是 heap、RSS 还是 working set?
  • 老年代是否在 GC 后仍持续抬升?
  • Dump 是否在摘流实例采集并确认磁盘空间?
  • 是否监控 direct buffer、线程、类加载器和 Metaspace?
  • 缓存与队列是否都有硬上限?
  • 容器限额是否覆盖堆、native、线程栈和安全余量?

内存问题的第一原则不是立刻 Dump,而是先确认增长发生在哪个内存域,再选择能看到该域的工具。

分享:
文章作者:狼码纪
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。文章可能参考了其他优秀文章,如有侵权请联系删除。