在运行于Docker容器中的JVM中,Resident Set Size (RSS)和Java总承诺内存(NMT)之间的区别是什么?

64

场景:

我有一个在Docker容器中运行的JVM。我使用了两个工具进行内存分析:1)top 2)Java Native Memory Tracking。数字看起来令人困惑,我正在尝试找出是什么导致了差异。

问题:

Java进程的RSS报告为1272MB,而总Java内存报告为790.55 MB。我该如何解释其余的内存1272-790.55 = 481.44 MB去了哪里?

为什么我想保持这个问题开放,即使在SO上看过this question的答案:

我确实看到了答案,解释是有道理的。但是,在获取了Java NMT和pmap -x的输出之后,我仍然无法具体地映射哪些Java内存地址实际上是常驻内存并且物理映射的。我需要一些具体的解释(包括详细的步骤),以找出是什么导致了RSS和Java总提交内存之间的差异。

顶部输出

enter image description here

Java NMT

enter image description here

Docker内存统计

enter image description here

图表

我有一个运行了超过48小时的docker容器。现在,当我查看包含以下内容的图表时:

  1. 分配给docker容器的总内存=2 GB
  2. Java最大堆=1 GB
  3. 总提交(JVM)=始终小于800 MB
  4. 堆使用(JVM)=始终小于200 MB
  5. 非堆使用(JVM)=始终小于100 MB。
  6. RSS=约为1.1 GB。

那么,在1.1 GB (RSS)和800 MB (Java总提交内存)之间消耗内存的是什么?

enter image description here


