簡體   English   中英

當沒有足夠的內存來拋出OutOfMemoryError時會發生什么?

[英]What happens when there's insufficient memory to throw an OutOfMemoryError?

我知道每個對象都需要堆內存,堆棧上的每個原語/引用都需要堆棧內存。

當我嘗試在堆上創建一個對象並且沒有足夠的內存來執行此操作時,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);
    }
}

JVM永遠不會耗盡內存。 它預先進行堆棧的內存計算。

JVM結構,第3章 ,第3.5.2節說明:

  • 如果可以動態擴展Java虛擬機堆棧,並且嘗試進行擴展但是可以使內存不足以實現擴展,或者如果可用的內存不足以為新線程創建初始Java虛擬機堆棧,則Java虛擬機機器拋出OutOfMemoryError

對於Heap ,第3.5.3節。

  • 如果計算需要的堆數超過自動存儲管理系統可用的堆,則Java虛擬機將拋出OutOfMemoryError

因此,它在分配對象之前預先進行計算。


會發生什么是JVM嘗試為名為Permanent Generation region(或PermSpace)的內存中的對象分配內存。 如果分配失敗(即使在JVM調用垃圾收集器嘗試分配可用空間之后),它OutOfMemoryError拋出OutOfMemoryError 即使異常也需要內存空間,因此錯誤將無限期地拋出。

進一步閱讀。 此外, OutOfMemoryError可以在不同的JVM結構中發生

格雷厄姆·博蘭德似乎是對的 :至少我的 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

順便說一句,我正在運行的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

大多數運行時環境將在啟動時預分配,或以其他方式保留足夠的內存來處理內存不足情況。 我想大多數理智的JVM實現都會這樣做。

上次我使用Java並使用調試器時,堆檢查器顯示JVM在啟動時分配了一個OutOfMemoryError實例。 換句話說,它在程序有機會開始消耗之前分配對象,更不用說耗盡內存了。

從JVM規范,第3.5.2節:

如果可以動態擴展Java虛擬機堆棧,並且嘗試進行擴展但是可以使內存不足以實現擴展,或者如果可用的內存不足以為新線程創建初始Java虛擬機堆棧, 則Java虛擬機機器拋出OutOfMemoryError

每個Java虛擬機都必須保證它會拋出OutOfMemoryError 這意味着,即使沒有剩余堆空間,它也必須能夠創建OutOfMemoryError的實例(或者預先創建)。

雖然它沒有必要保證,但是有足夠的內存來捕獲它並打印出漂亮的堆棧跟蹤......

加成

您添加了一些代碼來表明,如果必須拋出多個OutOfMemoryError ,JVM可能會耗盡堆空間。 但是這樣的實施會違反上述要求。

不要求拋出的OutOfMemoryError實例是唯一的或按需創建的。 JVM可以在啟動期間准備一個OutOfMemoryError實例,並在堆空間用完時拋出它 - 這在正常環境中是一次。 換句話說:我們看到的OutOfMemoryError實例可能是一個單例。

有趣的問題:-)。 雖然其他人對理論方面給出了很好的解釋,但我決定嘗試一下。 這是在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實例,但是如果推送,它將只重用相同的實例而不是沒有任何東西可以拋出。

產量

注意:我截斷了一些堆棧跟蹤以使輸出更易於閱讀(“[...]”)。

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

    }
}

我很確定,JVM將確保它至少有足夠的內存來在內存耗盡之前拋出異常。

您似乎混淆了JVM保留的虛擬內存,其中JVM運行Java程序與主機操作系統的本機內存,其中JVM作為本機進程運行。 計算機上的JVM在由OS管理的內存中運行,而不是在JVM保留用於運行Java程序的內存中運行。

進一步閱讀:

作為最后一點,嘗試捕獲 java.lang.Error (及其后代類)以打印堆棧跟蹤可能不會為您提供任何有用的信息。 您想要堆轉儲。

指示試圖違反托管存儲器環境邊界的異常由所述環境的運行時處理,在這種情況下是JVM。 JVM是它自己的進程,它運行你的應用程序的IL。 如果程序嘗試進​​行調用以擴展調用堆棧超出限制,或者分配比JVM可以保留的內存更多的內存,則運行時本身將注入異常,這將導致調用堆棧被展開。 無論程序當前需要多少內存,或者調用堆棧的深度如何,JVM都會在其自己的進程邊界內分配足夠的內存來創建所述異常並將其注入到代碼中。

為了進一步澄清@Graham Borland的答案,功能上,JVM在啟動時執行此操作:

private static final OutOfMemoryError OOME = new OutOfMemoryError();

稍后,JVM執行以下Java字節碼之一:'new','anewarray'或'multianewarray'。 此指令使JVM在內存不足的情況下執行許多步驟:

  1. 調用native函數,比如allocate() allocate()嘗試為某個特定類或數組的新實例分配內存。
  2. 該分配請求失敗,因此JVM調用另一個本機函數,比如doGC() ,它試圖進行垃圾收集。
  3. 當該函數返回時, allocate()嘗試再次為該實例分配內存。
  4. 如果失敗(*),那么在allocate()中的JVM只會throw OOME; ,指的是它在啟動時實例化的OOME。 請注意,它不必分配OOME,它只是引用它。

顯然,這些不是文字步驟; 它們在實現中會從JVM變為JVM,但這是高層次的想法。

(*)在失敗之前,這里發生了大量的工作。 JVM將嘗試清除SoftReference對象,在使用分代收集器時嘗試直接分配到tenured generation,以及其他可能的東西,例如finalization。

說JVM將預分配OutOfMemoryErrors的答案確實是正確的。
除了通過激發內存不足情況進行測試外,我們還可以檢查任何JVM的堆(我使用了一個只是睡眠的小程序,使用Oracle的Hotspot JVM從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的代碼,看起來像這樣(省略了一些部分):

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM