Điều gì xảy ra khi không đủ bộ nhớ để ném OutOfMemoryError?


207

Tôi biết rằng mọi đối tượng đều yêu cầu bộ nhớ heap và mọi tham chiếu nguyên thủy / tham chiếu trên ngăn xếp đều yêu cầu bộ nhớ stack.

Khi tôi cố gắng tạo một đối tượng trên heap và không đủ bộ nhớ để làm điều đó, JVM tạo một java.lang.OutOfMemoryError trên heap và ném nó cho tôi.

Vì vậy, điều này có nghĩa là có một số bộ nhớ được dành cho JVM khi khởi động.

Điều gì xảy ra khi bộ nhớ dành riêng này được sử dụng hết (chắc chắn sẽ được sử dụng hết, đọc thảo luận bên dưới) và JVM không có đủ bộ nhớ trên heap để tạo một thể hiện của java.lang.OutOfMemoryError ?

Nó chỉ treo? Hay anh ta sẽ ném cho tôi nullvì không có ký ức nào cho newOOM?

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
}

==

Tại sao JVM không thể dự trữ đủ bộ nhớ?

Cho dù có bao nhiêu bộ nhớ được bảo lưu, vẫn có thể sử dụng hết bộ nhớ đó nếu JVM không có cách "lấy lại" bộ nhớ đó:

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
                    }
                }
            }
        }
    }
}

Hay chính xác hơn:

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

2
câu trả lời của bạn sẽ phụ thuộc vào JVM ở một mức độ lớn
MozenRath

23
Một thư viện điện thoại mà tôi đã từng sử dụng (vào những năm 90) đã từng bắt OutOfMemoryExceptionvà sau đó làm một việc gì đó liên quan đến việc tạo ra một bộ đệm lớn ...
Tom Hawtin - tackline

@ TomHawtin-tackline Điều gì xảy ra nếu các hoạt động liên quan đến việc đó ném một OOM khác?
Pacerier

38
Giống như điện thoại di động, nó hết pin nhưng nó có đủ pin để tiếp tục spam "Bạn sắp hết pin".
Kazuma

1
"Điều gì xảy ra khi bộ nhớ dành riêng này được sử dụng hết": điều đó chỉ có thể xảy ra nếu chương trình bắt được phần đầu tiên OutOfMemoryErrorvà giữ lại một tham chiếu đến nó. Nó truyền đạt rằng việc bắt một OutOfMemoryErrorkhông hữu ích như người ta nghĩ, bởi vì bạn có thể đảm bảo hầu như không có gì về trạng thái của chương trình của bạn khi bắt nó. Xem stackoverflow.com/questions/8728866/
Mạnh

Câu trả lời:


145

JVM không bao giờ thực sự hết bộ nhớ. Nó không tính toán bộ nhớ của đống heap trước.

Các kết cấu của JVM, Chương 3 , phần 3.5.2 trạng thái:

  • Nếu các ngăn xếp máy ảo Java có thể được mở rộng một cách linh hoạt và cố gắng mở rộng nhưng không đủ bộ nhớ để thực hiện việc mở rộng hoặc nếu không đủ bộ nhớ để tạo ngăn xếp máy ảo Java ban đầu cho một luồng mới, thì ảo Java Máy ném một OutOfMemoryError.

Đối với Heap , Mục 3.5.3.

  • Nếu một tính toán đòi hỏi nhiều heap hơn có thể được cung cấp bởi hệ thống quản lý lưu trữ tự động, thì máy ảo Java sẽ ném một OutOfMemoryError.

Vì vậy, nó thực hiện một tính toán trước khi thực hiện phân bổ đối tượng.


Điều xảy ra là JVM cố gắng phân bổ bộ nhớ cho một đối tượng trong bộ nhớ được gọi là vùng thế hệ vĩnh viễn (hoặc PermSpace). Nếu việc phân bổ không thành công (ngay cả sau khi JVM gọi Trình thu gom rác để thử và phân bổ không gian trống), nó sẽ ném một OutOfMemoryError. Ngay cả các trường hợp ngoại lệ yêu cầu một không gian bộ nhớ nên lỗi sẽ được ném vô thời hạn.

Đọc thêm. ? Hơn nữa, OutOfMemoryErrorcó thể xảy ra khác nhau cấu trúc JVM .


10
Ý tôi là có, nhưng máy ảo Java cũng cần bộ nhớ để ném OutOfMemoryError? Điều gì xảy ra khi không có bộ nhớ để ném OOM?
Pacerier

5
Nhưng nếu JVM không trả lại tham chiếu cho cùng một thể hiện của OOM, bạn có đồng ý rằng cuối cùng nó sẽ hết bộ nhớ dành riêng không? (như được minh họa trong mã trong câu hỏi)
Pacerier

1
Cho phép tôi đặt tham chiếu đến nhận xét của Graham tại đây: stackoverflow.com/questions/9261705/iêu
Pacerier

2
Có thể tốt đẹp nếu VM giữ lại một đơn vị ngoại lệ OOM cho trường hợp cực đoan đã nêu, và tại một nhà máy điện hạt nhân.
John K

8
@ John: Tôi hy vọng các nhà máy điện hạt nhân không được lập trình bằng Java, giống như các tàu con thoi vũ trụ và máy bay Boeing 757 không được lập trình bằng Java.
Dietrich Epp

64

Graham Borland dường như đã đúng : ít nhất JVM của tôi dường như sử dụng lại OutOfMemoryErrors. Để kiểm tra điều này, tôi đã viết một chương trình thử nghiệm đơn giản:

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

Chạy nó tạo ra đầu ra này:

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

BTW, JVM tôi đang chạy (trên Ubuntu 10.04) là đây:

$ 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)

Chỉnh sửa: Tôi đã cố gắng xem điều gì sẽ xảy ra nếu tôi buộc JVM chạy hết bộ nhớ bằng chương trình sau:

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

Hóa ra, nó dường như lặp lại mãi mãi. Tuy nhiên, thật tò mò, cố gắng chấm dứt chương trình với Ctrl+ Ckhông hoạt động, mà chỉ đưa ra thông báo sau:

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


Thử nghiệm tuyệt vời, tương tự đối với tôi với phiên bản "1.7.0_01" Java HotSpot (TM) Máy chủ 64-bit VM
Pacerier

Hấp dẫn. Điều đó làm cho có vẻ như JVM hoàn toàn không hiểu đệ quy đuôi ... (Như thể nó đang thực hiện tái sử dụng ngăn xếp đệ quy đuôi, nhưng không đủ để dọn sạch tất cả bộ nhớ ...)
Izkata

Sửa đổi mã của bạn để xem tôi nhận được bao nhiêu mã khác nhau - Tôi luôn nhận được nhánh khác được thực thi chính xác 5 lần.
Irfy

@Izkata: Tôi muốn nói rằng đó là một quyết định có ý thức trong việc phân bổ trước các nOOM và tái sử dụng một trong số chúng sau đó, để OOM luôn có thể bị ném. JVM của Sun / Oracle không hỗ trợ đệ quy đuôi ở tất cả IIRC?
Irfy

10
@Izkata Vòng lặp đang chạy dường như vô tận vì JVM liên tục ném một và cùng một (5 hoặc hơn) OOM sau khi hết bộ nhớ. Vì vậy, nó có ncác khung trên ngăn xếp và cuối cùng nó tạo ra và phá hủy khung hình n+1vĩnh cửu, mang lại vẻ ngoài chạy vô tận.
Irfy

41

Hầu hết các môi trường thời gian chạy sẽ phân bổ trước khi khởi động, hoặc nếu không dự trữ, đủ bộ nhớ để xử lý các tình huống đói bộ nhớ. Tôi tưởng tượng hầu hết các triển khai JVM lành mạnh sẽ làm điều này.


1
stackoverflow.com/questions/9261215/ Mạnh : Đúng nhưng nếu JVM dành 100 đơn vị bộ nhớ để làm điều đó và đã dành 1 đơn vị cho OOM đầu tiên, điều gì xảy ra nếu trong khối bắt OOM của tôi, tôi có làm e.printStackTrace () không? e.printStackTrace () cũng yêu cầu bộ nhớ. Sau đó, JVM sẽ dành một đơn vị bộ nhớ khác để ném cho tôi một OOM khác (còn lại 98 đơn vị) và tôi bắt được nó với một e.printStackTrace () để JVM ném cho tôi một OOM khác (còn lại 97 đơn vị) và tôi cũng bị bắt và tôi muốn ..
Pacerier

