WeakHashMap kullanılmasına rağmen OutOfMemoryException


9

Arama yapmazsanız System.gc(), sistem bir OutOfMemoryException kurar. Neden System.gc()açıkça aramam gerektiğini bilmiyorum ; JVM gc()kendisini aramalı , değil mi? Tavsiye lütfen.

Aşağıdaki benim test kodum:

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();
    }
}

Aşağıdaki gibi, -XX:+PrintGCDetailsGC bilgilerini yazdırmak için ekleyin ; gördüğünüz gibi, aslında, JVM tam bir GC çalışması yapmaya çalışır, ancak başarısız olur; Ben hala nedenini bilmiyorum. System.gc();Çizgiyi kaldırırsam sonucun olumlu olması çok garip :

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 sürümü nedir? -Xms ve -Xmx parametreleri kullanıyor musunuz? hangi adımda OOM aldınız?
Vladislav Kysliy

1
Bunu sistemimde çoğaltamıyorum. Hata ayıklama modunda GC'nin işini yaptığını görebiliyorum. Harita gerçekten temizleniyor mu yoksa hata ayıklama modunda kontrol edebilir misiniz?
magicmn

jre 1.8.0_212-b10 -Xmx200m Eklediğim gc günlüğünden daha fazla ayrıntı görebilirsiniz; thx
Dominic Peng

Yanıtlar:


7

JVM kendi başına GC'yi arayacaktır, ancak bu durumda çok az geç olacaktır. Bu durumda hafızayı temizlemekten sadece GC sorumlu değildir. Harita değerlerine güçlü bir şekilde ulaşılabilir ve belirli işlemler çağrıldığında haritanın kendisi tarafından temizlenir.

GC olaylarını açarsanız çıktı (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

Haritaya değer koymak için yapılan son girişime kadar GC tetiklenmez.

WeakHashMap, bir başvuru kuyruğunda harita anahtarları oluşana kadar eski girdileri temizleyemez. Harita anahtarları, çöp toplanana kadar bir başvuru kuyruğunda oluşmaz. Yeni harita değeri için bellek tahsisi, haritanın kendini temizleme şansı olmadan tetiklenir. Bellek ayırma başarısız olduğunda ve GC'yi tetiklediğinde, harita anahtarları toplanır. Ancak çok geç değil - yeni harita değeri tahsis etmek için yeterli hafıza serbest bırakılmadı. Yükü azaltırsanız, muhtemelen yeni harita değeri tahsis etmek için yeterli belleğe sahip olursunuz ve eski girişler kaldırılır.

Başka bir çözüm, değerleri WeakReference içine sarmak olabilir. Bu, haritanın kendi başına yapmasını beklemeden GC net kaynaklarına izin verecektir. İşte çıktı:

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)

Çok daha iyi.


Cevabınız için teşekkürler, sonucunuz doğru görünüyor; yükü 1024 * 10000'den 1024 * 1000'e düşürmeye çalışırken; kod iyi çalışabilir; ama açıklamanızı hala anlamıyorum; anlamınız olarak, WeakHashMap'ten alan bırakmanız gerekiyorsa, en az iki kez gc yapmalısınız; ilk zaman haritadan anahtarları toplamak ve bunları referans kuyruğuna eklemek; ikinci kez değer toplamaktır? ancak sağladığınız ilk kayıttan JVM zaten iki kez tam gc almıştı;
Dominic Peng

"Harita değerlerine güçlü bir şekilde ulaşılabilir ve belirli işlemler çağrıldığında haritanın kendisi tarafından temizlenir" diyorsunuz. Nereden ulaşılabilir?
Andronicus

1
Durumunuzda sadece iki GC çalışması olması yeterli olmayacaktır. İlk önce bir GC çalışmasına ihtiyacınız var, bu doğru. Ancak bir sonraki adım haritanın kendisi ile biraz etkileşim gerektirecektir. Aramanız gereken, java.util.WeakHashMap.expungeStaleEntriesreferans kuyruğunu okuyan ve girişleri haritadan kaldıran, böylece değerlere ulaşılamaz ve koleksiyona tabi olan bir yöntemdir . Ancak bundan sonra GC'nin ikinci geçişi biraz hafıza boşaltacaktır. expungeStaleEntriesget / put / size veya genellikle bir harita ile yaptığınız hemen hemen her şeye denir. Yakalama bu.
tentacle

