螞蟻金服寒泉子:JVM原始碼分析之臨門一腳的OutOfMemoryError完全解讀

聽雲APM發表於2016-10-10

原文出自【聽雲技術部落格】:http://blog.tingyun.com/web/article/detail/1210

概述

OutOfMemoryError,說的是java.lang.OutOfMemoryError,是JDK裡自帶的異常,顧名思義,說的就是記憶體溢位,當我們的系統記憶體嚴重不足的時候就會丟擲這個異常(PS:注意這是一個Error,不是一個Exception,所以當我們要catch異常的時候要注意哦),這個異常說常見也常見,說不常見其實也見得不多,不過作為Java程式設計師至少應該都聽過吧,如果你對jvm不是很熟,或者對OutOfMemoryError這個異常瞭解不是很深的話,這篇文章肯定還是可以給你帶來一些驚喜的,通過這篇文章你至少可以瞭解到如下幾點:

OutOfMemoryError一定會被載入嗎

什麼時候丟擲OutOfMemoryError

會建立無數OutOfMemoryError例項嗎

為什麼大部分OutOfMemoryError異常是無堆疊的

我們如何去分析這樣的異常

OutOfMemoryError類載入

既然要說OutOfMemoryError,那就得從這個類的載入說起來,那這個類什麼時候被載入呢?你或許會不假思索地說,根據java類的延遲載入機制,這個類一般情況下不會被載入,除非當我們丟擲OutOfMemoryError這個異常的時候才會第一次被載入,如果我們的系統一直不丟擲這個異常,那這個類將一直不會被載入。說起來好像挺對,不過我這裡首先要糾正這個說法,要明確的告訴你這個類在jvm啟動的時候就已經被載入了,不信你就執行java -verbose:class -version列印JDK版本看看,看是否有OutOfMemoryError這個類被載入,再輸出裡你將能找到下面的內容:

```
[Loaded java.lang.OutOfMemoryError from /Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar]
```

這意味著這個類其實在vm啟動的時候就已經被載入了,那JVM裡到底在哪裡進行載入的呢,且看下面的方法:

```
bool universe_post_init() {
...
// 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);
    Universe::_out_of_memory_error_realloc_objects = 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("Java heap space: failed reallocation of scalar replaced objects", CHECK_false);
    java_lang_Throwable::set_message(Universe::_out_of_memory_error_realloc_objects, 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;
  }
}
```

上面的程式碼其實就是在vm啟動過程中載入了OutOfMemoryError這個類,並且建立了好幾個OutOfMemoryError物件,每個OutOfMemoryError物件代表了一種記憶體溢位的場景,比如說Java heap space不足導致的OutOfMemoryError,抑或Metaspace不足導致的OutOfMemoryError,上面的程式碼來源於JDK8,所以能看到metaspace的內容,如果是JDK8之前,你將看到Perm的OutOfMemoryError,不過本文metaspace不是重點,所以不展開討論,如果大家有興趣,可以專門寫一篇文章來介紹metsapce來龍去脈,說來這個坑填起來還挺大的。

能通過agent攔截到這個類載入嗎

熟悉位元組碼增強的人,可能會條件反射地想到是否可以攔截到這個類的載入呢,這樣我們就可以做一些譬如記憶體溢位的監控啥的,哈哈,我要告訴你的是NO WAY,因為通過agent的方式來監聽類載入過程是在vm初始化完成之後才開始的,而這個類的載入是在vm初始化過程中,因此不可能攔截到這個類的載入,於此類似的還有java.lang.Object,java.lang.Class等。

為什麼要在vm啟動過程中載入這個類

這個問題或許看了後面的內容你會有所體會,先賣個關子。包括為什麼要預先建立這幾個例項物件後面也會解釋。

何時丟擲OutOfMemoryError

要丟擲OutOfMemoryError,那肯定是有地方需要進行記憶體分配,可能是heap裡,也可能是metsapce裡(如果是在JDK8之前的會是Perm裡),不同地方的分配,其策略也不一樣,簡單來說就是嘗試分配,實在沒辦法就gc,gc還是不能分配就丟擲異常。

不過還是以Heap裡的分配為例說一下具體的過程:

正確情況下物件建立需要分配的記憶體是來自於Heap的Eden區域裡,當Eden記憶體不夠用的時候,某些情況下會嘗試到Old裡進行分配(比如說要分配的記憶體很大),如果還是沒有分配成功,於是會觸發一次ygc的動作,而ygc完成之後我們會再次嘗試分配,如果仍不足以分配此時的記憶體,那會接著做一次full gc(不過此時的soft reference不會被強制回收),將老生代也回收一下,接著再做一次分配,仍然不夠分配那會做一次強制將soft reference也回收的full gc,如果還是不能分配,那這個時候就不得不丟擲OutOfMemoryError了。這就是Heap裡分配記憶體丟擲OutOfMemoryError的具體過程了。