3
Đây chính xác là lý do tại sao OOME không bao giờ được sử dụng để bao gồm dấu vết ngăn xếp - dấu vết ngăn xếp chiếm bộ nhớ! OOME chỉ bắt đầu cố gắng để bao gồm một chồng dấu vết trong java 6 ( blogs.oracle.com/alanb/entry/... ). Tôi giả sử rằng nếu theo dõi ngăn xếp là không thể, thì ngoại lệ được ném mà không có dấu vết ngăn xếp.
Sean Reilly

1
@SeanReilly Ý tôi là một ngoại lệ không có dấu vết ngăn xếp vẫn là Object, vẫn cần bộ nhớ. Bộ nhớ là cần thiết cho dù có cung cấp dấu vết ngăn xếp hay không. Có đúng là trong khối bắt nếu không còn bộ nhớ để tạo OOM (không có bộ nhớ để tạo ngay cả khi không có dấu vết ngăn xếp), thì tôi sẽ bắt được null?
Pacerier

17
JVM có thể trả về nhiều tham chiếu cho một cá thể tĩnh của ngoại lệ OOM. Vì vậy, ngay cả khi catchmệnh đề của bạn cố gắng sử dụng nhiều bộ nhớ hơn, JVM vẫn có thể tiếp tục ném cùng một cá thể OOM nhiều lần.
Graham Borland

1
@TheEliteGentman Tôi đồng ý rằng đó cũng là những câu trả lời rất hay, nhưng JVM sống trên một máy vật lý, Những câu trả lời đó không giải thích làm thế nào JVM có thể có đủ bộ nhớ để luôn cung cấp một thể hiện của OOM. "Nó luôn luôn là cùng một ví dụ" dường như để giải quyết câu đố.
Pacerier

23

Lần trước tôi đang làm việc với Java và sử dụng trình gỡ lỗi, trình kiểm tra heap cho thấy JVM đã cấp phát một thể hiện của OutOfMemoryError khi khởi động. Nói cách khác, nó phân bổ đối tượng trước khi chương trình của bạn có cơ hội bắt đầu tiêu thụ, chứ đừng nói đến việc hết bộ nhớ.


12

Từ Thông số JVM, Chương 3.5.2:

Nếu các ngăn xếp máy ảo Java có thể được mở rộng một cách linh hoạt và cố gắng mở rộng nhưng không đủ bộ nhớ để thực hiện việc mở rộng hoặc nếu không đủ bộ nhớ để tạo ngăn xếp máy ảo Java ban đầu cho một luồng mới, thì ảo Java Máy ném mộtOutOfMemoryError .

Mỗi máy ảo Java phải đảm bảo rằng nó sẽ ném OutOfMemoryError. Điều đó ngụ ý rằng nó phải có khả năng tạo ra một thể hiện OutOfMemoryError(hoặc phải được tạo trước) ngay cả khi không còn chỗ trống.

Mặc dù không phải đảm bảo, nhưng vẫn còn đủ bộ nhớ để bắt và in một ngăn xếp đẹp ...

Thêm vào

Bạn đã thêm một số mã để hiển thị, rằng JVM có thể hết dung lượng heap nếu nó phải ném nhiều hơn một OutOfMemoryError. Nhưng việc thực hiện như vậy sẽ vi phạm các yêu cầu từ phía trên.

Không có yêu cầu rằng các trường hợp ném OutOfMemoryErrorlà duy nhất hoặc được tạo theo yêu cầu. Một JVM có thể chuẩn bị chính xác một thể hiện OutOfMemoryErrortrong khi khởi động và ném nó bất cứ khi nào nó hết dung lượng heap - một lần, trong môi trường bình thường. Nói cách khác: ví dụ OutOfMemoryErrormà chúng ta thấy có thể là một người độc thân.


