64位JVM为什么在达到xmx之前就抛出内存溢出?

12

我正在为Java应用程序的大内存需求而苦苦挣扎。

为了解决更多的内存问题,我已经切换到64位JVM并使用了较大的Xmx。但是,当Xmx超过2GB时,该应用程序似乎比预期更早地耗尽内存。在使用2400M Xmx运行并查看来自-verbosegc的GC信息时,我得到了...

[Full GC 2058514K->2058429K(2065024K), 0.6449874 secs] 

...然后它抛出了内存不足的异常。我本来期望它在耗尽内存之前将堆大小增加到2065024K以上。

举一个简单的例子,我有一个测试程序,在循环中分配内存,并从Runtime.getRuntime().maxMemory()Runtime.getRuntime().totalMemory()打印信息,直到最终耗尽内存。

通过对各种xmx值运行此代码,似乎Runtime.getRuntime().maxMemory()报告的值比xmx小约10%,而总内存不会超过Runtime.getRuntime().maxMemory()的90%。

我使用以下64位jvm:

java version "1.6.0_26"
Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode)

以下是代码:

import java.util.ArrayList;

public class XmxTester {


private static String xmxStr;

private long maxMem;
private long usedMem;
private long totalMemAllocated;
private long freeMem;


private ArrayList list;

/**
 * @param args
 */
public static void main(String[] args) {

xmxStr = args[0];
XmxTester xmxtester = new XmxTester();
}

public XmxTester() {

byte[] mem = new byte[(1024 * 1024 * 50)];

list = new ArrayList();
while (true) {
    printMemory();
    eatMemory();
}

}

private void eatMemory() {
// TODO Auto-generated method stub
byte[] mem = null;
try {
    mem = new byte[(1024 * 1024)];
} catch (Throwable e) {
    System.out.println(xmxStr + "," + ConvertMB(maxMem) + ","
        + ConvertMB(totalMemAllocated) + "," + ConvertMB(usedMem)
        + "," + ConvertMB(freeMem));

    System.exit(0);
}

list.add(mem);

}

private void printMemory() {
maxMem = Runtime.getRuntime().maxMemory();
freeMem = Runtime.getRuntime().freeMemory();
totalMemAllocated = Runtime.getRuntime().totalMemory();
usedMem = totalMemAllocated - freeMem;


}

double ConvertMB(long bytes) {

int CONVERSION_VALUE = 1024;

return Math.round((bytes / Math.pow(CONVERSION_VALUE, 2)));

}

}

我使用这个批处理文件在多个xmx设置上运行它。它包含对32位JVM的引用,我想与32位jvm进行比较 - 显然,一旦xmx大于约1500M,此调用将失败。

@echo off
set java64=<location of 64bit JVM>
set java32=<location of 32bit JVM>
set xmxval=64


:start


SET /a xmxval  = %xmxval% + 64

 %java64%  -Xmx%xmxval%m  -XX:+UseCompressedOops -XX:+DisableExplicitGC XmxTester %xmxval%

%java32% -Xms28m -Xmx%xmxval%m   XmxTester %xmxval%

if %xmxval% == 4500 goto end
goto start
:end
pause
这将输出一个csv文件,导入excel后的格式如下(对我的糟糕格式表示歉意): 32位 XMX max mem total mem free mem %of xmx used before out of mem exception 128 127 127 125 2 98.4% 192 191 191 189 1 99.0% 256 254 254 252 2 99.2% 320 318 318 316 1 99.4% 384 381 381 379 2 99.5% 448 445 445 443 1 99.6% 512 508 508 506 2 99.6% 576 572 572 570 1 99.7% 640 635 635 633 2 99.7% 704 699 699 697 1 99.7% 768 762 762 760 2 99.7% 832 826 826 824 1 99.8% 896 889 889 887 2 99.8% 960 953 953 952 0 99.9% 1024 1016 1016 1014 2 99.8% 1088 1080 1080 1079 1 99.9% 1152 1143 1143 1141 2 99.8% 1216 1207 1207 1205 2 99.8% 1280 1270 1270 1268 2 99.8% 1344 1334 1334 1332 2 99.9% 64位 128 122 122 116 6 90.6% 192 187 187 180 6 93.8% 256 238 238 232 6 90.6% 320 285 281 275 6 85.9% 384 365 365 359 6 93.5% 448 409 409 402 6 89.7% 512 455 451 445 6 86.9% 576 512 496 489 7 84.9% 640 595 595 565 30 88.3% 704 659 659 629 30 89.3% 768 683 682 676 6 88.0% 832 740 728 722 6 86.8% 896 797 772 766 6 85.5% 960 853 832 825 6 85.9% 1024 910 867 860 7 84.0% 1088 967 916 909 6 83.5% 1152 1060 1060 1013 47 87.9% 1216 1115 1115 1068 47 87.8% 1280 1143 1143 1137 6 88.8% 1344 1195 1174 1167 7 86.8% 1408 1252 1226 1220 6 86.6% 1472 1309 1265 1259 6 85.5% 1536 1365 1317 1261 56 82.1% 1600 1422 1325 1318 7 82.4% 1664 1479 1392 1386 6 83.3% 1728 1536 1422 1415