OutOfMemoryError物件可能會很多嗎

想象有這麼一種場景,我們的程式碼寫得足夠爛,並且存在記憶體洩漏,這意味著系統跑到一定程度之後,只要我們建立物件要分配記憶體的時候就會進行gc,但是gc沒啥效果,進而丟擲OutOfMemoryError的異常,那意味著每發生此類情況就應該建立一個OutOfMemoryError物件,並且丟擲來,也就是說我們會看到一個帶有堆疊的OutOfMemoryError異常被丟擲,那事實是如此嗎?如果真是如此,那為什麼在VM啟動的時候會建立那幾個OutOfMemoryError物件呢?

丟擲異常的java程式碼位置需要我們關心嗎

這個問題或許你仔細想想就清楚了,如果沒想清楚,請在這裡停留一分鐘仔細想想再往後面看。

丟擲OutOfMemoryError異常的java方法其實只是臨門一腳而已,導致記憶體洩漏的不一定就是這個方法,當然也不排除可能是這個方法,不過這種情況的可能性真的非常小。所以你大可不必去關心丟擲這個異常的堆疊。

既然可以不關心其異常堆疊,那意味著這個異常其實沒必要每次都建立一個不一樣的了,因為不需要堆疊的話,其他的東西都可以完全相同,這樣一來回到我們前面提到的那個問題,為什麼要在vm啟動過程中載入這個類,或許你已經有答案了,在vm啟動過程中我們把類載入起來,並建立幾個沒有堆疊的物件快取起來,只需要設定下不同的提示資訊即可,當需要丟擲特定型別的OutOfMemoryError異常的時候,就直接拿出快取裡的這幾個物件就可以了。

所以OutOfMemoryError的物件其實並不會太多,哪怕你程式碼寫得再爛,當然,如果你程式碼裡要不斷new OutOfMemoryError(),那我就無話可說啦。

為什麼我們有時候還是可以看到有堆疊的OutOfMemoryError

如果都是用jvm啟動的時候建立的那幾個OutOfMemoryError物件,那不應該再出現有堆疊的OutOfMemoryError異常,但是實際上我們偶爾還是能看到有堆疊的異常,如果你細心點的話,可能會總結出一個規律,發現最多出現4次有堆疊的OutOfMemoryError異常,當4次過後,你都將看到無堆疊的OutOfMemoryError異常。

這個其實在我們上面貼的程式碼裡也有體現,最後有一個for迴圈,這個迴圈裡會建立幾個OutOfMemoryError物件,如果我們將StackTraceInThrowable設定為true的話(預設就是true的),意味著我們丟擲來的異常正確情況下都將是有堆疊的,那根據PreallocatedOutOfMemoryErrorCount這個引數來決定預先建立幾個OutOfMemoryError異常物件,但是這個引數除非在debug版本下可以被設定之外,正常release出來的版本其實是無法設定這個引數的,它會是一個常量,值為4,因此在jvm啟動的時候會預先建立4個OutOfMemoryError異常物件,但是這幾個異常物件的堆疊,是可以動態設定的,比如說某個地方要丟擲OutOfMemoryError異常了,於是先從預存的OutOfMemoryError裡取出一個(其他是預存的物件還有),將此時的堆疊填上,然後丟擲來,並且這個物件的使用是一次性的,也就是這個物件被丟擲之後將不會再次被利用,直到預設的這幾個OutOfMemoryError物件被用完了,那接下來丟擲的異常都將是一開始快取的那幾個無棧的OutOfMemoryError物件。

這就是我們看到的最多出現4次有堆疊的OutOfMemoryError異常及大部分情況下都將看到沒有堆疊的OutOfMemoryError物件的原因。

如何分析OutOfMemoryError異常

既然看堆疊也沒什麼意義,那隻能從提示上入手了,我們看到這類異常,首先要確定的到底是哪塊記憶體何種情況導致的記憶體溢位,比如說是Perm導致的,那丟擲來的異常資訊裡會帶有Perm的關鍵資訊,那我們應該重點看Perm的大小,以及Perm裡的內容;如果是Heap的,那我們就必須做記憶體Dump,然後分析為什麼會發生這樣的情況,記憶體裡到底存了什麼物件,至於記憶體分析的最佳的分析工具自然是MAT啦,不瞭解的請google之。

相關文章