为什么当堆大小等保持稳定时,Sun JVM仍然不断消耗更多的RSS内存?

33

在过去一年中,我在我的应用程序的Java堆使用方面取得了巨大的进展 - 实现了坚实的66%的减少。为此,我一直在通过SNMP监视各种指标,例如Java堆大小、CPU、Java非堆等等。

最近,我一直在监视JVM消耗的实际内存(RSS,常驻集)并感到有些惊讶。JVM消耗的实际内存似乎完全独立于我的应用程序堆大小、非堆、伊甸园空间、线程数等等。

Java SNMP测量的堆大小 Java堆使用图表 http://lanai.dietpizza.ch/images/jvm-heap-used.png

占用的实际内存(以KB为单位,例如:1 MB的KB = 1 GB) Java RSS图表 http://lanai.dietpizza.ch/images/jvm-rss.png

(堆图中的三个低谷对应应用程序的更新/重启。)

这对我来说是一个问题,因为JVM所消耗的所有额外内存都会“窃取”操作系统可用于文件缓存的内存。实际上,一旦RSS值达到约2.5-3GB,我开始看到我的应用程序响应时间变慢和CPU利用率更高,主要是由于IO等待。在某个时刻,会开始进行交换分区的页面交换。这都是非常不希望看到的。

所以,我的问题是:

  • 为什么会发生这种情况?在“幕后”发生了什么?
  • 我该怎么做才能控制JVM的实际内存消耗?

详细信息如下:

  • RHEL4 64位(Linux - 2.6.9-78.0.5.ELsmp #1 SMP Wed Sep 24 ... 2008 x86_64 ... GNU / Linux)
  • Java 6(构建1.6.0_07-b06)
  • Tomcat 6
  • 应用程序(按需HTTP视频流)
    • 通过java.nio FileChannels进行高I/O
    • 几百到几千个线程
    • 低数据库使用率
    • Spring、Hibernate

相关的JVM参数:

-Xms128m  
-Xmx640m  
-XX:+UseConcMarkSweepGC  
-XX:+AlwaysActAsServerClassMachine  
-XX:+CMSIncrementalMode    

-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps  
-XX:+PrintGCApplicationStoppedTime  
-XX:+CMSLoopWarn  
-XX:+HeapDumpOnOutOfMemoryError 

如何测量 RSS:

ps x -o command,rss | grep java | grep latest | cut -b 17-

这个输出被写入文本文件,并在定期间隔内由监视系统读入RRD数据库。注意,ps输出的是千字节。


问题与解决方案:

最终证明正确的是ATorras的答案,但是kdgregory通过使用pmap指导我正确的诊断路径。 (请为他们的答案投票!)以下是发生的情况:

我确定的事情:

  1. 我的应用程序使用JRobin 1.4记录和显示数据,这是我三年前编码到我的应用程序中的。
  2. 当前应用程序最繁忙的实例会在启动后的一小时内创建超过1000个新的JRobin数据库文件(每个文件大小约为1.3MB),每天会创建大约100个以上。
  3. 如果有数据需要写入,则应用程序每15秒更新这些JRobin数据库对象一次。
  4. 在默认配置中,JRobin:
    1. 使用基于java.nio的文件访问后端。 此后端将MappedByteBuffers映射到文件本身。
    2. 每5分钟,JRobin守护进程线程会在每个JRobin基础数据库MBB上调用MappedByteBuffer.force()
  5. pmap列出:
    1. 6500个映射
    2. 其中有5500个1.3MB的JRobin数据库文件,这相当于约7.1GB

那是我“有所领悟”的时刻。

我的纠正措施:

  1. 考虑更新为最新的JRobinLite 1.5.2,这明显更好。
  2. 对JRobin数据库实现正确的资源处理。 目前,一旦我的应用程序创建了数据库,就不再将其转储,而在数据库不再被主动使用后。
  3. 尝试将MappedByteBuffer.force()移动到数据库更新事件中,而不是周期性计时器中。 这个问题会神奇地消失吗?
  4. 立即将JRobin后端更改为java.io实现-一行代码更改。 这将更慢,但可能并不是问题。 这是一个显示此更改的立即影响的图表。

Java RSS内存使用情况图表http://lanai.dietpizza.ch/images/stackoverflow-rss-problem-fixed.png

我可能没有时间弄清楚的问题:

  • MappedByteBuffer.force()在JVM内部发生了什么? 如果没有任何更改,它是否仍然写入整个文件? 部分文件? 它先加载吗?
  • MBB的一定数量始终保持在RSS中吗? (RSS大约是总分配MBB大小的一半。 这是巧合吗? 我怀疑不是。)
  • 如果将MappedByteBuffer.force()移到数据库更新事件中而不是周期性计时器中,这个问题是否会神奇地消失?
  • RSS

感谢提供额外信息--这确实澄清了您处于一个不寻常的情况。我认为ATorras的想法是正确的,所以不会对我的答案进行更多编辑(这可能对处于不那么不寻常情况下的人有用)。不幸的是,除非您不关闭通道,否则我怀疑唯一的解决方案是水平扩展。或者,我想,增加更多物理内存,但这也最终会耗尽。 - kdgregory
起初我也认为ATorras的想法是正确的,但后来我意识到我期望服务器活动和RSS大小轨迹之间存在相关性。但实际上并没有。事实上,它非常稳定。这让人感到困惑... - Stu Thompson
2
好的,再提一个建议:每天(或两次)从重新启动开始对进程进行一次pmap,并查找差异。这些文件将非常庞大,大部分输出将是表示内存映射段的“anon”块。我希望这些“anon”块按大小分组:线程堆栈为1/2兆,文件通道为其他值。差异至少会让你知道是什么在消耗你的虚拟映射,这应该会导致找到停留在内存中的内容。 - kdgregory
实际上,FileChannel 应该在 pmap 输出中显示为命名段。 - kdgregory
1
好的,那么另一个评论(并且有一点自我推销):http://www.kdgregory.com/index.php?page=java.outOfMemory 可能会在查看pmap输出时对你有所帮助(页面滚到底部)。 - kdgregory
显示剩余5条评论
4个回答

18

+1 - 尽管这只会影响到正在被主动访问的文件。 - kdgregory
3
对此的反驳是:a) RSS图形具有惊人的规律的直线斜率,b) FileChannel的使用情况与应用程序的繁忙程度有关,后者在每小时、每天都会波动。我期望看到它们之间存在一种相关性。 - Stu Thompson
1
请参考 https://dev59.com/L18e5IYBdhLWcg3wJXvm 获取解决方案。 - Lari Hotari

