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 堆使用图 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)
  • 雄猫 6
  • 应用程序(点播 HTTP 视频流)
    • 通过 java.nio FileChannels 实现高 I/O
    • 数百到数千个线程
    • 数据库使用率低
    • 春天,休眠

相关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. 当前创建的应用程序最繁忙的实例
    1. 启动后一小时内有超过 1000 个新的 JRobin 数据库文件(每个大约 1.3MB)
    2. 开机后每天~100+
  3. 如果有要写的东西,应用程序每 15 秒更新一次这些 JRobin 数据库对象。
  4. 在默认配置 JRobin 中:
    1. 使用java.nio基于 - 的文件访问后端。此后端映射MappedByteBuffers到文件本身。
    2. 每五分钟一次,JRobin 守护线程调用MappedByteBuffer.force()每个 JRobin 底层数据库 MBB
  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

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

  • JVM 内部发生了MappedByteBuffer.force()什么?如果没有任何变化,它是否仍然写入整个文件?文件的一部分?它首先加载它吗?
  • RSS 中是否始终存在一定数量的 MBB?(RSS 大约是分配的 MBB 总大小的一半。巧合?我怀疑不是。)
  • 如果我移动MappedByteBuffer.force()到数据库更新事件,而不是定期计时器,问题会神奇地消失吗?
  • 为什么 RSS 斜率如此规则?它与任何应用程序负载指标无关。
4

4 回答 4

18

Just an idea: NIO buffers are placed outside the JVM.

EDIT: As per 2016 it's worth considering @Lari Hotari comment [ Why does the Sun JVM continue to consume ever more RSS memory even when the heap, etc sizes are stable? ] because back to 2009, RHEL4 had glibc < 2.10 (~2.3)

Regards.

于 2009-10-23T12:19:14.890 回答
14

RSS 表示正在使用的页面——对于 Java,它主要是堆中的活动对象,以及 JVM 中的内部数据结构。除了使用更少的对象或进行更少的处理之外,您无法做太多的事情来减小它的大小。

在你的情况下,我认为这不是问题。该图似乎显示消耗了 3 兆,而不是您在文本中写的 3 演出。这真的很小,不太可能导致分页。

那么您的系统中还发生了什么?是否有很多 Tomcat 服务器,每个服务器消耗 3M 的 RSS?你扔了很多 GC 标志,它们是否表明进程大部分时间都花在 GC 上?你有在同一台机器上运行的数据库吗?

根据评论进行编辑

关于 3M RSS 大小 - 是的,这对于 Tomcat 进程来说似乎太低了(我检查了我的框,并且有一个 89M 的 RSS 有一段时间没有激活)。但是,我不一定期望它大于堆大小,我当然也不期望它几乎是堆大小的 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,垃圾收集器将定期遍历整个对象图。如果这个对象图占据了大部分堆空间,那么收集器将接触堆中的每一页,要求所有这些页都驻留在内存中。GC 非常适合在每次主要收集后压缩堆,因此如果您使用部分堆运行,则大多数页面不需要在 RAM 中。

还有一些其他的选择

我注意到您提到有数百到数千个线程。这些线程的堆栈也会添加到 RSS 中,尽管应该不会太多。假设线程的调用深度很浅(应用程序服务器处理程序线程的典型情况),每个线程应该只消耗一两页物理内存,即使每个线程都有半兆的提交费用。

于 2009-10-23T12:01:00.287 回答
3

为什么会这样?“引擎盖下”发生了什么?

JVM 使用的内存不仅仅是堆。例如,Java 方法、线程堆栈和本机句柄被分配在与堆分开的内存中,以及 JVM 内部数据结构中。

在您的情况下,可能导致问题的原因可能是:NIO(已经提到)、JNI(已经提到)、过多的线程创建。

关于 JNI,您写道该应用程序没有使用 JNI,但是……您使用的是什么类型的 JDBC 驱动程序?会不会是 2 型,并且会泄漏?尽管您说数据库使用率很低,但这不太可能。

关于过多的线程创建,每个线程都有自己的堆栈,可能非常大。堆栈大小实际上取决于虚拟机、操作系统和体系结构,例如对于JRockit,它在 Linux x64 上为 256K,我在 Sun 的 Sun 虚拟机文档中没有找到参考。这直接影响线程内存(线程内存 = 线程堆栈大小 * 线程数)。如果你创建和销毁大量线程,内存可能不会被重用。

我可以做些什么来控制 JVM 的实际内存消耗?

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

于 2009-10-23T14:35:48.620 回答
1

The current garbage collector in Java is well known for not releasing allocated memory, although the memory is not required anymore. It's quite strange however, that your RSS size increases to >3GB although your heap size is limited to 640MB. Are you using any native code in your application or are you having the native performance optimization pack for Tomcat enabled? In that case, you may of course have a native memory leak in your code or in Tomcat.

With Java 6u14, Sun introduced the new "Garbage-First" garbage collector, which is able to release memory back to the operating system if it's not required anymore. It's still categorized as experimental and not enabled by default, but if it is a feasible option for you, I would try to upgrade to the newest Java 6 release and enable the new garbage collector with the command line arguments "-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC". It might solve your problem.

于 2009-10-23T12:19:57.783 回答