OutOfMemoryException, несмотря на использование WeakHashMap

9

Если не вызывать System.gc(), система выдаст исключение OutOfMemoryException. Я не знаю, почему мне нужно звонить System.gc()явно; JVM должна называть gc()себя, верно? Пожалуйста, порекомендуйте.

Вот мой тестовый код:

public static void main(String[] args) throws InterruptedException {
    WeakHashMap<String, int[]> hm = new WeakHashMap<>();
    int i  = 0;
    while(true) {
        Thread.sleep(1000);
        i++;
        String key = new String(new Integer(i).toString());
        System.out.println(String.format("add new element %d", i));
        hm.put(key, new int[1024 * 10000]);
        key = null;
        //System.gc();
    }
}

Как следует, добавьте, -XX:+PrintGCDetailsчтобы распечатать информацию GC; как вы видите, на самом деле JVM пытается выполнить полный запуск GC, но терпит неудачу; Я до сих пор не знаю причину. Очень странно, что если я раскомментирую System.gc();строку, результат будет положительным:

add new element 1
add new element 2
add new element 3
add new element 4
add new element 5
[GC (Allocation Failure) --[PSYoungGen: 48344K->48344K(59904K)] 168344K->168352K(196608K), 0.0090913 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 48344K->41377K(59904K)] [ParOldGen: 120008K->120002K(136704K)] 168352K->161380K(196608K), [Metaspace: 5382K->5382K(1056768K)], 0.0380767 secs] [Times: user=0.09 sys=0.03, real=0.04 secs] 
[GC (Allocation Failure) --[PSYoungGen: 41377K->41377K(59904K)] 161380K->161380K(196608K), 0.0040596 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 41377K->41314K(59904K)] [ParOldGen: 120002K->120002K(136704K)] 161380K->161317K(196608K), [Metaspace: 5382K->5378K(1056768K)], 0.0118884 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at test.DeadLock.main(DeadLock.java:23)
Heap
 PSYoungGen      total 59904K, used 42866K [0x00000000fbd80000, 0x0000000100000000, 0x0000000100000000)
  eden space 51712K, 82% used [0x00000000fbd80000,0x00000000fe75c870,0x00000000ff000000)
  from space 8192K, 0% used [0x00000000ff800000,0x00000000ff800000,0x0000000100000000)
  to   space 8192K, 0% used [0x00000000ff000000,0x00000000ff000000,0x00000000ff800000)
 ParOldGen       total 136704K, used 120002K [0x00000000f3800000, 0x00000000fbd80000, 0x00000000fbd80000)
  object space 136704K, 87% used [0x00000000f3800000,0x00000000fad30b90,0x00000000fbd80000)
 Metaspace       used 5409K, capacity 5590K, committed 5760K, reserved 1056768K
  class space    used 576K, capacity 626K, committed 640K, reserved 1048576K
Доминик Пэн
источник
какая версия jdk? вы используете параметры -Xms и -Xmx? на каком этапе вы получили OOM?
Владислав Кислый
1
Я не могу воспроизвести это в моей системе. В режиме отладки я вижу, что GC выполняет свою работу. Можете ли вы проверить в режиме отладки, действительно ли карта очищена или нет?
magicmn
jre 1.8.0_212-b10 -Xmx200m Вы можете увидеть более подробную информацию из журнала gc, который я прикрепил; THX
Доминик Пэн

Ответы:

7

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

Вот вывод, если вы включите события GC (XX: + PrintGC):