1
32位JVM在某些硬件平台上只有在1500 MB时才会失败。 - Thorbjørn Ravn Andersen
1个回答

13

为什么会这样呢?

基本上,JVM / GC可以使用两种策略来决定何时放弃并抛出OOME。

  • 它可以持续进行下去,直到垃圾收集后没有足够的内存来分配下一个对象。

  • 它可以一直运行,直到JVM花费的垃圾收集器运行时间超过给定的百分比。

第一种方法的问题是对于典型应用程序,JVM将花费越来越多的时间运行GC,最终徒劳无功地完成任务。

第二种方法的问题是它可能会放弃得太快。


GC在这个区域的实际行为由JVM选项(-XX:...)控制。显然,默认行为在32位和64位JVM之间不同。这有点说得通,因为(直观地说),对于64位JVM,“内存不足的死亡螺旋”效应将持续更长时间,更加明显。


我的建议是不要管这个问题。除非您真的需要用所有的内存填满东西,否则让JVM早早地死亡并避免浪费大量时间是更好的选择。然后,您可以使用更多的内存重新启动它并完成工作。

显然,您的基准测试是非典型的。大多数真实程序根本不会尝试抓取堆的所有内容。您的应用程序也可能是非典型的。但是,您的应用程序也可能遭受内存泄漏的影响。如果是这种情况,您应该调查泄漏而不是试图弄清楚为什么不能使用所有内存。


但我的问题主要在于为什么它不尊重我的xmx设置。

正在尊重! -Xmx是堆大小的上限,而不是决定何时放弃的标准。

我设置了XMX为2432M,但是要求JVM返回其最大内存的理解,结果只有2162M。

它返回的是已使用的最大内存,而不是允许使用的最大内存。

请参见上文。

此外,当堆达到2006M时,为什么它不将堆扩展至至少2162 M?

我认为这是因为JVM已经达到“垃圾回收所花费时间过长”的阈值。

这是否意味着在64位JVM中,应该将XMX设置虚高11%以达到预期的最大值?

通常情况下不是。调整措施取决于您的应用程序。例如,具有更高的对象翻转率(即每个有用工作单元创建和丢弃的对象数量更多)的应用程序可能更容易遇到OOME。

我可以根据数据库大小预测要求,并具有调整xmx的包装器,但是我的问题是11%问题,其中我的监视器显示应用程序需要2 GB,因此我设置了2.4GB的xmx。然而,jvm只允许堆增长到2006M,而不是应有的400MB的'headroom'。

我认为解决方案是在当前设置的基础上额外增加20%(或更多)。假设您有足够的物理内存,给JVM一个更大的堆将减少总体GC开销并使应用程序运行更快。

其他可以尝试的技巧是将-Xmx和-Xms设置为相同的值,并调整设置最大“垃圾回收所花费时间过长”的比率的调优参数。


谢谢Stephen,64位JVM在32位JVM之前“放弃”这一点很有道理。然而,我的问题主要是为什么它不遵守我的xmx设置。在2.4 GB的示例中,我已经设置了2432M的XMX,但是要求JVM返回其对最大内存的理解时,返回2162M。为什么它“认为”最大内存比xmx少11%?此外,当堆达到2006M时,为什么它不将堆至少扩展到2162?这是否意味着在64位JVM中,应该将XMX设置虚高11%以上,以达到预期的最大值? - Al Quinn
我很感激你提到内存泄漏的问题,我的测试程序显然是一个内存泄漏——将一系列1MB字节数组分配到列表中...然而,真实世界的情况是在一个内存中的Java数据库之上的应用程序,随着数据库的增长,内存需求也会增加。我可以根据数据库大小预测需求,并有一个包装器来调整xmx,但是我有11%的问题,监视表明应用程序需要2GB,所以我设置了2.4GB的xmx。然而,与其预期的400MB“余地”相比,jvm只允许堆增长到2006M。 - Al Quinn
好的,我现在明白了。理论上,内存使用量会增长到最大值,但是考虑到对象的频繁变动率,GC(根据您上述的第二个选项)决定继续下去是一个毫无意义的举动,所以放弃并抛出内存不足的异常。感谢您的帮助。我同意将安全边界增加20%,这将解决我的问题。还有感谢您提供的xms=xmx提示-这似乎可以在gc放弃之前允许堆增长更大。 - Al Quinn

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