Tôi đoán để thực hiện điều này, nó sẽ phải kiềm chế ghi lại dấu vết ngăn xếp, nếu không gian chật hẹp.
Raedwald

@Raedwald: Thực tế, đây chỉ là những gì Oracle VM làm, xem câu trả lời của tôi.
sleske

11

Câu hỏi thú vị :-). Trong khi những người khác đã đưa ra những lời giải thích tốt về các khía cạnh lý thuyết, tôi quyết định thử nó. Đây là trên Oracle JDK 1.6.0_26, Windows 7 64 bit.

Kiểm tra thiết lập

Tôi đã viết một chương trình đơn giản để cạn kiệt bộ nhớ (xem bên dưới).

Chương trình chỉ tạo một tĩnh java.util.Listvà tiếp tục nhồi các chuỗi mới vào đó, cho đến khi OOM được ném. Sau đó, nó bắt nó và tiếp tục nhồi trong một vòng lặp vô tận (JVM nghèo ...).

Kết quả kiểm tra

Như người ta có thể thấy từ đầu ra, bốn lần đầu tiên OOME được ném, nó đi kèm với một dấu vết ngăn xếp. Sau đó, các OOME tiếp theo chỉ in java.lang.OutOfMemoryError: Java heap spacenếu printStackTrace()được gọi.

Vì vậy, rõ ràng JVM đã nỗ lực in dấu vết ngăn xếp nếu có thể, nhưng nếu bộ nhớ thực sự chật, nó chỉ bỏ qua dấu vết, giống như các câu trả lời khác gợi ý.

Cũng thú vị là mã băm của OOME. Lưu ý rằng một số OOME đầu tiên đều có các giá trị băm khác nhau. Khi JVM bắt đầu bỏ qua dấu vết ngăn xếp, hàm băm luôn giống nhau. Điều này cho thấy rằng JVM sẽ sử dụng các cá thể OOME mới (preallocated?) Càng lâu càng tốt, nhưng nếu đẩy đến, nó sẽ chỉ sử dụng lại cá thể đó chứ không phải không có gì để ném.

Đầu ra

Lưu ý: Tôi đã cắt bớt một số dấu vết ngăn xếp để làm cho đầu ra dễ đọc hơn ("[...]").

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...
[...]

Chương trình

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

    }
}

Khi đẩy đến, nó không thể phân bổ bộ nhớ cho OOME, vì vậy nó sẽ xóa những cái đã được tạo.
Buhake Sindi

1
Lưu ý nhỏ: một số đầu ra của bạn dường như không theo thứ tự, có lẽ vì bạn đang in System.outnhưng printStackTrace()sử dụng System.errtheo mặc định. Bạn có thể nhận được kết quả tốt hơn bằng cách sử dụng một trong hai luồng nhất quán.
Ilmari Karonen

@IlmariKaronen: Vâng, tôi nhận thấy điều đó. Đó chỉ là một ví dụ, vì vậy nó không thành vấn đề. Rõ ràng là bạn sẽ không sử dụng nó trong mã sản xuất.
sleske

Đúng vậy, tôi chỉ mất một lúc để tìm hiểu những gì **** đang diễn ra khi tôi lần đầu tiên nhìn vào đầu ra.
Ilmari Karonen

6

Tôi khá chắc chắn, JVM sẽ đảm bảo chắc chắn rằng nó có ít nhất đủ bộ nhớ để ném một ngoại lệ trước khi hết bộ nhớ.


1
Nhưng họ sẽ gặp phải tình huống này cho dù có bao nhiêu bộ nhớ được bảo lưu: stackoverflow.com/questions/9261705/iêu
Pacerier

4

