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)

Значно краще.


Thx для вашої відповіді, здається, ваш висновок правильний; поки я намагаюся зменшити корисну навантаження з 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 ця відповідь , особливо друга половина, також може бути корисною. Також це питання і відповіді
Holger

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 МБ ), це буде називатися a 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.
Холгер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.