Почему 64-битная JVM выбрасывает Out Of Memory до достижения xmx?

Я борюсь с большими требованиями к памяти для приложения java.

Чтобы решить проблему с большим объемом памяти, я переключился на 64-битную JVM и использую большой xmx. Однако, когда xmx превышает 2 ГБ, приложение, похоже, исчерпало память раньше, чем ожидалось. При работе с xmx 2400M и просмотром информации GC от -verbosegc я получаю...

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

... и затем он выдает исключение из памяти. Я ожидаю, что он увеличит кучу выше 2065024K до исчерпания памяти.

В тривиальном примере у меня есть тестовая программа, которая выделяет память в цикле и выводит информацию из Runtime.getRuntime().maxMemory() и Runtime.getRuntime().totalMemory() до тех пор, пока в конечном итоге не закончится память.

Запуск этого значения в диапазоне значений xmx показывает, что Runtime.getRuntime().maxMemory() сообщает о 10% меньше, чем xmx, и что общая память не будет расти выше 90% Runtime.getRuntime().maxMemory().

Я использую следующие 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 больше, чем около 1500 М

@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  7  81.9%
1792  1593  1455  1448  6  80.8%
1856  1650  1579  1573  6  84.8%
1920  1707  1565  1558  7  81.1%
1984  1764  1715  1649  66  83.1%
2048  1821  1773  1708  65  83.4%
2112  1877  1776  1769  7  83.8%
2176  1934  1842  1776  66  81.6%
2240  1991  1899  1833  65  81.8%
2304  2048  1876  1870  6  81.2%
2368  2105  1961  1955  6  82.6%
2432  2162  2006  2000  6  82.2%

Ответ 1

Почему это происходит?

В принципе, есть две стратегии, которые JVM/GC может использовать, чтобы решить, когда отказаться и выбросить OOME.

  • Он может продолжать идти и идти, пока не будет достаточно памяти после сбора мусора, чтобы выделить следующий объект.

  • Это может продолжаться до тех пор, пока JVM не потратит больше определенного процента времени на сборщик мусора.

Первый подход имеет проблему, что для типичного приложения JVM будет тратить больший и больший процент своего времени на выполнение GC, в конечном итоге бесполезное усилие для выполнения задачи.

У второго подхода есть проблема, что он может скоро отказаться.


Фактическое поведение GC в этой области определяется параметрами JVM (-XX:...). По-видимому, поведение по умолчанию отличается от 32 до 64 бит JVM. Этот тип имеет смысл, потому что (интуитивно) эффект "смерти из спирали" для 64-битной JVM будет длиться дольше и быть более выраженным.


Мой совет должен был оставить эту проблему в покое. Если вам действительно не нужно заполнять каждый последний байт памяти, лучше, чтобы JVM умерла раньше и не тратила много времени. Затем вы можете перезапустить его с большим объемом памяти и выполнить задание.

Очевидно, что ваш тест является нетипичным. Большинство реальных программ просто не пытаются захватить всю кучу. Возможно, ваше приложение тоже нетипично. Но также возможно, что ваше приложение страдает от утечки памяти. Если это так, вам следует исследовать утечку, а не пытаться выяснить, почему вы не можете использовать всю память.


Однако моя проблема в основном связана с тем, почему она не соблюдает мои настройки xmx.

Он чествовал его! -Xmx - верхний предел размера кучи, а не критерий для принятия решения о сдаче.

Я установил XMX из 2432M, но попросил JVM вернуть свое понимание максимального количества возвратов памяти 2162M.

Он возвращает максимальную память, которую он использовал, а не максимальную память, которую он может использовать.

Почему "думает", что максимальная память на 11% меньше, чем xmx?

См. выше.

Кроме того, почему, когда куча поражает 2006M, она не расширяет кучу как минимум до 2162?

Я предполагаю, что это связано с тем, что JVM попала в порог "слишком много времени, затраченного на сбор мусора".

Означает ли это, что в 64-битных JVM следует подталкивать настройку XMX на 11% выше, чем предполагаемый максимум?

Не в общем. Фактор вымывания зависит от вашего приложения. Например, приложение с большей скоростью оттока объекта (т.е. Больше объектов, созданных и отброшенных на единицу полезной работы) скорее всего умрет с OOME раньше.

Я могу предсказать требования, основанные на размере db, и иметь обертку, которая настраивает xmx, однако имеет 11% -ную проблему, в соответствии с которой мое наблюдение предполагает, что приложение нуждается в 2 ГБ, поэтому я установил 2,4 ГБ xmx. однако вместо того, чтобы иметь ожидаемый 400 МБ "запаса", jvm позволяет только куче вырасти до 2006М.

IMO, решение состоит в том, чтобы просто добавить дополнительные 20% (или более) поверх того, что вы сейчас добавляете. Предполагая, что у вас достаточно физической памяти, давая JVM большую кучу, мы сократим общие накладные расходы GC и сделаем ваше приложение быстрее.

Другие трюки, которые вы могли бы попробовать, - установить -Xmx и -Xms на одно и то же значение и отрегулировать параметр настройки, который устанавливает максимальное соотношение времени, затраченного на сбор мусора.