เกิดอะไรขึ้นเมื่อมีหน่วยความจำไม่เพียงพอที่จะโยน OutOfMemoryError


207

ฉันทราบว่าวัตถุทุกชิ้นต้องการหน่วยความจำฮีปและการอ้างอิงดั้งเดิมบนสแต็กทั้งหมดต้องใช้หน่วยความจำสแต็ก

เมื่อฉันพยายามที่จะสร้างวัตถุบนกองและมีหน่วยความจำไม่เพียงพอที่จะทำเช่นนั้น JVM สร้างjava.lang.OutOfMemoryErrorบนกองและโยนมันมาให้ฉัน

โดยปริยายนั่นหมายความว่ามีหน่วยความจำบางส่วนที่สงวนไว้โดย JVM เมื่อเริ่มต้น

จะเกิดอะไรขึ้นเมื่อหน่วยความจำที่สงวนไว้นี้จะใช้ขึ้น (มันแน่นอนจะนำมาใช้ขึ้นอ่านการอภิปรายด้านล่าง) และ JVM ไม่ได้มีหน่วยความจำเพียงพอในกองเพื่อสร้างตัวอย่างของjava.lang.OutOfMemoryError ?

มันแขวนไหม หรือเขาจะขว้างฉันnullตั้งแต่ไม่มีความทรงจำเกี่ยวกับnewตัวอย่างของ OOM?

try {
    Object o = new Object();
    // and operations which require memory (well.. that's like everything)
} catch (java.lang.OutOfMemoryError e) {
    // JVM had insufficient memory to create an instance of java.lang.OutOfMemoryError to throw to us
    // what next? hangs here, stuck forever?
    // or would the machine decide to throw us a "null" ? (since it doesn't have memory to throw us anything more useful than a null)
    e.printStackTrace(); // e.printStackTrace() requires memory too.. =X
}

==

ทำไม JVM ไม่สำรองหน่วยความจำเพียงพอ

ไม่ว่าจะสงวนหน่วยความจำไว้เท่าใดก็ยังสามารถใช้หน่วยความจำนั้นได้หาก JVM ไม่มีวิธี "เรียกคืน" หน่วยความจำนั้น:

try {
    Object o = new Object();
} catch (java.lang.OutOfMemoryError e) {
    // JVM had 100 units of "spare memory". 1 is used to create this OOM.
    try {
        e.printStackTrace();
    } catch (java.lang.OutOfMemoryError e2) {
        // JVM had 99 units of "spare memory". 1 is used to create this OOM.
        try {
            e.printStackTrace();
        } catch (java.lang.OutOfMemoryError e3) {
            // JVM had 98 units of "spare memory". 1 is used to create this OOM.
            try {
                e.printStackTrace();
            } catch (java.lang.OutOfMemoryError e4) {
                // JVM had 97 units of "spare memory". 1 is used to create this OOM.
                try {
                    e.printStackTrace();
                } catch (java.lang.OutOfMemoryError e5) {
                    // JVM had 96 units of "spare memory". 1 is used to create this OOM.
                    try {
                        e.printStackTrace();
                    } catch (java.lang.OutOfMemoryError e6) {
                        // JVM had 95 units of "spare memory". 1 is used to create this OOM.
                        e.printStackTrace();
                        //........the JVM can't have infinite reserved memory, he's going to run out in the end
                    }
                }
            }
        }
    }
}

หรือรัดกุมมากขึ้น:

private void OnOOM(java.lang.OutOfMemoryError e) {
    try {
        e.printStackTrace();
    } catch (java.lang.OutOfMemoryError e2) {
        OnOOM(e2);
    }
}

2
คำตอบของคุณจะ JVM ขึ้นอยู่ในระดับที่ดี
MozenRath

