Java在大量内存上的-Xmx参数表现异常

7
您可以使用-Xmx选项控制Java中的最大堆大小。
我们在Windows上使用此选项时遇到了一些奇怪的行为。我们运行一些非常强大的服务器(考虑196GB RAM)。Windows版本是Windows Server 2008R2。
Java版本为1.6.0_18,64位(显然)。
无论如何,我们遇到了一些奇怪的错误,即使进程使用的内存比-Xmx设置指定的内存少得多,进程也会退出并报告内存不足异常。
因此,我们编写了一个简单的程序,每次按回车键时都会分配1GB的字节数组,并将字节数组初始化为随机值(以防止任何内存压缩等)。
基本上,如果我们使用-Xmx35000m(大约35 GB)运行程序,则在达到25 GB的进程空间(使用Windows任务管理器进行测量)时会出现内存不足异常。顺便说一下,我们分配了24 GB的1 GB块后就会达到这个限制。
仅仅指定更大的-Xmx选项值就可以让程序正常工作到更大的内存量。
那么,到底发生了什么?-Xmx是否关闭了?顺便说一句:我们需要指定-Xmx55000m才能获得35 GB的进程空间...
有关正在发生什么的任何想法吗?
Windows JVM中是否存在错误?
即使-Xmx选项与进程情况之间存在不一致,将-Xmx选项设置得更大是否安全?

除非你调整NewRatio,否则你基本上不会使用年轻代来处理如此巨大的对象。尝试使用明显较小的byte[]进行测试,结果将会改善。 - bestsss
当块大小分别设置为1KB和10KB时,没有任何变化,因此我认为对象的大小不是问题。 - SvrGuy
5个回答

8
理论#1
当您使用-Xmx35000m请求35GB堆时,您实际上是在说允许用于堆的总空间为35GB。但是总空间包括Tenured Object空间(用于存活多个GC周期的对象),新创建对象的Eden空间以及垃圾收集期间将对象复制到的其他空间。
问题在于,一些空间不能用于分配新对象。因此,事实上,您失去了相当大比例的35GB用于开销。
有各种-XX选项可用于调整各个空间的大小等。您可以尝试调整它们,看看它们是否有所不同。有关更多信息,请参见此文档。(常用的GC调整选项列在第8节中。-XX:NewSpace选项看起来很有前途...)
理论#2
这可能是因为您正在分配巨大的对象。如果我没记错,某个大小以上的对象可以直接分配到Tenured Object空间中。在您的(高度人工)基准测试中,这可能导致JVM不将东西放入Eden空间,因此能够使用比正常情况下少得多的总堆空间。
作为一个实验,尝试更改您的基准测试以分配大量小对象,并查看它是否能够在OOM之前使用更多可用空间。
以下是我会排除的其他理论:
- “您遇到了操作系统强制限制。”我会排除这一点,因为您说通过增加-Xmx...设置可以获得显着更大的内存利用率。 - “Windows任务管理器报告虚假数字。”我会排除这一点,因为报告的数字大致与您认为应用程序已成功分配的25GB相匹配。 - “您正在失去其他东西的空间;例如permgen堆。”据我所知,permgen堆大小独立于“正常”堆控制和计算。其他非堆内存使用量要么是一个常数(对于应用程序),要么取决于应用程序执行特定操作。 - “您正在遭受堆碎片化。”所有JVM垃圾收集器都是“复制收集器”,并且这个收集器系列具有自动压缩堆节点的属性。 - “Windows上的JVM错误。”高度不可能。必须有成千上万的64位Java Windows安装程序最大化堆大小。其他人也会注意到...

最后,如果你不是因为应用程序需要在巨大的块中分配内存,并且“永久”地保留它而执行此操作...那么你很有可能是在追逐幻影。一个“正常”的大内存应用程序不会做这种事情,JVM 也是针对正常应用程序进行调整的...而不是异常的应用程序。

如果你的应用程序确实表现出这种行为,务实的解决方案就是将 -Xmx...选项设置得更大,只有当你开始遇到操作系统级别的问题时才需要担心。


@bestsss - 显然不是;请看问题下面对你评论的回复。 - Stephen C

2
为了确切地了解您正在测量的内容,您应该使用一些不同的工具:
  1. Windows任务管理器(我只知道Windows XP,但我听说任务管理器自那以后有所改进。)
  2. Sysinternals的procexpvmmap
  3. JVM的jconsole(您正在使用SunOracle HotSpot JVM,是吗?)
现在,您应该回答以下问题:
  • jconsole关于使用的堆大小有什么说法?与procexp有何不同?
  • 如果您将字节数组填充为非零数字而不是保持其为0procexp中的值是否会更改?

我更喜欢JDK中的VisualVM而不是jconsole,但对于Java本地工具我还是给一个赞。 - user330315
哦,是的,我应该真正熟悉所有本地HotSpot工具。 - Roland Illig

2

您是否尝试打开GC的详细输出以查找上次分配失败的原因。是因为操作系统无法为本机JVM进程分配超过25GB的堆,还是因为GC达到了它可以管理的最大内存限制。我建议您还连接到使用jconsole的命令行进程,以在分配失败之前查看堆的状态。此外,像sysinternals进程资源管理器这样的工具可能会更好地说明如果故障发生在jvm进程中的位置。

由于进程在25GB处崩溃,而您有一个分代收集器,也许其余的代消耗了10GB。我建议您安装JDK 1.6_u24并使用jvisualvm和visualGC插件,特别是考虑所有代的大小,以查看GC正在做什么,以及GC / VM内存管理器如何将35GB堆切成不同的区域。

如果您不熟悉分代GC,请参阅此链接http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html#generation_sizing.total_heap


[GC 11539579K->11534560K(12440896K), 0.0411290秒] [GC 11534560K->11534528K(12440896K), 0.0311573秒] [Full GC 11534528K->11534499K(11950336K), 0.0139061秒] [GC 11534499K->11534499K(12440896K), 0.0317842秒] [Full GC 11534499K->11534475K(11977280K), 0.0139566秒] - SvrGuy
以上是冗长的GC输出,其中设置了-Xmx18000m。进程在大约使用11GB内存时退出。 - SvrGuy
你是否在使用 -XX:-PrintGCDetails 来输出信息,还是只用了 --XX:-PrintGC?能否分享一下 -XX:-PrintGCDetails 的输出结果? - ams

0

我猜这与堆的分段有关。可用的空闲内存可能不是一个单一的连续空闲区域,当您尝试分配一个大块时,会失败,因为请求的内存无法在一个单一的块中分配。


1
通常我也会建议这样做,但SvrGuy提到他以1 GB块分配内存。即使如此,在进行完整的垃圾回收和堆压缩后,他应该能够再次使用整个堆。也许跟踪垃圾收集器(-verbose:gc -Xloggc)有助于追踪真正的原因。 - Roland Illig
我们编写测试程序的原因是为了避免堆碎片化的可能性。使用1GB的块,并且所有对象都是可达的,因此没有进行垃圾回收。 - SvrGuy

0

Windows任务管理器显示的内存是分配给进程的总内存,包括代码、堆栈、永久代和堆内存。

您使用点击程序测量的内存是JVM为正在运行的JVM程序提供的堆内存数量。

自然地,Windows分配给JVM的总内存应该大于JVM为您的程序提供的堆内存。


这是正确的,但并不能解释30-50%的差距。代码大约有500MB,而不是15GB。 - SvrGuy

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