码农戏码

新生代农民工的自我修养

0%

内存泄漏排查总结

服务通过 - Xmx=6G 指定最大堆分配为 6G,但实际 RSS 已达到 11G,开始怀疑堆外内存是否有内存泄露。

RSS从启动之后,就是一路增长,由于是测试机,业务量小,以53M/小时的增量增长。

JVM 的进程总内存 ≈

Java Heap + Metaspace + CodeCache + Thread Stack + JNI/Direct Buffer + GC 结构 + 内部 allocator fragment + C 库分配 + mmap 区域 + …

由于现象是RES比较高,先看一下java堆是否有异常。把java堆dump下来仔细排查一下,jmap -histo:live pid,发现整个堆回收完也才几百兆,远不到8G的Xmx的上限值,GC日志看着也没啥异常。基本排查java堆内存泄露的可能性。

1
jcmd 1 VM.native_memory detail scale=MB > nmt-`date +%F-%H-%M-%S`.log & pmap -x 1 > pmap-`date +%F-%H-%M-%S`.log

整体思路是:通过多次收集,进行比较,查看明显的差异

1
icdiff pmap-2023-07-27-09-46-36.log pmap-2023-07-28-09-29-55.log | less -SR

排查顺序

1️⃣ 先开 -XX:NativeMemoryTracking=detail

2️⃣ jcmd VM.native_memory summary 定位哪一类区域增长

3️⃣ 如果是 Internal / Arena Chunk → JNI / DirectBuffer 泄漏

4️⃣ 用 heaptrack / tcmalloc 进一步追踪分配源

5️⃣ 如果是 Thread 区域 → 检查线程数

6️⃣ 若都是正常,考虑 libc allocator fragment,可尝试使用 -XX:+UseG1GC-XX:MaxDirectMemorySize 或更换 allocator(jemalloc)

这一次排查,先通过NMT,发现Java能追踪到的内存区域一切正常

再通过pmap工具,发现了JNI调用C++有内存泄漏

C++修复后,内存增长大幅降低,但RSS依然会增长

最后,只能使用jemalloc替换原生的glibc来排查一下

替换后,内存还真的不再增长。

cat /etc/os-release

1
2
3
4
5
6
7
8
9
10
11
12
PRETTY_NAME="Ubuntu 22.04.4 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.4 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

ldd –version

1
2
3
4
5
ldd (Ubuntu GLIBC 2.35-0ubuntu3.6) 2.35
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

RSS 持续增长的几种可能情况:

  1. 正常情况(常见):你的程序频繁地分配和释放大小不一的内存。这会导致堆内存出现“空洞”(外部碎片),即使有大量空闲内存,也因为不连续而无法从堆顶归还给 OS。RSS 会稳定在一个较高的水平,这是 ptmalloc2 的设计特点,目的是提高分配效率。
  2. 内存泄漏(需要警惕):你的程序持续分配内存,但忘记释放。这会导致堆不断增长,即使通过 brkmmap 向 OS 索要更多内存,RSS 会无限制地增长,直到被系统 OOM Killer 杀死。
  3. 峰值使用后的稳定期:程序在某个阶段需要大量内存,之后虽然释放了,但 glibc 没有立即归还给 OS(堆未收缩)。这部分内存成为了进程的“缓存”,如果程序后续再次需要分配内存,就可以快速重用,避免了系统调用的开销。
  • RSS 增长不一定是内存泄漏,很可能是 glibc 的内存池策略。
  • glibc 不会因系统内存紧张而主动释放内存,它的行为是预定义的。
  • 真正的回收保障来自 Linux 内核,当系统内存不足时,它会强制回收,可能包括将你的进程内存换出(Swap)。
  • 如果 RSS 增长是无界的、持续的,并且与你的业务逻辑预期不符,那么首先应该怀疑和排查内存泄漏

Reference

Jemalloc简介及使用方法

使用jemalloc解决JVM内存泄露问题

Java 进程内存占用及可观测性调研&内存异常排查最佳实践