Tomcat内存消耗超过了堆空间和永久代空间

14

我看到Tomcat的RAM消耗在操作系统和jVisualVM之间存在不匹配。

从htop可以看到,Tomcat JVM有993 MB的驻留内存。

从jVisualVM可以看到,Tomcat JVM正在使用:

  • 堆最大值:1,070,399,488 B
  • 堆大小:298.438.656 B
  • 堆已用:可变,介于170MB和270MB之间
  • PermGen最大值:268,435,456 B
  • PermGen大小:248,872,960 B
  • PermGen已用:略微变化,大约150MB

根据我的理解,操作系统内存消耗应该是Heap Size + PermGen Size ~= 522 MB。但这比我观察到的少了471 MB

有人知道我错过了什么吗?

PS:我知道我的最大堆远高于实际使用,但我假设如果JVM不使用它(即Heap Size较低),那应该没有影响。

谢谢! Marc


你的堆内存包括新生代和老年代吗? - Kevin
在你的帖子中,你两次提到了“PermGem Size”:“PermGen Size: 248,872,960 B,PermGen Size: 稍微变动,大约150MB”。这样可以吗? - jalopaba
+1 我也想了解这个。我在使用pergem时遇到了一些内存不足的错误,如果能给我一些想法就会很有帮助。 - gigadot
@Kevin 我假设它包括老年代和新生代 - Marc
4个回答

6
根据我的理解,操作系统内存消耗应该是堆大小 + PermGen 大小约等于 522 MB。但是这比我观察到的少了 471 MB。有人知道我漏掉了什么吗?
如果我理解问题正确,您所看到的是内存碎片化和JVM在其他领域的内存开销的结合。我们经常看到生产程序的内存使用量是我们从内存设置中期望看到的两倍。 内存碎片化可能意味着尽管JVM认为操作系统已经给它一些字节,但由于内存子系统的优化,还需要给出一定数量的字节。
就JVM开销而言,还有许多其他存储区域未包含在标准内存配置中。这里有一个关于此的很好的讨论。引用一下:
以下是不属于垃圾收集堆的一部分,但是是进程所需内存的一部分的示例:
  • 实现JVM的代码
  • 用于实现JVM的数据结构的C手动堆
  • 系统中所有线程(应用程序+JVM)的堆栈
  • 缓存的Java字节码(用于库和应用程序)
  • JIT编译的机器代码(用于库和应用程序)
  • 所有加载类的静态变量

1
所有的答案都给了很好的提示,但这个答案最终解释了大部分额外的内存。其中一点是XMX将在操作系统级别保留虚拟内存,但这不算作“已提交”或“常驻”内存(由XMS设置)。另一个要点是,使用jconsole我可以解释更多的“非堆”内存(约20MB)。最后,剩下的内存似乎是JVM本身所使用的,就像上面所描述的那样。 - Marc

4
首先,我们要记住的是:JVM进程堆(操作系统进程)= Java对象堆 + [永久空间 + 代码生成 + 套接字缓冲区 + 线程堆栈 + 直接内存空间 + JNI代码 + JNI分配内存 + 垃圾回收],其中在这个“集合”中,permSpace通常是最大的块。
鉴于此,我猜关键在于JVM选项-XX:MinFreeHeapRatio=n,其中n的取值范围为0到100,它指定如果堆的空闲空间小于n%,则应该扩展堆。通常默认值为40(Sun),因此当JVM分配内存时,它会获得足够的内存以获得40%的空闲空间(如果您的-Xms == -Xmx,则不适用此规则)。 "-XX:MaxHeapFreeRatio"的"双胞胎选项"通常默认为70(Sun)。
因此,在Sun JVM中,每次垃圾回收时,活动对象的比率保持在40-70%之间。如果GC后堆的空闲空间小于40%,则会扩展堆。因此,假设您正在运行Sun JVM,则“java对象堆”的大小已达到约445Mb的峰值,从而产生了约740 Mb的扩展“对象堆”(以保证40%的空闲空间)。然后,(对象堆)+(perm space)= 740 + 250 = 990 Mb。
也许您可以尝试输出GC详细信息或使用jconsole来验证堆大小的演变。
附注:在处理此类问题时,最好发布操作系统和JVM详细信息。

我今天刚调整了JVM并发现当Xms==Xmx=3000m时,从操作系统的角度来看,完整的内存分配已经达到了8-10GB左右......甚至在我将Xmx设置为7000时,由于进程占用了几乎所有16GB可用内存,内核会杀掉该进程,导致OOM killer。通常我看到如果Xmx=1g,则在操作系统级别上最大只能到1.5GB,但这是另一回事,目前不理解。这迫使我将Xmx降低到操作系统角度最大RAM的2.5倍。 - kensai
实际上,根据Oracle文档中的一项说明,他们指出:例如,在配置了1GB JVM的Linux上运行的HotSpot JVM大约会消耗1.2GB的RAM。 - kensai

3

在应用程序启动时,JVM将保留大约等于您堆最大值(-Xmx)大小加上一些其他内容的内存。这样可以避免JVM之后需要返回操作系统来保留更多内存。

即使您的应用程序仅使用了298mb堆空间,仍会有993mb与操作系统预留。您需要深入了解保留和提交内存的区别。

讨论垃圾收集时,大多数文章都是从堆的角度而不是操作系统层面进行分配。通过在启动时为应用程序保留内存,垃圾收集可以在自己的空间中运行。

如果需要更多细节,请阅读文章Tuning Garbage Collection。以下是文档中的一些重要摘录。

在初始化时,虚拟地保留了一定数量的最大地址空间,但只有在需要时才分配给物理内存。

还请查看文档中的第3.2节(iv)

在虚拟机初始化时,整个堆空间都被保留。可以使用-Xmx选项指定保留空间的大小。如果-Xms参数的值小于-Xmx参数的值,则保留的空间并不会立刻分配给虚拟机。

1

操作系统会报告JVM使用的内存和您的程序使用的内存。因此,它始终高于JVM报告的内存使用情况。 JVM本身需要一定量的内存才能执行您的程序,操作系统无法区分。

不幸的是,使用系统内存工具跟踪程序的内存消耗并不是一种非常精确的方法。 JVM通常分配大块内存,因此对象创建很快,但这并不意味着您的程序正在消耗该内存。

了解程序实际运行情况的更好方法是运行jconsole并查看内存使用情况。那是一个非常简单易用的查看内存的工具。


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