Почему 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 на одно и то же значение и отрегулировать параметр настройки, который устанавливает максимальное соотношение времени, затраченного на сбор мусора.