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 ในขั้นตอนใด
Vladislav Kysliy

1
ฉันไม่สามารถทำซ้ำสิ่งนี้ในระบบของฉัน ในโหมดดีบั๊กฉันเห็นว่า GC กำลังทำงานอยู่ คุณสามารถเช็คอินโหมด debug ได้ไหมถ้า Map นั้นกำลังทำการล้างข้อมูลอยู่จริงหรือไม่?
magicmn

jre 1.8.0_212-b10 -Xmx200m คุณสามารถดูรายละเอียดเพิ่มเติมจากบันทึก gc ที่ฉันแนบ; ขอบคุณ
Dominic Peng

คำตอบ:


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 * 10,000 เหลือ 1024 * 1000; รหัสสามารถทำงานได้ดี; แต่ฉันยังไม่เข้าใจคำอธิบายของคุณมากนัก ตามความหมายของคุณถ้าต้องการปล่อยพื้นที่จาก WeakHashMap ควรทำ gc สองครั้งเป็นอย่างน้อย เวลาเริ่มต้นคือการรวบรวมกุญแจจากแผนที่และเพิ่มลงในคิวการอ้างอิง ครั้งที่สองคือการรวบรวมค่า? แต่จากบันทึกเริ่มต้นที่คุณให้ไว้จริงๆแล้ว JVM ใช้เวลาเต็ม gc สองครั้งแล้ว
Dominic Peng

คุณกำลังพูดว่า "ค่าแผนที่สามารถเข้าถึงได้อย่างมากและจะถูกล้างด้วยตัวแผนที่เองเมื่อมีการเรียกใช้การดำเนินการบางอย่าง" พวกเขามาจากไหน
Andronicus

1
มันจะไม่เพียงพอที่จะมีเพียงแค่ GC สองตัวที่วิ่งในกรณีของคุณ ก่อนอื่นคุณต้องรันหนึ่ง GC นั่นถูกต้อง แต่ขั้นตอนต่อไปจะต้องมีการโต้ตอบกับตัวแผนที่เอง สิ่งที่คุณควรมองหาคือวิธีการjava.util.WeakHashMap.expungeStaleEntriesที่อ่านคิวการอ้างอิงและลบรายการออกจากแผนที่จึงทำให้ค่าไม่สามารถเข้าถึงได้และอาจมีการรวบรวม หลังจากนั้นจะมีการส่ง GC ครั้งที่สองเพื่อเพิ่มหน่วยความจำ expungeStaleEntriesมีการเรียกใช้ในหลายกรณีเช่นรับ / วาง / ขนาดหรือทุกอย่างที่คุณทำกับแผนที่ นั่นคือการจับ
งวง

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 MB มัก ) 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 ก็ยังล้างภูมิภาค humongous , นี่เป็นข้อบกพร่อง


ตอนนี้คุณจะเห็นว่าjdk-13ไม่ต้องรอให้ขยะกองในพื้นที่เก่าเมื่อมีการจัดสรรวัตถุขนาดใหญ่จริง ๆ แต่ทริกเกอร์วงจร GC ที่เกิดขึ้นพร้อมกัน ไม่เหมือน jdk-8

คุณอาจต้องการอ่านสิ่งที่DisableExplicitGCและ / หรือExplicitGCInvokesConcurrentหมายถึงควบคู่ไปกับSystem.gcและเข้าใจว่าทำไมการโทรSystem.gcช่วยจริงๆที่นี่


1
Java 8 ไม่ได้ใช้ G1GC เป็นค่าเริ่มต้น และบันทึกของ GC ของ OP ยังแสดงให้เห็นอย่างชัดเจนว่ากำลังใช้ GC แบบขนานสำหรับรุ่นเก่า และสำหรับนักสะสมที่ไม่เกิดขึ้นพร้อมกันมันง่ายอย่างที่อธิบายไว้ในคำตอบนี้
Holger

@ Holger ฉันได้ตรวจสอบคำตอบวันนี้ในตอนเช้าเท่านั้นที่จะตระหนักว่ามันแน่นอนParalleGCฉันได้แก้ไขและขออภัย (และขอบคุณ) สำหรับการพิสูจน์ฉันผิด
ยูจีน

1
“ การจัดสรรอย่างมีมนุษยธรรม” ยังเป็นคำใบ้ที่ถูกต้อง เมื่อใช้ตัวสะสมแบบไม่พร้อมกันมันก็หมายความว่า GC ตัวแรกจะทำงานเมื่อคนรุ่นเก่าเต็มดังนั้นความล้มเหลวในการเรียกคืนพื้นที่ที่เพียงพอจะทำให้เสียชีวิต ในทางตรงกันข้ามเมื่อคุณลดขนาดอาร์เรย์ GC หนุ่มจะถูกเรียกใช้เมื่อยังมีหน่วยความจำในรุ่นเก่าเหลือดังนั้นตัวสะสมสามารถเลื่อนวัตถุและดำเนินการต่อ สำหรับนักสะสมพร้อมกันในทางกลับกันมันเป็นเรื่องปกติที่จะทริกเกอร์ gc ก่อนที่ฮีปจะหมด-XX:+UseG1GCทำให้ทำงานใน Java 8 เหมือนกับ-XX:+UseParallelOldGCทำให้มันล้มเหลวใน JVM ใหม่
Holger
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.