23
ห้องสมุดโทรศัพท์หนึ่งที่ฉันเคยใช้ (ใน 90's) เคยจับOutOfMemoryExceptionแล้วทำอะไรบางอย่างที่เกี่ยวข้องกับการสร้างบัฟเฟอร์ขนาดใหญ่ ...
Tom Hawtin - tackline

@ TomHawtin-tackline จะเกิดอะไรขึ้นถ้าการดำเนินการที่เกี่ยวข้องกับการทำเช่นนั้นเป็นการโยน OOM อื่น
ม้า

38
เช่นโทรศัพท์มือถือแบตเตอรี่หมด แต่มีแบตเตอรี่เพียงพอที่จะส่งสแปม "คุณหมดแบตเตอรี"
Kazuma

1
"จะเกิดอะไรขึ้นเมื่อหน่วยความจำที่สงวนไว้ใช้หมด": อาจเกิดขึ้นได้ก็ต่อเมื่อโปรแกรมถูกตรวจจับครั้งแรกOutOfMemoryErrorและเก็บข้อมูลอ้างอิงไว้ มันปรากฏว่าการจับ a OutOfMemoryErrorไม่มีประโยชน์อย่างที่ใคร ๆ คิดเพราะคุณไม่สามารถรับประกันได้เลยว่าสถานะของโปรแกรมของคุณจะเป็นอย่างไร ดูstackoverflow.com/questions/8728866/…
Raedwald

คำตอบ:


145

JVM ไม่เคยมีหน่วยความจำไม่เพียงพอ มันทำการคำนวณหน่วยความจำของฮีปสแต็กล่วงหน้า

โครงสร้างของ JVM, บทที่ 3มาตรา 3.5.2 ฯ :

  • หากสแต็กเครื่องเสมือน Java สามารถขยายได้แบบไดนามิกและพยายามขยาย แต่มีหน่วยความจำไม่เพียงพอที่จะทำให้พร้อมใช้งานสำหรับการขยายตัวหรือหากหน่วยความจำไม่เพียงพอสามารถทำให้พร้อมใช้งานเพื่อสร้างสแต็กเครื่องเสมือน Java เริ่มต้นสำหรับเธรดใหม่ เครื่องพ่นOutOfMemoryErrorและ

สำหรับกองส่วน 3.5.3

  • หากการคำนวณต้องการฮีปมากกว่าที่สามารถทำให้พร้อมใช้งานโดยระบบการจัดการที่เก็บข้อมูลอัตโนมัติเครื่องเสมือน Java จะส่งผ่าน OutOfMemoryErrorผ่าน

ดังนั้นจึงเป็นการคำนวณล่วงหน้าก่อนทำการจัดสรรวัตถุ


สิ่งที่เกิดขึ้นคือ JVM พยายามจัดสรรหน่วยความจำสำหรับวัตถุในหน่วยความจำที่เรียกว่า Permanent Generation region (หรือ PermSpace) หากการจัดสรรล้มเหลว (แม้ว่าหลังจาก JVM จะเรียกใช้ Garbage Collector ให้ลอง & จัดสรรพื้นที่ว่าง) มันจะส่งข้อOutOfMemoryErrorผิดพลาด แม้แต่ข้อยกเว้นต้องใช้พื้นที่หน่วยความจำดังนั้นข้อผิดพลาดจะถูกโยนทิ้งไปเรื่อย ๆ

อ่านเพิ่มเติม. ? นอกจากนี้OutOfMemoryErrorสามารถเกิดขึ้นได้ในโครงสร้าง JVM ที่แตกต่างกัน


10
ฉันหมายถึงใช่ แต่ Java virtual machine จะไม่ต้องการหน่วยความจำเพื่อโยน OutOfMemoryError เกิดอะไรขึ้นเมื่อไม่มีความทรงจำในการขว้าง OOM?
Pacerier

5
แต่ถ้า JVM ไม่ส่งคืนการอ้างอิงไปยังอินสแตนซ์เดียวกันของ OOM คุณไม่เห็นด้วยหรือไม่ว่าในที่สุดมันจะหมดลงในหน่วยความจำที่สงวนไว้ (ดังที่แสดงในรหัสในคำถาม)
Pacerier

1
อนุญาตให้ฉันใส่การอ้างอิงถึงความคิดเห็นของ Graham ที่นี่: stackoverflow.com/questions/9261705/…
Pacerier

2
อาจดีถ้า VM ยังคงเก็บ OOM Exton ไว้หนึ่งตันสำหรับกรณีที่ระบุไว้อย่างรุนแรงและที่โรงไฟฟ้านิวเคลียร์
John K

8
@ JohnK: ฉันหวังว่าโรงไฟฟ้านิวเคลียร์จะไม่ได้ตั้งโปรแกรมใน Java เช่นเดียวกับกระสวยอวกาศและโบอิ้ง 757 ไม่ได้ตั้งโปรแกรมใน Java
Dietrich Epp

64

ดูเหมือนว่าเกรแฮมบอร์แลนด์จะถูกต้อง : อย่างน้อยJVM ของฉันก็เห็นได้ชัดว่าใช้ OutOfMemoryErrors อีกครั้ง เพื่อทดสอบสิ่งนี้ฉันเขียนโปรแกรมทดสอบอย่างง่าย:

class OOMTest {
    private static void test (OutOfMemoryError o) {
        try {
            for (int n = 1; true; n += n) {
                int[] foo = new int[n];
            }
        } catch (OutOfMemoryError e) {
            if (e == o)
                System.out.println("Got the same OutOfMemoryError twice: " + e);
            else test(e);
        }
    }
    public static void main (String[] args) {
        test(null);
    }
}

การรันมันจะสร้างเอาต์พุตนี้:

$ javac OOMTest.java && java -Xmx10m OOMTest 
Got the same OutOfMemoryError twice: java.lang.OutOfMemoryError: Java heap space

BTW, JVM ที่ฉันใช้อยู่ (บน Ubuntu 10.04) คือ:

$ java -version
java version "1.6.0_26"
Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode)

แก้ไข:ฉันพยายามดูว่าจะเกิดอะไรขึ้นหากฉันบังคับให้ JVM ใช้หน่วยความจำหมดโดยใช้โปรแกรมต่อไปนี้:

class OOMTest2 {
    private static void test (int n) {
        int[] foo;
        try {
            foo = new int[n];
            test(n * 2);
        }
        catch (OutOfMemoryError e) {
            test((n+1) / 2);
        }
    }
    public static void main (String[] args) {
        test(1);
    }
}

ดูเหมือนว่าจะวนซ้ำไปตลอดกาล อย่างไรก็ตามอยากรู้อยากเห็นพยายามยกเลิกโปรแกรมด้วยCtrl+ Cไม่ทำงาน แต่ให้เฉพาะข้อความต่อไปนี้:

Java HotSpot(TM) 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated


การทดสอบที่ดีเหมือนกันสำหรับฉันกับรุ่น "1.7.0_01" เซิร์ฟเวอร์ Java HotSpot (TM) 64- บิต VM
Pacerier

น่าสนใจ นั่นทำให้ดูเหมือนว่า JVM ไม่เข้าใจการเรียกซ้ำหางอย่างสมบูรณ์ ... (ราวกับว่ามันกำลังนำมาใช้ซ้ำแบบกองซ้อนแบบเรียกซ้ำ แต่ไม่มากพอที่จะทำความสะอาดหน่วยความจำทั้งหมด ... )
Izkata

ปรับเปลี่ยนรหัสของคุณเพื่อดูจำนวนที่แตกต่างกันที่ฉันได้รับ - ฉันได้รับการดำเนินการสาขาอื่นเสมอ 5 ครั้ง
Irfy

@ อิซกะตะ: ฉันอยากจะบอกว่ามันเป็นการตัดสินใจที่มีสติในการจัดสรรnOOM ล่วงหน้าและนำหนึ่งในนั้นมาใช้ใหม่ในภายหลังเพื่อที่ OOM จะถูกโยนทิ้งไป Sun / JVM ของ Oracle ไม่รองรับการเรียกซ้ำแบบหางที่ IIRC ทั้งหมดหรือไม่
Irfy

10
@Izkata ดูเหมือนว่าลูปจะทำงานอย่างไม่มีที่สิ้นสุดเนื่องจาก JVM กำลังขว้างหนึ่งและ OOM (ที่ 5 หรือมากกว่านั้น) อย่างต่อเนื่องหลังจากหน่วยความจำหมด ดังนั้นมันจึงมีnเฟรมอยู่บนสแต็กและท้ายที่สุดก็คือการสร้างและทำลายเฟรมn+1สำหรับนิรันดร์
Irfy

41

สภาพแวดล้อมรันไทม์ส่วนใหญ่จะจัดสรรล่วงหน้าเมื่อเริ่มต้นหรือสำรองหน่วยความจำเพียงพอที่จะจัดการกับสถานการณ์ความอดอยากของหน่วยความจำ ฉันจินตนาการว่าการใช้งาน JVM อย่างมีสติจะทำเช่นนี้


1
stackoverflow.com/questions/9261215/… : จริง แต่ถ้า JVM สงวนหน่วยความจำไว้ 100 หน่วยให้ทำและใช้ 1 หน่วยใน OOM แรกจะเกิดอะไรขึ้นถ้าใน OOM catch block ฉันจะทำ e.printStackTrace ()? e.printStackTrace () ต้องการหน่วยความจำด้วย จากนั้น JVM จะใช้หน่วยความจำอีกหน่วยหนึ่งเพื่อขว้างฉันอีก OOM (เหลือ 98 หน่วย) และฉันก็จับมันด้วย e.printStackTrace () ดังนั้น JVM ก็เหวี่ยง OOM อีก (เหลือ 97 หน่วย) และฉันก็อยากได้ ..
Pacerier

3
นี่คือสาเหตุที่ OOME ไม่เคยใช้เพื่อรวมการติดตามสแต็ก - การติดตามสแต็กใช้หน่วยความจำ! OOME เริ่มพยายามรวมการติดตามสแต็กใน java 6 ( blogs.oracle.com/alanb/entry/… ) ฉันสมมติว่าหากการติดตามสแต็กเป็นไปไม่ได้ข้อยกเว้นจะถูกส่งออกไปโดยไม่มีการติดตามสแต็ก
Sean Reilly

1
@ SeanReilly ฉันหมายถึงข้อยกเว้นที่ไม่มีการติดตามสแต็กยังคงเป็นวัตถุซึ่งยังคงต้องใช้หน่วยความจำ จำเป็นต้องใช้หน่วยความจำไม่ว่าจะมีการติดตามสแต็กหรือไม่ เป็นความจริงหรือไม่ที่ใน catch block ถ้าไม่มีหน่วยความจำเหลือสำหรับสร้าง OOM (ไม่มีหน่วยความจำที่จะสร้างแม้แต่อันเดียวโดยไม่มีการติดตามสแต็ก) จากนั้นฉันจะจับ null ได้หรือไม่
Pacerier