Các ngoại lệ cho thấy nỗ lực vi phạm các ranh giới của môi trường bộ nhớ được quản lý được xử lý bởi thời gian chạy của môi trường đã nói, trong trường hợp này là JVM. JVM là quy trình riêng của nó, đang chạy IL của ứng dụng của bạn. Nếu một chương trình cố gắng thực hiện một cuộc gọi mở rộng ngăn xếp cuộc gọi vượt quá giới hạn hoặc phân bổ nhiều bộ nhớ hơn JVM có thể dự trữ, thì thời gian chạy sẽ tạo ra một ngoại lệ, điều này sẽ khiến cho ngăn xếp cuộc gọi bị hủy. Bất kể dung lượng bộ nhớ mà chương trình của bạn hiện đang cần, hoặc ngăn xếp cuộc gọi của nó sâu đến mức nào, JVM sẽ phân bổ đủ bộ nhớ trong giới hạn quy trình của chính nó để tạo ngoại lệ đã nói và đưa mã vào mã của bạn.


"JVM sẽ phân bổ đủ bộ nhớ trong giới hạn quy trình của chính nó để tạo ngoại lệ đã nói" nhưng nếu mã của bạn giữ lại một tham chiếu đến ngoại lệ đó, vì vậy nó không thể được sử dụng lại, làm thế nào nó có thể tạo ra một cái khác? Hay bạn đang đề nghị nó có một đối tượng OOME Singleton đặc biệt?
Raedwald

Tôi không gợi ý điều đó. Nếu chương trình của bạn bẫy và bám vào mọi ngoại lệ đã được tạo, bao gồm các ngoại lệ được tạo và chèn bởi JVM hoặc OS, thì cuối cùng, chính JVM sẽ vượt quá một số ranh giới do HĐH đặt ra và HĐH sẽ tắt nó cho GPF hoặc lỗi tương tự. Tuy nhiên, đó là thiết kế tồi ở nơi đầu tiên; các trường hợp ngoại lệ nên được xử lý và sau đó đi ra khỏi phạm vi hoặc bị loại bỏ. Và bạn KHÔNG BAO GIỜ cố gắng bắt và tiếp tục SOE hoặc OOME; ngoài việc "dọn dẹp" để bạn có thể thoát ra một cách duyên dáng, bạn không thể làm gì để tiếp tục thực hiện trong những tình huống đó.
KeithS

"Đó là thiết kế tồi ở nơi đầu tiên": thiết kế khủng khiếp. Nhưng về mặt giáo dục, liệu JVM có phù hợp với đặc tả nếu nó thất bại theo cách đó không?
Raedwald

4

Bạn dường như đang nhầm lẫn bộ nhớ ảo được bảo lưu bởi JVM trong đó JVM chạy các chương trình Java với bộ nhớ riêng của HĐH máy chủ trong đó JVM được chạy như một quy trình riêng. JVM trên máy của bạn đang chạy trong bộ nhớ do HĐH quản lý, không phải trong bộ nhớ mà JVM đã dành riêng để chạy các chương trình Java.

Đọc thêm:

Và như một lưu ý cuối cùng, cố gắng nắm bắt một java.lang.Error (và lớp hậu duệ của nó) để in một stacktrace có thể không cung cấp cho bạn bất kỳ thông tin hữu ích. Bạn muốn một đống đổ thay thế.


4

Để làm rõ hơn về câu trả lời của @Graham Borland, về mặt chức năng, JVM thực hiện điều này khi khởi động:

private static final OutOfMemoryError OOME = new OutOfMemoryError();

Sau đó, JVM thực thi một trong các mã byte Java sau: 'new', 'anewarray' hoặc 'multianewarray'. Hướng dẫn này khiến JVM thực hiện một số bước trong tình trạng hết bộ nhớ:

  1. Gọi một hàm riêng, nói allocate() . allocate()cố gắng phân bổ bộ nhớ cho một số phiên bản mới của một lớp hoặc mảng cụ thể.
  2. Yêu cầu phân bổ đó không thành công, do đó, JVM gọi một hàm riêng khác, nói doGC() , cố gắng thực hiện việc thu gom rác.
  3. Khi chức năng đó trở lại, allocate() cố gắng phân bổ bộ nhớ cho ví dụ một lần nữa.
  4. Nếu điều đó không thành công (*), thì JVM, trong allocate (), chỉ cần thực hiện một throw OOME; , tham chiếu đến OOME mà nó khởi tạo khi khởi động. Lưu ý rằng nó không phải phân bổ OOME đó, nó chỉ đề cập đến nó.