可能是为什么JVM报告的已提交内存比Linux进程常驻集大小更大?的重复问题...嗯,反之亦然。 - the8472
我也有同样的问题,找不到答案 :( 你使用的是什么类型的应用程序? - Michael
你是否对之前的问题(http://stackoverflow.com/a/38630406/6309)所得到的答案感到满意? - VonC
2
@sunsin1985 你可以尝试将malloc实现更改为jemalloc,我曾看到过在此之后RSS显著减少的情况。请参见https://gdstechnology.blog.gov.uk/2015/12/11/using-jemalloc-to-get-to-the-bottom-of-a-memory-leak/我的应用程序使用600MB堆和1.3GB RSS运行-我还没有仔细调查,但内存简单地消失了-没有分配重要的本机内存。我怀疑这是由于内存碎片化引起的(这就是为什么jemalloc有所帮助的原因)。 - mabn
如果您正在追踪Java本地内存泄漏或想要最小化RSS使用,可以参考以下问题及其答案:https://dev59.com/L18e5IYBdhLWcg3wJXvm#35610063 - Lari Hotari
2个回答

64
你在Mikhail Krestjaninoff的一篇文章中提到了"分析Docker容器中Java内存使用"的一些线索:
(需要澄清的是,在2019年5月,三年后,随着openJDK 8u212的发布,情况得到了改善
Resident Set Size是指进程当前分配并使用的物理内存量(不包括交换出的页面)。它包括代码、数据和共享库(在使用它们的每个进程中都计数)。为什么docker stats信息与ps数据不同?第一个问题的答案非常简单-Docker存在一个错误(或功能-取决于您的心情):它将文件缓存包含在总内存使用信息中。因此,我们可以避免此度量标准,并使用关于RSS的ps信息。那好吧-但RSS为什么比Xmx高?从理论上讲,在Java应用程序的情况下。
RSS = Heap size + MetaSpace + OffHeap size

OffHeap包括线程堆栈、直接缓冲区、映射文件(库和jar)和JVM代码。

JDK 1.8.40以来,我们有了本机内存跟踪器

正如您所看到的,我已经将-XX:NativeMemoryTracking=summary属性添加到JVM中,因此我们可以从命令行调用它:

docker exec my-app jcmd 1 VM.native_memory summary

(这是原帖作者所做的)

不要担心“未知”部分 - 看起来 NMT 是一个不成熟的工具,无法处理 CMS GC(当您使用另一个 GC 时,此部分将消失)。

请记住,NMT 显示的是“已提交”内存,而不是“常驻”内存(通过 ps 命令获得)。换句话说,一个内存页面可以被提交而不被视为常驻(直到它被直接访问)

这意味着非堆区域(堆始终是预初始化的)的 NMT 结果可能比 RSS 值更大

(这就是 "为什么 JVM 报告的已提交内存比 Linux 进程常驻集大小更大?" 的原因)

作为结果,尽管我们将jvm堆限制设置为256m,但我们的应用程序消耗了367M。其中“其他”164M主要用于存储类元数据、已编译代码、线程和GC数据。
前三点通常是应用程序的常量,因此随着堆大小增加的唯一变化是GC数据。
这种依赖关系是线性的,但“k”系数(y=kx+b)远小于1。
更普遍的情况是,自docker 1.7以来,类似问题已被报告
我正在运行一个简单的Scala(JVM)应用程序,将大量数据加载到内存中并从内存中读取。 我将JVM设置为8G堆(-Xmx8G)。 我有一台132G内存的机器,但由于容器远远超过了我对JVM施加的8G限制,因此最多只能处理7-8个容器。
(docker stat之前被报道为误导性信息,因为它显然将文件缓存包括在总内存使用信息中) docker stat显示每个容器本身使用的内存比JVM应该使用的内存要多得多。例如:
CONTAINER CPU % MEM USAGE/LIMIT MEM % NET I/O
dave-1 3.55% 10.61 GB/135.3 GB 7.85% 7.132 MB/959.9 MB
perf-1 3.63% 16.51 GB/135.3 GB 12.21% 30.71 MB/5.115 GB

几乎可以认为JVM在向操作系统请求内存,然后在容器中分配内存,并且当其GC运行时释放内存,但是容器没有将内存释放回主操作系统。所以……内存泄漏。

2
@sunsin1985,“1.1 GB(RSS)和800 MB(Java总承诺内存)之间的内存消耗是什么?”我怀疑这些是文章中提到的“存储类元数据、编译代码、线程和GC数据”,此外,如果容器不释放内存,GC可能不会释放任何东西。 - VonC
谢谢,但这不会成为从Java NMT获得的“总承诺”数字的一部分吗? - sunsin1985
@sunsin1985 真(除非你的图表将值叠加在一起) - VonC
1
@sunsin1985 又一篇好文章:https://devcenter.heroku.com/articles/java-memory-issues,其中完整的Java选项为`JAVA_OPTS="-XX:NativeMemoryTracking=detail -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics"`。 - VonC
2
我认为这里的答案并不是针对所问的问题。问题是为什么Java进程RSS > JVM堆+非堆。而答案则是相反的问题:为什么JVM堆+非堆可能会大于Java进程RSS。@VonC,我有什么遗漏吗? - Artem Nakonechny
显示剩余4条评论

3
免责声明:我不是专家。
最近在高负载下遇到了一个生产事故,Pod的RSS(Resident Set Size)出现了大幅度跳变,导致Kubernetes杀死了这些Pod。没有OOM(Out of Memory)错误异常,但Linux以最严厉的方式停止了进程。
RSS和JVM总保留空间之间存在很大差距。堆内存、本地内存、线程等方面看起来都没问题,但RSS很大。
后来发现这是由于malloc内部工作原理造成的。malloc从内存中取一块块内存时,会有很大的空隙。如果机器上有很多核心,malloc会尝试适应并为每个核心分配自己的空间,以避免资源争用。设置export MALLOC_ARENA_MAX=2解决了这个问题。您可以在以下链接中了解更多相关情况:
  1. Java进程的增长住址空间使用(RSS)
  2. https://devcenter.heroku.com/articles/tuning-glibc-memory-behavior
  3. https://www.gnu.org/software/libc/manual/html_node/Malloc-Tunable-Parameters.html
  4. https://github.com/jeffgriffith/native-jvm-leaks
附言:我不知道为什么RSS内存会跳变。Pod是基于Spring Boot + Kafka构建的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接