14

RSS代表活跃使用的页面,对于Java而言,主要指堆中的实时对象和JVM中的内部数据结构。除了使用更少的对象或进行更少的处理,很难减小其大小。

在您的情况下,我认为这不是一个问题。图表显示消耗了3兆字节,而不是您在文本中写的3吉字节。这非常小,不太可能导致分页。

那么您的系统还发生了什么?您是否有许多Tomcat服务器,每个服务器都消耗3M的RSS?您添加了许多GC标志,它们是否表示进程大部分时间都在进行垃圾回收?您是否在同一台机器上运行数据库?

根据评论进行编辑

关于3M RSS大小-是的,这似乎对于Tomcat进程来说太低了(我检查了我的电脑,有一个长时间没有活动的Tomcat进程大小为89M)。但我并不希望它大于堆大小,我也肯定不希望它几乎是堆大小的5倍(您使用-Xmx640)-最坏的情况应该是堆大小+一些每个应用程序恒定值。

这让我怀疑您的数字。因此,请运行以下命令以获取快照(将7429替换为您正在使用的进程ID):

ps -p 7429 -o pcpu,cutime,cstime,cmin_flt,cmaj_flt,rss,size,vsize

(由Stu编辑,以便我们可以获得格式化的结果来响应上述有关ps信息的请求:)

[stu@server ~]$ ps -p 12720 -o pcpu,cutime,cstime,cmin_flt,cmaj_flt,rss,size,vsize
%CPU - - - -  RSS SZ  VSZ
28.8 - - - - 3262316 1333832 8725584

编辑以解释这些数字供后人参考

RSS,如前所述,是常驻内存集的大小:物理内存中的页面。SZ保存了进程可写的页面数(提交费用);手册将此值描述为“非常粗略”。VSZ保存进程的虚拟内存映射的大小:可写页面加上共享页面。

通常,VSZ略大于SZ,而且远远大于RSS。此输出表明了一个非常不寻常的情况。

详细说明为什么唯一的解决方案是减少对象

RSS表示在RAM中驻留的页面数量--活动访问的页面。对于Java,垃圾收集器会定期遍历整个对象图。如果该对象图占用了大部分堆空间,则收集器将触及堆中的每个页面,需要所有这些页面变为内存驻留页面。垃圾回收器在每次主要收集后非常擅长压缩堆,因此,如果您使用部分堆运行,则大多数页面应该不需要存在RAM中。

还有其他选项

我注意到您提到具有数百到数千个线程。这些线程的堆栈也会增加RSS,尽管不应该太多。假设线程具有浅层调用深度(对于应用程序服务器处理程序线程而言很典型),则每个线程应仅消耗一页或两页的物理内存,尽管每个线程的提交费用为半兆字节。


