Почему 64-битная JVM выбрасывает из памяти до достижения xmx? - PullRequest
11 голосов
/ 25 октября 2011

Я борюсь с большими требованиями к памяти для 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 больше, чем примерно 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  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 Ответ

13 голосов
/ 25 октября 2011

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

По сути, есть две стратегии, которые 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 раньше.

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

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

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

...