天天看點

螞蟻金服寒泉子: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這個類被加載,再輸出裡你将能找到下面的内容:

1 2 3 <code>```</code> <code>[loaded java.lang.outofmemoryerror from /library/java/javavirtualmachines/jdk1.7.0_79.jdk/contents/home/jre/lib/rt.jar]</code>

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

4 5 6 7 8 9 10 11 12 13 14 15 16 <code>bool universe_post_init() {</code> <code>...</code> <code>// setup preallocated outofmemoryerror errors</code> <code>    </code><code>k = systemdictionary::resolve_or_fail(vmsymbols::java_lang_outofmemoryerror(), true, check_false);</code> <code>    </code><code>k_h = instanceklasshandle(thread, k);</code> <code>    </code><code>universe::_out_of_memory_error_java_heap = k_h-&gt;allocate_instance(check_false);</code> <code>    </code><code>universe::_out_of_memory_error_metaspace = k_h-&gt;allocate_instance(check_false);</code> <code>    </code><code>universe::_out_of_memory_error_class_metaspace = k_h-&gt;allocate_instance(check_false);</code> <code>    </code><code>universe::_out_of_memory_error_array_size = k_h-&gt;allocate_instance(check_false);</code> <code>    </code><code>universe::_out_of_memory_error_gc_overhead_limit =</code> <code>      </code><code>k_h-&gt;allocate_instance(check_false);</code> <code>    </code><code>universe::_out_of_memory_error_realloc_objects = k_h-&gt;allocate_instance(check_false);</code> <code>    </code>  17 18 19 20 21 <code>if (!dumpsharedspaces) {</code> <code>    </code><code>// these are the only java fields that are currently set during shared space dumping.</code> <code>    </code><code>// we prefer to not handle this generally, so we always reinitialize these detail messages.</code> <code>    </code><code>handle msg = java_lang_string::create_from_str("java heap space", check_false);</code> <code>    </code><code>java_lang_throwable::set_message(universe::_out_of_memory_error_java_heap, msg());</code> <code>    </code><code>msg = java_lang_string::create_from_str("metaspace", check_false);</code> <code>    </code><code>java_lang_throwable::set_message(universe::_out_of_memory_error_metaspace, msg());</code> <code>    </code><code>msg = java_lang_string::create_from_str("compressed class space", check_false);</code> <code>    </code><code>java_lang_throwable::set_message(universe::_out_of_memory_error_class_metaspace, msg());</code> <code>    </code><code>msg = java_lang_string::create_from_str("requested array size exceeds vm limit", check_false);</code> <code>    </code><code>java_lang_throwable::set_message(universe::_out_of_memory_error_array_size, msg());</code> <code>    </code><code>msg = java_lang_string::create_from_str("gc overhead limit exceeded", check_false);</code> <code>    </code><code>java_lang_throwable::set_message(universe::_out_of_memory_error_gc_overhead_limit, msg());</code> <code>    </code><code>msg = java_lang_string::create_from_str("java heap space: failed reallocation of scalar replaced objects", check_false);</code> <code>    </code><code>java_lang_throwable::set_message(universe::_out_of_memory_error_realloc_objects, msg());</code> <code>    </code><code>msg = java_lang_string::create_from_str("/ by zero", check_false);</code> <code>    </code><code>java_lang_throwable::set_message(universe::_arithmetic_exception_instance, msg());</code> <code>    </code><code>// setup the array of errors that have preallocated backtrace</code> <code>    </code><code>k = universe::_out_of_memory_error_java_heap-&gt;klass();</code> <code>    </code><code>assert(k-&gt;name() == vmsymbols::java_lang_outofmemoryerror(), "should be out of memory error");</code> <code>int len = (stacktraceinthrowable) ? (int)preallocatedoutofmemoryerrorcount : 0;</code> <code>    </code><code>universe::_preallocated_out_of_memory_error_array = oopfactory::new_objarray(k_h(), len, check_false);</code> <code>    </code><code>for (int i=0; i&lt;len; i++) {</code> <code>      </code><code>oop err = k_h-&gt;allocate_instance(check_false);</code> <code>      </code><code>handle err_h = handle(thread, err);</code> <code>      </code><code>java_lang_throwable::allocate_backtrace(err_h, check_false);</code> <code>      </code><code>universe::preallocated_out_of_memory_errors()-&gt;obj_at_put(i, err_h());</code> <code>    </code><code>}</code> <code>    </code><code>universe::_preallocated_out_of_memory_error_avail_count = (jint)len;</code> <code>  </code><code>}</code> <code>}</code>

上面的代碼其實就是在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之。

 【寒泉子】目前在阿裡從事jvm相關工作,為各業務系統做性能優化,性能問題分析,之前主要從事支付寶架構容器的開發