17
JVM สามารถส่งคืนการอ้างอิงหลายรายการไปยังอินสแตนซ์แบบคงที่เดียวของข้อยกเว้น OOM ดังนั้นแม้ว่าcatchประโยคของคุณจะพยายามใช้หน่วยความจำมากขึ้น JVM ก็สามารถขว้างอินสแตนซ์ของ OOM เดิมซ้ำ ๆ ได้
Graham Borland

1
@TheEliteGentleman ฉันเห็นด้วยว่านั่นเป็นคำตอบที่ดีมากเช่นกัน แต่ JVM อาศัยอยู่บนเครื่องทางกายภาพคำตอบเหล่านั้นไม่ได้อธิบายว่า JVM สามารถมีหน่วยความจำเพียงพอที่จะให้อินสแตนซ์ของ OOM ได้อย่างไร "เป็นเช่นเดียวกันเสมอ"ดูเหมือนว่าจะไขปริศนา
Pacerier

23

ครั้งล่าสุดที่ฉันทำงานใน Java และใช้ดีบักเกอร์ตัวตรวจสอบฮีปแสดงให้เห็นว่า JVM จัดสรรอินสแตนซ์ของ OutOfMemoryError เมื่อเริ่มต้น กล่าวอีกนัยหนึ่งมันจัดสรรวัตถุก่อนที่โปรแกรมของคุณจะมีโอกาสเริ่มต้นบริโภคหน่วยความจำหมด


12

จากข้อกำหนดของ JVM บทที่ 3.5.2:

หาก Java กองเครื่องเสมือนสามารถขยายแบบไดนามิกและการขยายตัวจะพยายาม แต่ความทรงจำไม่เพียงพอที่สามารถให้บริการที่จะทำให้เกิดการขยายตัวหรือถ้าหน่วยความจำไม่เพียงพอที่สามารถทำใช้ได้ในการสร้างครั้งแรก Java สแต็คของเครื่องเสมือนสำหรับหัวข้อใหม่ชวาเสมือน เครื่องพ่นOutOfMemoryErrorและ

ทุกโปรแกรม Java Virtual Machine OutOfMemoryErrorที่มีการรับประกันว่ามันจะโยน นั่นหมายความว่ามันจะต้องมีความสามารถในการสร้างตัวอย่างของOutOfMemoryError (หรือมีการสร้างล่วงหน้า) แม้ว่าจะไม่มีพื้นที่เหลือกอง

แม้ว่าจะไม่ต้องรับประกันว่ามีหน่วยความจำเหลือพอที่จะจับมันและพิมพ์ stacktrace ที่ดี ...

ส่วนที่เพิ่มเข้าไป

คุณเพิ่มโค้ดเพื่อแสดงว่า JVM อาจมีพื้นที่ว่างเหลืออยู่หากต้องมีมากกว่าหนึ่งรายการ OutOfMemoryErrorรายการ แต่การดำเนินการดังกล่าวจะละเมิดข้อกำหนดจากด้านบน

ไม่มีข้อกำหนดว่าอินสแตนซ์ที่ส่งออกมาOutOfMemoryErrorนั้นไม่ซ้ำกันหรือสร้างขึ้นตามความต้องการ JVM สามารถเตรียมอินสแตนซ์หนึ่งตัวได้อย่างแน่นอนOutOfMemoryErrorในระหว่างการเริ่มต้นและโยนสิ่งนี้เมื่อใดก็ตามที่พื้นที่หน่วยความจำไม่เพียงพอซึ่งเป็นหนึ่งครั้งในสภาพแวดล้อมปกติ กล่าวอีกนัยหนึ่ง: ตัวอย่างOutOfMemoryErrorที่เราเห็นอาจเป็นซิงเกิลตัน


ฉันเดาว่าจะใช้สิ่งนี้มันจะต้องละเว้นจากการบันทึกสแต็ก - ร่องรอยถ้ามีพื้นที่แคบ
Raedwald

@ Raedwald: อันที่จริงนี่เป็นสิ่งที่ Oracle VM ทำให้ดูคำตอบของฉัน
sleske

11

คำถามที่น่าสนใจ :-) ในขณะที่คนอื่นให้คำอธิบายที่ดีเกี่ยวกับแง่มุมทางทฤษฎีฉันก็ตัดสินใจลอง นี่คือบน Oracle JDK 1.6.0_26, Windows 7 64 บิต

ทดสอบการตั้งค่า

ฉันเขียนโปรแกรมง่าย ๆ เพื่อทำให้หน่วยความจำหมด (ดูด้านล่าง)

โปรแกรมเพิ่งสร้างสแตติกjava.util.Listและเก็บสตริงใหม่เข้าไปเรื่อย ๆ จนกว่า OOM จะถูกโยนทิ้ง จากนั้นจะจับมันและบรรจุต่อไปเรื่อย ๆ ในวงวนไม่รู้จบ (JVM ไม่ดี ... )