Rõ ràng, đây không phải là các bước theo nghĩa đen; họ sẽ thay đổi từ JVM sang JVM, nhưng đây là ý tưởng cấp cao.

(*) Một lượng đáng kể công việc xảy ra ở đây trước khi thất bại. JVM sẽ cố gắng xóa các đối tượng SoftReference, cố gắng phân bổ trực tiếp vào thế hệ được thuê khi sử dụng trình thu thập thế hệ và có thể các thứ khác, như hoàn thiện.


3

Các câu trả lời nói rằng JVM sẽ phân bổ trước OutOfMemoryErrorslà thực sự chính xác.
Ngoài việc kiểm tra điều này bằng cách kích hoạt tình huống hết bộ nhớ, chúng ta có thể kiểm tra hàng đống JVM (Tôi đã sử dụng một chương trình nhỏ chỉ ngủ, chạy nó bằng Hotspot JVM của Oracle từ bản cập nhật Java 8 31).

Sử dụng jmapchúng tôi thấy rằng dường như có 9 trường hợp OutOfMemoryError (mặc dù chúng tôi có rất nhiều bộ nhớ):

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

Sau đó chúng ta có thể tạo ra một đống heap:

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

và mở nó bằng Trình phân tích bộ nhớ Eclipse , trong đó một truy vấn OQL cho thấy JVM thực sự dường như phân bổ trước OutOfMemoryErrorscho tất cả các thông báo có thể:

nhập mô tả hình ảnh ở đây

Mã cho JVM Hotspot Java 8 thực sự phát sinh những thứ này có thể được tìm thấy ở đây và trông như thế này (với một số phần bị bỏ qua):

...
// 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;
}
...

mã này cho thấy rằng JVM trước tiên sẽ cố gắng sử dụng một trong các lỗi được phân bổ trước có khoảng trống cho dấu vết ngăn xếp và sau đó rơi trở lại một lỗi mà không có dấu vết ngăn xếp:

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

Bài đăng tốt, tôi sẽ chấp nhận câu trả lời này trong một tuần để hiển thị rõ hơn trước khi quay lại câu trả lời trước đó.
Pacerier

Trong Java 8 trở lên, Không gian thế hệ vĩnh viễn đã bị xóa hoàn toàn và bạn không còn có thể chỉ định kích thước bộ nhớ phân bổ không gian heap từ Java 8 trở lên kể từ khi chúng được giới thiệu quản lý bộ nhớ siêu dữ liệu lớp động Metaspace. Sẽ thật tuyệt nếu bạn có thể hiển thị một đoạn mã phục vụ cho PermGen và so sánh nó với Metaspace.
Buhake Sindi

@BuhakeSindi - Tôi không thấy Thế hệ Thường trực phải làm gì với điều này. Các đối tượng mới không được phân bổ trong Thế hệ vĩnh viễn như bạn nêu trong câu trả lời của mình. Bạn cũng không bao giờ đề cập đến thực tế là OutOfMemoryErrors được phân bổ trước (đó là câu trả lời thực tế cho câu hỏi).
Johan Kaving

1
Ok, điều tôi đang nói là từ Java 8, phân bổ đối tượng được tính toán linh hoạt trong khi trước khi nó được phân bổ trên một không gian heap được xác định trước, do đó có lẽ nó được phân bổ trước. Mặc dù OOME được phân bổ trước, vẫn có "tính toán" để xác định xem OOME có cần được ném hay không (do đó tại sao tôi đặt tham chiếu đến đặc tả JLS).
Buhake Sindi

1
Kích thước heap Java giống như được xác định trước trong Java 8 như trước đây. Thế hệ vĩnh viễn là một phần của đống chứa dữ liệu meta lớp, chuỗi nội bộ và thống kê lớp. Nó có kích thước giới hạn cần được điều chỉnh tách biệt với tổng kích thước heap. Trong Java 8, dữ liệu meta lớp đã được chuyển sang bộ nhớ riêng và các chuỗi và các thống kê lớp đã được chuyển sang heap Java thông thường (xem ví dụ tại đây: infoq.com/articles/Java-PERMGEN-Remond ).
Johan Kaving
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.