add new element 1
add new element 2
add new element 3
add new element 4
add new element 5
add new element 6
add new element 7
[GC (Allocation Failure)  2407753K->2400920K(2801664K), 0.0123285 secs]
[GC (Allocation Failure)  2400920K->2400856K(2801664K), 0.0090720 secs]
[Full GC (Allocation Failure)  2400856K->2400805K(2590720K), 0.0302800 secs]
[GC (Allocation Failure)  2400805K->2400805K(2801664K), 0.0069942 secs]
[Full GC (Allocation Failure)  2400805K->2400753K(2620928K), 0.0146932 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

GC не срабатывает до последней попытки поместить значение в карту.

WeakHashMap не может очистить устаревшие записи, пока в карте ссылок не появятся ключи карты. И ключи карты не появляются в ссылочной очереди до тех пор, пока они не будут удалены. Выделение памяти для нового значения карты происходит до того, как карта сможет очистить себя. Когда распределение памяти не удается и запускает GC, ключи карты собираются. Но слишком поздно, слишком поздно - недостаточно памяти было освобождено для выделения нового значения карты. Если вы уменьшите полезную нагрузку, у вас, вероятно, будет достаточно памяти для выделения нового значения карты, и устаревшие записи будут удалены.

Другое решение может заключаться в переносе самих значений в WeakReference. Это позволит GC очищать ресурсы, не дожидаясь, пока карта сделает это самостоятельно. Вот вывод:

add new element 1
add new element 2
add new element 3
add new element 4
add new element 5
add new element 6
add new element 7
[GC (Allocation Failure)  2407753K->2400920K(2801664K), 0.0133492 secs]
[GC (Allocation Failure)  2400920K->2400888K(2801664K), 0.0090964 secs]
[Full GC (Allocation Failure)  2400888K->806K(190976K), 0.1053405 secs]
add new element 8
add new element 9
add new element 10
add new element 11
add new element 12
add new element 13
[GC (Allocation Failure)  2402096K->2400902K(2801664K), 0.0108237 secs]
[GC (Allocation Failure)  2400902K->2400838K(2865664K), 0.0058837 secs]
[Full GC (Allocation Failure)  2400838K->1024K(255488K), 0.0863236 secs]
add new element 14
add new element 15
...
(and counting)

Намного лучше.

щупальце
источник
Спасибо за ваш ответ, кажется, ваш вывод правильный; пока я пытаюсь уменьшить полезную нагрузку с 1024 * 10000 до 1024 * 1000; код может работать нормально; но я все еще не очень понимаю ваше объяснение; как ваше значение, если вам нужно освободить место из WeakHashMap, следует сделать gc как минимум два раза; первое время - собрать ключи с карты и добавить их в очередь ссылок; второй раз собирать ценности? но из первого журнала, который вы предоставили, JVM уже дважды брала полный gc;
Доминик Пэн
Вы говорите, что «Значения карты являются сильно достижимыми и очищаются самой картой при вызове определенных операций над ней». Откуда они доступны?
Андроник
1
Недостаточно просто провести два прогона GC в вашем случае. Сначала вам нужен один прогон GC, это правильно. Но следующий шаг потребует некоторого взаимодействия с самой картой. То, что вы должны искать, это метод, java.util.WeakHashMap.expungeStaleEntriesкоторый читает справочную очередь и удаляет записи с карты, таким образом делая значения недоступными и подлежащими сбору. Только после этого второй проход GC освободит память. expungeStaleEntriesвызывается в ряде случаев, таких как get / put / size или почти во всех случаях, которые вы обычно делаете с картой. Это подвох.
щупальце
1
@ Andronicus, это самая запутанная часть WeakHashMap. Это было покрыто несколько раз. stackoverflow.com/questions/5511279/…
щупальце
2
@ Andronicus этот ответ , особенно вторая половина, может быть полезным. Также это Q & A ...
Хольгер
5

Другой ответ действительно правильный, я отредактировал мой. Как небольшое дополнение, G1GCне будет демонстрировать это поведение, в отличие от ParallelGC; который по умолчанию под java-8.

Как вы думаете , что произойдет , если я немного изменить свою программу (бежать под jdk-8с -Xmx20m)

public static void main(String[] args) throws InterruptedException {
    WeakHashMap<String, int[]> hm = new WeakHashMap<>();
    int i = 0;
    while (true) {
        Thread.sleep(200);
        i++;
        String key = "" + i;
        System.out.println(String.format("add new element %d", i));
        hm.put(key, new int[512 * 1024 * 1]); // <--- allocate 1/2 MB
    }
}

Это будет работать просто отлично. Это почему? Потому что это дает вашей программе достаточно места для новых распределений, прежде чем WeakHashMapочищать свои записи. А другой ответ уже объясняет, как это происходит.

Теперь G1GCвсе будет немного иначе. Когда выделяется такой большой объект ( обычно более 1/2 МБ ), это называется humongous allocation. Когда это произойдет, одновременный GC будет запущен. Как часть этого цикла: будет запущена молодая коллекция, и Cleanup phaseбудет инициирована операция, которая позаботится о публикации события в ReferenceQueue, чтобы WeakHashMapочистить его записи.

Итак, для этого кода:

public static void main(String[] args) throws InterruptedException {
    Map<String, int[]> hm = new WeakHashMap<>();
    int i = 0;
    while (true) {
        Thread.sleep(1000);
        i++;
        String key = "" + i;
        System.out.println(String.format("add new element %d", i));
        hm.put(key, new int[1024 * 1024 * 1]); // <--- 1 MB allocation
    }
}

что я запускаю с JDK-13 (где G1GCпо умолчанию)

java -Xmx20m "-Xlog:gc*=debug" gc.WeakHashMapTest

Вот часть логов:

[2.082s][debug][gc,ergo] Request concurrent cycle initiation (requested by GC cause). GC cause: G1 Humongous Allocation

Это уже делает что-то другое. Он запускает concurrent cycle(сделано во время работы вашего приложения), потому что был G1 Humongous Allocation. Как часть этого параллельного цикла, он выполняет молодой цикл GC (который останавливает ваше приложение во время работы)

 [2.082s][info ][gc,start] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation)

Как часть этого молодого GC, он также очищает огромные области , вот недостаток .


Теперь вы можете видеть, что jdk-13не ожидает накопления мусора в старом регионе, когда выделяются действительно большие объекты, но запускает параллельный цикл GC, который спас день; в отличие от JDK-8.

Возможно, вы захотите прочитать, что DisableExplicitGCи / или ExplicitGCInvokesConcurrentимеется в виду, в сочетании с System.gcпониманием того, почему на System.gcсамом деле помогает звонок .

Евгений
источник
1
Java 8 не использует G1GC по умолчанию. И журналы GC OP также ясно показывают, что он использует параллельный GC для старого поколения. И для такого не одновременного сборщика это так же просто, как описано в этом ответе
Хольгер
@ Holger Я сегодня утром проверял этот ответ, только чтобы понять, что это действительно так ParalleGC, я отредактировал и извиняюсь (и спасибо), что доказал, что я не прав.
Евгений
1
«Огромное распределение» по-прежнему является правильным намеком. В случае не одновременного сборщика это означает, что первый GC будет работать, когда старое поколение заполнено, поэтому неспособность освободить достаточно места сделает его фатальным. Напротив, когда вы уменьшаете размер массива, молодой GC будет запускаться, когда в старом поколении еще останется память, поэтому сборщик сможет продвигать объекты и продолжать. Для параллельного сборщика, с другой стороны, нормально запускать gc до того, как куча будет исчерпана, поэтому -XX:+UseG1GCсделайте так, чтобы он работал в Java 8, точно так же, как -XX:+UseParallelOldGCи в новых JVM.
Хольгер