ผลการทดสอบ

อย่างที่เราเห็นได้จากผลลัพธ์สี่ครั้งแรก OOME ถูกโยนมันมาพร้อมกับการติดตามสแต็ก หลังจากนั้น OOME ต่อมาจะพิมพ์เฉพาะjava.lang.OutOfMemoryError: Java heap spaceเมื่อprintStackTrace()มีการเรียกใช้

เห็นได้ชัดว่า JVM พยายามพิมพ์กองติดตามถ้าทำได้ แต่ถ้าหน่วยความจำแน่นจริง ๆ เพียงแค่ข้ามร่องรอยเช่นเดียวกับคำตอบอื่น ๆ ที่แนะนำ

ที่น่าสนใจคือรหัสแฮชของ OOME โปรดทราบว่า OOME สองสามตัวแรกทั้งหมดมีแฮชต่างกัน เมื่อ JVM เริ่มละเว้นการติดตามสแต็กแฮชจะเหมือนเดิมเสมอ สิ่งนี้ชี้ให้เห็นว่า JVM จะใช้อินสแตนซ์ OOME ใหม่ (preallocated?) ให้นานที่สุดเท่าที่จะเป็นไปได้ แต่ถ้าการผลักมาถึงการผลักดันมันจะใช้อินสแตนซ์เดียวกันซ้ำมากกว่าไม่มีอะไรจะโยน

เอาท์พุต

หมายเหตุ: ฉันตัดทอนร่องรอยสแต็กบางส่วนเพื่อให้เอาต์พุตอ่านง่ายขึ้น ("[... ]")

iteration 0
iteration 100000
iteration 200000
iteration 300000
iteration 400000
iteration 500000
iteration 600000
iteration 700000
iteration 800000
iteration 900000
iteration 1000000
iteration 1100000
iteration 1200000
iteration 1300000
iteration 1400000
iteration 1500000
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1069480624
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
    at java.util.Arrays.copyOf(Unknown Source)
    at java.util.ArrayList.ensureCapacity(Unknown Source)
    at java.util.ArrayList.add(Unknown Source)
    at testsl.Div.gobbleUpMemory(Div.java:23)
    at testsl.Div.exhaustMemory(Div.java:12)
    at testsl.Div.main(Div.java:7)
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
[...]
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 616699029
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
[...]
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 2136955031
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
[...]
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1535562945
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
Keep on trying...
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
Keep on trying...
[...]

โปรแกรม

public class Div{
    static java.util.List<String> list = new java.util.ArrayList<String>();

    public static void main(String[] args) {
        exhaustMemory();
    }

    private static void exhaustMemory() {
        try {
            gobbleUpMemory();
        } catch (OutOfMemoryError e) {
            System.out.println("Ouch: " + e+"; hash: "+e.hashCode());
            e.printStackTrace();
            System.out.println("Keep on trying...");
            exhaustMemory();
        }
    }

    private static void gobbleUpMemory() {
        for (int i = 0; i < 10000000; i++) {
            list.add(new String("some random long string; use constructor to force new instance"));
            if (i % 10000000== 0) {
                System.out.println("iteration "+i);
            }
        }

    }
}

เมื่อกดเข้ามาจะไม่สามารถจัดสรรหน่วยความจำสำหรับ OOME ได้ดังนั้นจึงล้างหน่วยความจำที่สร้างไว้แล้ว
Buhake Sindi

1
หมายเหตุเล็กน้อย: ผลลัพธ์บางส่วนของคุณดูเหมือนจะไม่เป็นไปตามลำดับสันนิษฐานว่าเป็นเพราะคุณกำลังพิมพ์System.outแต่printStackTrace()ใช้เป็นSystem.errค่าเริ่มต้น คุณอาจได้รับผลลัพธ์ที่ดีขึ้นโดยใช้สตรีมทั้งสองอย่างสม่ำเสมอ
Ilmari Karonen

@IlmariKaronen: ใช่ฉันสังเกตเห็นว่า มันเป็นเพียงตัวอย่างดังนั้นมันไม่สำคัญ เห็นได้ชัดว่าคุณจะไม่ใช้มันในรหัสการผลิต
sleske

จริงฉันเพิ่งจะใช้เวลาสักครู่เพื่อคิดออกสิ่งที่เกิดขึ้นเมื่อฉันดูผลลัพธ์
Ilmari Karonen

6

ฉันค่อนข้างแน่ใจว่า JVM จะทำให้แน่ใจอย่างแน่นอนว่ามีหน่วยความจำอย่างน้อยพอที่จะโยนข้อยกเว้นก่อนที่หน่วยความจำจะหมด


1
แต่พวกเขาจะเผชิญกับสถานการณ์นี้ไม่ว่าหน่วยความจำจะถูกสงวนไว้เท่าไหร่: stackoverflow.com/questions/9261705/…
Pacerier

4