GC 时间看起来还行。我会继续监测它们。就像我所说的,io 等待时间正在增加。我可以看到,与 JVM 没有吸收大量实际内存时相比,系统文件缓存缩小到了非常小的数量。 - Stu Thompson
RSS值为3GB,而不是3MB。图表以千字节为单位。3兆字节的KB = 3GB。我会更新问题以便更清楚地理解。(此外,人们可以合理地期望实际内存大于Java堆大小。3MB只是400MB的一小部分。) - Stu Thompson
正如您可以从ps输出中看到的那样(编辑到您的答案中),3GB的数字是准确的。(在注意到长时间运行的实例中topps显示大量数字后,我开始随时间绘制图形。)这让我感到惊讶——如果我的RSS值是堆大小的5倍,似乎有些不对劲。因此出现了这个SO问题。 - Stu Thompson

3
JVM使用的内存不仅限于堆。例如Java方法、线程堆栈和本机句柄都分配在与堆不同的内存中,以及JVM内部数据结构。
在您的情况下,麻烦的可能原因是:NIO(已经提到)、JNI(已经提到)和过多的线程创建。
关于JNI,您写道应用程序没有使用JNI,但是...您使用了哪种类型的JDBC驱动程序?它可能是类型2并且泄漏吗?尽管您说数据库使用率很低,但这是非常不可能的。
关于过多的线程创建,每个线程都有自己的堆栈,可能相当大。堆栈大小实际上取决于VM、操作系统和架构,例如对于JRockit,在Linux x64上为256K,在Sun的VM文档中未找到参考。这直接影响线程内存(线程内存=线程堆栈大小*线程数)。如果您创建和销毁大量线程,则可能不会重复使用内存。

我该如何控制JVM的实际内存消耗?

说实话,对我来说,数百到几千个线程似乎是巨大的。话虽如此,如果您确实需要那么多线程,可以通过-Xss选项配置线程堆栈大小。这可能会减少内存消耗。但我不认为这能解决整个问题。当我查看实际内存图时,我倾向于认为存在某种泄漏。


线程:我的应用程序线程,特别是处理HTTP连接的线程,经常存在很长时间:几十秒、几分钟或更长时间。我的服务器可以同时处理的连接数(HTTP流的数量)是我可以拥有的线程数量的线性函数。在日常使用中,就像上面的图表范围内,线程数量在50到700之间变化。这是一个不寻常的应用程序。 - Stu Thompson
JNI:很好的观点,我不知道它是什么类型。但我暂时不想调查,因为正如你所指出的,我的应用程序不是DB密集型的。 - Stu Thompson
Xss:这是我考虑过的一件事,但数学并不表明这是个问题。即使有1000个线程,每个线程256k,我们仍然只有256MB。更实际地说,我的应用程序的线程栈总共128MB。这两个值都与我所遇到的3GB空洞相去甚远。现在调整“-Xss?”只会是过早优化,最坏的情况下是随机猜测。 - Stu Thompson
无论如何,感谢您的想法。看起来我会在“实际内存图”中找到问题,这对我来说是一个全新的世界。 - Stu Thompson

1

Java中的当前垃圾收集器因不释放已分配的内存而闻名,尽管该内存不再需要。然而,如果您的RSS大小增加到>3GB,尽管堆大小限制为640MB,则相当奇怪。您是否在应用程序中使用任何本机代码或启用了Tomcat的本机性能优化包?在这种情况下,您可能会在代码或Tomcat中有本机内存泄漏。

随着Java 6u14的推出,Sun引入了新的“Garbage-First”垃圾收集器,它能够将不再需要的内存释放回操作系统。它仍被归类为实验性质,并且默认情况下未启用,但如果这对您来说是可行的选项,我建议您升级到最新的Java 6版本,并使用命令行参数“-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC”启用新的垃圾收集器。这可能会解决您的问题。


没有使用JNI,但应用程序在将数据从磁盘发送到网络接口卡时,确实大量依赖于java.nio.FileChannel - Stu Thompson
你没有使用本地Tomcat功能(http://tomcat.apache.org/tomcat-6.0-doc/apr.html)吗?除非你正在保留大量打开的FileChannel对象的引用(这将导致其他问题,比如达到允许的最大打开文件数),否则仅使用FileChannel是无法解释应用行为的。 - jarnbjo
不,我不是。a) 在64位Linux上它有点不稳定(或者上次我检查时是这样的),b) 我在一个第三方jar包中遇到了问题,c) 我真的没有那么多每秒连接来担心连接器性能。 - Stu Thompson

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