JVM源碼分析之臨門一腳的OutOfMemoryError完全解讀

概述
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之。
最後更新:2017-04-11 19:32:01