ข้อยกเว้นที่บ่งบอกถึงความพยายามในการละเมิดขอบเขตของสภาพแวดล้อมหน่วยความจำที่ได้รับการจัดการจะถูกจัดการโดยรันไทม์ของสภาพแวดล้อมดังกล่าวในกรณีนี้คือ JVM JVM เป็นกระบวนการของตัวเองซึ่งใช้งานแอพพลิเคชั่น IL ของคุณ หากโปรแกรมพยายามโทรที่ขยาย call stack เกินขีด จำกัด หรือจัดสรรหน่วยความจำมากกว่า JVM สามารถจองรันไทม์เองก็จะฉีดข้อยกเว้นซึ่งจะทำให้ call stack ไม่สามารถเปิดได้ โดยไม่คำนึงถึงจำนวนหน่วยความจำที่โปรแกรมของคุณต้องการในปัจจุบันหรือความลึกสแต็กการโทร JVM จะจัดสรรหน่วยความจำให้เพียงพอภายในขอบเขตกระบวนการของตัวเองเพื่อสร้างข้อยกเว้นดังกล่าวและฉีดเข้าไปในรหัสของคุณ


"JVM จะจัดสรรหน่วยความจำให้เพียงพอภายในขอบเขตของกระบวนการเพื่อสร้างข้อยกเว้นดังกล่าว" แต่หากรหัสของคุณยังคงมีการอ้างอิงถึงข้อยกเว้นนั้นดังนั้นจึงไม่สามารถนำมาใช้ซ้ำได้จะสร้างอีกอย่างไร หรือคุณแนะนำว่ามันมีวัตถุแบบซิงเกิล OOME หรือไม่?
Raedwald

ฉันไม่ได้แนะนำอย่างนั้น หากโปรแกรมของคุณดักจับและหยุดการทำงานทุกข้อยกเว้นที่เคยสร้างรวมถึงรายการที่สร้างและฉีดโดย JVM หรือ OS ในที่สุดแล้วตัว JVM เองจะเกินขอบเขตที่กำหนดโดยระบบปฏิบัติการและระบบปฏิบัติการจะปิดลงสำหรับ GPF หรือ ข้อผิดพลาดที่คล้ายกัน อย่างไรก็ตามนั่นเป็นการออกแบบที่ไม่ดีตั้งแต่แรก ควรมีการจัดการข้อยกเว้นและออกไปนอกขอบเขตหรือถูกโยนออกไป และคุณไม่ควรพยายามที่จะจับและดำเนินการต่อใน SOE หรือ OOME; นอกเหนือจาก "การล้างข้อมูล" เพื่อให้คุณสามารถออกจากระบบได้อย่างงดงามไม่มีอะไรที่คุณสามารถทำได้เพื่อดำเนินการต่อในสถานการณ์เหล่านั้น
KeithS

"นั่นเป็นการออกแบบที่ไม่ดีตั้งแต่แรก": การออกแบบที่แย่มาก แต่อวดรู้JVMจะเป็นไปตามข้อกำหนดหากมันล้มเหลวในลักษณะนั้นหรือไม่?
Raedwald

4

ดูเหมือนว่าคุณจะสับสนหน่วยความจำเสมือนที่สงวนไว้โดย JVM ซึ่ง JVM รันโปรแกรม Java ด้วยหน่วยความจำดั้งเดิมของโฮสต์ OS ซึ่ง JVM นั้นทำงานเป็นกระบวนการดั้งเดิม JVM บนเครื่องของคุณกำลังทำงานในหน่วยความจำที่จัดการโดยระบบปฏิบัติการไม่ใช่ในหน่วยความจำที่ JVM สงวนไว้ให้เรียกใช้โปรแกรม Java

อ่านเพิ่มเติม:

และเป็นบันทึกสุดท้ายพยายามที่จะจับjava.lang.Error (และการเรียนลูกหลานของมัน) เพื่อพิมพ์ stacktrace อาจไม่ให้ข้อมูลที่เป็นประโยชน์ใด ๆ คุณต้องการกองการถ่ายโอนข้อมูลแทน


4

เพื่อชี้แจงเพิ่มเติมเกี่ยวกับคำตอบของ @Graham Borland โดยหน้าที่ JVM จะทำสิ่งนี้เมื่อเริ่มต้น:

private static final OutOfMemoryError OOME = new OutOfMemoryError();

หลังจากนั้น JVM จะดำเนินการตามหนึ่งในจาวาไบต์ต่อไปนี้: 'ใหม่', 'anewarray' หรือ 'multianewarray' คำสั่งนี้ทำให้ JVM ดำเนินการหลายขั้นตอนในสภาพหน่วยความจำไม่เพียงพอ:

  1. allocate()เรียกใช้ฟังก์ชั่นพื้นเมืองพูด allocate()พยายามจัดสรรหน่วยความจำสำหรับอินสแตนซ์ใหม่ของคลาสหรืออาร์เรย์เฉพาะ
  2. การร้องขอการจัดสรรนั้นล้มเหลวดังนั้น JVM จะเรียกใช้ฟังก์ชันเนทีฟdoGC()อื่นซึ่งจะพยายามทำการรวบรวมขยะ
  3. เมื่อฟังก์ชันนั้นส่งคืนallocate()พยายามจัดสรรหน่วยความจำสำหรับอินสแตนซ์อีกครั้ง
  4. หากสิ่งนั้นล้มเหลว (*) ดังนั้น JVM ภายใน allocate () จะทำการ a throw OOME;โดยอ้างถึง OOME ที่อินสแตนซ์เมื่อเริ่มต้น โปรดทราบว่ามันไม่จำเป็นต้องจัดสรร OOME มันแค่อ้างถึงมัน