1
@Andronicus, bu WeakHashMap'in en kafa karıştırıcı kısmı. Birden çok kez ele alındı. stackoverflow.com/questions/5511279/…
dokunaç

2
@Andronicus bu cevap , özellikle ikinci yarı da yardımcı olabilir. Ayrıca bu soru-cevap
Holger

5

Diğer cevap gerçekten doğru, benimkini düzenledim. Küçük bir zeyilname olarak G1GC, aksine, bu davranış sergilemez ParallelGC; altında varsayılan olan java-8.

Eğer ben biraz (altında pistine programınızı değiştirirseniz ne olacak sizce jdk-8ile -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
    }
}

Sadece iyi çalışır. Neden? Çünkü programınıza yeni tahsislerin gerçekleşmesi için yeterince nefes alan sağlar WeakHashMap, girişlerini silmeden önce . Ve diğer cevap zaten bunun nasıl olduğunu açıklıyor.

Şimdi, G1GCişler biraz farklı olurdu. Böyle büyük bir nesne tahsis edildiğinde ( genellikle 1/2 MB'den fazla ), buna a denir humongous allocation. Bu olduğunda, eşzamanlı bir GC tetiklenir. Bu döngünün bir parçası olarak: genç bir koleksiyon tetiklenecek ve Cleanup phaseetkinliğin adresine gönderilmesine özen gösterecek ve ReferenceQueueböylece WeakHashMapgirişlerini temizleyecek bir koleksiyon başlatılacak .

Yani bu kod için:

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 ile çalıştığım ( G1GCvarsayılan nerede )

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

Günlüklerin bir kısmı:

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

Bu zaten farklı bir şey yapıyor. Bu bir başlatır concurrent cycle(tamamlandı iken bir olduğu için, uygulamanın çalıştığı) G1 Humongous Allocation. Bu eş zamanlı döngünün bir parçası olarak genç bir GC döngüsü yapar ( çalışırken uygulamanızı durdurur )

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

Bu genç GC'nin bir parçası olarak , aynı zamanda humongo bölgeleri de temizler , işte kusur .


Artık jdk-13gerçekten büyük nesneler tahsis edildiğinde eski bölgede çöpün birikmesini beklemediğini, ancak günü kurtaran eşzamanlı bir GC döngüsünü tetiklediğini görebilirsiniz ; JDK-8'in aksine.

Neyin DisableExplicitGCve / veya ne ExplicitGCInvokesConcurrentanlama geldiğini okumak ve bununla System.gcçağrılmanın neden System.gcburada yardımcı olduğunu anlayabilirsiniz .


1
Java 8 varsayılan olarak G1GC kullanmaz. OP'nin GC günlükleri de eski nesil için paralel GC kullandığını açıkça gösteriyor. Ve böyle bir eşzamanlı olmayan toplayıcı için, bu cevapta
Holger

@Holger Bu cevabı bugün sabah gerçekten gözden geçirdiğimi anlamak için gözden geçiriyordum ParalleGC, yanlış olduğunu kanıtladığım için düzenledim ve üzgünüm (ve teşekkür ederim).
Eugene

1
“Humongo tahsis” hala doğru bir ipucudur. Eşzamanlı olmayan bir toplayıcı ile, ilk GC'nin eski nesil dolduğunda çalışacağı anlamına gelir, bu nedenle yeterli alanın geri kazanılmaması onu ölümcül hale getirecektir. Buna karşılık, dizi boyutunu azalttığınızda, eski nesilde hala bellek kaldığında genç bir GC tetiklenir, böylece toplayıcı nesneleri tanıtabilir ve devam edebilir. Eşzamanlı bir toplayıcı için, öbek tükenmeden önce gc'yi tetiklemek normaldir, bu yüzden yeni JVM'lerde başarısız olması -XX:+UseG1GCgibi Java 8'de çalışmasını sağlayın -XX:+UseParallelOldGC.
Holger
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.