OutOfMemoryException, несмотря на использование WeakHashMap - PullRequest
9 голосов
/ 08 марта 2020

Если не вызывать 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, чтобы распечатать информацию G C; как вы видите, на самом деле JVM пытается выполнить полный прогон G C, но терпит неудачу; Я все еще не знаю причину. Очень странно, что если я раскомментирую строку 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

Ответы [ 2 ]

7 голосов
/ 08 марта 2020

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

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

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

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

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

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

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)

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

5 голосов
/ 08 марта 2020

Другой ответ действительно правильный, я отредактировал свой. Как небольшое дополнение, 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. Когда это произойдет, будет запущен одновременный G C. В рамках этого цикла: будет запущена коллекция young и будет инициирована 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

Вот часть logs:

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

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

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

Как часть этого young G C, он также очищает огромные области , здесь есть дефект .


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

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

...