เห็นได้ชัดว่านี่ไม่ใช่ขั้นตอนตามตัวอักษร พวกเขาจะแตกต่างจาก JVM ถึง JVM ในการนำไปใช้ แต่นี่เป็นแนวคิดระดับสูง

(*) งานจำนวนมากเกิดขึ้นที่นี่ก่อนที่จะล้มเหลว JVM จะพยายามล้างวัตถุ SoftReference พยายามจัดสรรโดยตรงไปยังการสร้างที่ครอบครองเมื่อใช้ตัวสะสม generational และสิ่งอื่น ๆ เช่นการสรุป


3

คำตอบที่บอกว่า JVM จะจัดสรรล่วงหน้าOutOfMemoryErrorsนั้นถูกต้องแน่นอน
นอกเหนือจากการทดสอบโดยการยั่วยุสถานการณ์ความจำไม่เพียงพอเราสามารถตรวจสอบฮีปของ JVM ใด ๆ (ฉันใช้โปรแกรมขนาดเล็กที่เพิ่งเข้าสู่โหมดสลีปรันโดยใช้ JVM Hotspot จาก Java 8 การอัพเดต 31)

การใช้jmapเราเห็นว่ามี OutOfMemoryError 9 อินสแตนซ์ (แม้ว่าเราจะมีหน่วยความจำมากมาย):

> jmap -histo 12103 | grep OutOfMemoryError
 71: 9 288 java.lang.OutOfMemoryError
170: 1 32 [Ljava.lang.OutOfMemoryError;

จากนั้นเราสามารถสร้างกองดัมพ์:

> jmap -dump: format = b, file = heap.hprof 12315

และเปิดโดยใช้Eclipse Memory Analyzerโดยที่เคียวรี OQL แสดงว่า JVM จริง ๆ แล้วดูเหมือนว่าจะจัดสรรล่วงหน้าOutOfMemoryErrorsสำหรับข้อความที่เป็นไปได้ทั้งหมด:

ป้อนคำอธิบายรูปภาพที่นี่

รหัสสำหรับ Java 8 Hotspot JVM ที่ preallocates เหล่านี้จริงสามารถพบได้ที่นี่และมีลักษณะเช่นนี้ (โดยบางส่วนถูกละเว้น):

...
// Setup preallocated OutOfMemoryError errors
k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_OutOfMemoryError(), true, CHECK_false);
k_h = instanceKlassHandle(THREAD, k);
Universe::_out_of_memory_error_java_heap = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_metaspace = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_class_metaspace = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_array_size = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_gc_overhead_limit =
  k_h->allocate_instance(CHECK_false);

...

if (!DumpSharedSpaces) {
  // These are the only Java fields that are currently set during shared space dumping.
  // We prefer to not handle this generally, so we always reinitialize these detail messages.
  Handle msg = java_lang_String::create_from_str("Java heap space", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_java_heap, msg());

  msg = java_lang_String::create_from_str("Metaspace", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_metaspace, msg());
  msg = java_lang_String::create_from_str("Compressed class space", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_class_metaspace, msg());

  msg = java_lang_String::create_from_str("Requested array size exceeds VM limit", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_array_size, msg());

  msg = java_lang_String::create_from_str("GC overhead limit exceeded", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_gc_overhead_limit, msg());

  msg = java_lang_String::create_from_str("/ by zero", CHECK_false);
  java_lang_Throwable::set_message(Universe::_arithmetic_exception_instance, msg());

  // Setup the array of errors that have preallocated backtrace
  k = Universe::_out_of_memory_error_java_heap->klass();
  assert(k->name() == vmSymbols::java_lang_OutOfMemoryError(), "should be out of memory error");
  k_h = instanceKlassHandle(THREAD, k);

  int len = (StackTraceInThrowable) ? (int)PreallocatedOutOfMemoryErrorCount : 0;
  Universe::_preallocated_out_of_memory_error_array = oopFactory::new_objArray(k_h(), len, CHECK_false);
  for (int i=0; i<len; i++) {
    oop err = k_h->allocate_instance(CHECK_false);
    Handle err_h = Handle(THREAD, err);
    java_lang_Throwable::allocate_backtrace(err_h, CHECK_false);
    Universe::preallocated_out_of_memory_errors()->obj_at_put(i, err_h());
  }
  Universe::_preallocated_out_of_memory_error_avail_count = (jint)len;
}
...

และรหัสนี้แสดงให้เห็นว่า JVM จะพยายามใช้ข้อผิดพลาดที่จัดสรรไว้ล่วงหน้าอย่างใดอย่างหนึ่งซึ่งมีพื้นที่สำหรับการติดตามสแต็กจากนั้นย้อนกลับไปที่ข้อผิดพลาดหนึ่งโดยไม่มีการติดตามสแต็ก:

oop Universe::gen_out_of_memory_error(oop default_err) {
  // generate an out of memory error:
  // - if there is a preallocated error with backtrace available then return it wth
  //   a filled in stack trace.
  // - if there are no preallocated errors with backtrace available then return
  //   an error without backtrace.
  int next;
  if (_preallocated_out_of_memory_error_avail_count > 0) {
    next = (int)Atomic::add(-1, &_preallocated_out_of_memory_error_avail_count);
    assert(next < (int)PreallocatedOutOfMemoryErrorCount, "avail count is corrupt");
  } else {
    next = -1;
  }
  if (next < 0) {
    // all preallocated errors have been used.
    // return default
    return default_err;
  } else {
    // get the error object at the slot and set set it to NULL so that the
    // array isn't keeping it alive anymore.
    oop exc = preallocated_out_of_memory_errors()->obj_at(next);
    assert(exc != NULL, "slot has been used already");
    preallocated_out_of_memory_errors()->obj_at_put(next, NULL);

    // use the message from the default error
    oop msg = java_lang_Throwable::message(default_err);
    assert(msg != NULL, "no message");
    java_lang_Throwable::set_message(exc, msg);

    // populate the stack trace and return it.
    java_lang_Throwable::fill_in_stack_trace_of_preallocated_backtrace(exc);
    return exc;
  }
}

โพสต์ที่ดีฉันจะยอมรับคำตอบนี้เป็นเวลาหนึ่งสัปดาห์เพื่อให้มองเห็นได้ชัดเจนขึ้นก่อนจะย้อนกลับไปยังคำตอบก่อนหน้า
Pacerier

ใน Java 8 และสูงกว่าปลัดอวกาศรุ่นได้รับการลบออกอย่างสมบูรณ์และคุณจะไม่สามารถระบุขนาดหน่วยความจำการจัดสรรพื้นที่กองจาก Java 8 Metaspaceและชั้นแบบไดนามิกจัดการหน่วยความจำเมตาดาต้าตั้งแต่พวกเขาได้เปิดตัวสูงขึ้นเรียกว่า มันจะดีถ้าคุณสามารถแสดงชิ้นส่วนของรหัสที่เหมาะสำหรับ PermGen และเปรียบเทียบกับ Metaspace ด้วย
Buhake Sindi

@BuhakeSindi - ฉันไม่เห็นสิ่งที่คนรุ่นถาวรจะทำอย่างไรกับสิ่งนี้ ไม่มีการจัดสรรวัตถุใหม่ในการสร้างแบบถาวรตามที่คุณระบุไว้ในคำตอบของคุณ คุณไม่เคยพูดถึงความจริงที่ว่า OutOfMemoryErrors ได้รับการจัดสรรล่วงหน้า (ซึ่งเป็นคำตอบที่แท้จริงของคำถาม)
Johan Kaving

1
ตกลงสิ่งที่ฉันพูดคือจาก Java 8 การจัดสรรวัตถุจะถูกคำนวณแบบไดนามิกในขณะที่ก่อนที่มันจะถูกจัดสรรในพื้นที่ฮีปที่กำหนดไว้ล่วงหน้าดังนั้นอาจจัดสรรล่วงหน้าไว้ล่วงหน้า แม้ว่า OOME จะได้รับการจัดสรรล่วงหน้า แต่ก็มี "การคำนวณ" เพื่อพิจารณาว่า OOME จำเป็นต้องถูกโยนทิ้งหรือไม่ (เพราะเหตุใดฉันจึงอ้างอิงถึงข้อกำหนด JLS)
Buhake Sindi

1
ขนาดฮีพของ Java นั้นถูกกำหนดไว้ล่วงหน้าใน Java 8 เหมือนกับเมื่อก่อน การสร้างแบบถาวรเป็นส่วนหนึ่งของกองที่มีเมตาดาต้าระดับ, สตริงฝึกงานและสถิตยศาสตร์ระดับ มันมีขนาดที่ จำกัด ซึ่งจำเป็นต้องปรับแต่งแยกต่างหากจากขนาดฮีพทั้งหมด ใน Java 8 ข้อมูลเมตาของคลาสถูกย้ายไปยังหน่วยความจำดั้งเดิมและสตริงที่อยู่ภายในและสแตติกระดับได้ถูกย้ายไปที่ฮีป Java ปกติ (ดูตัวอย่างที่นี่: infoq.com/articles/Java-PERMGEN-Removed )
Johan Kaving
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.