螞蟻金服寒泉子:JVM源碼分析之臨門一腳的OutOfMemoryError徹底解讀

原文出自【聽雲技術博客】:http://blog.tingyun.com/web/a...java

概述程序員

OutOfMemoryError,說的是java.lang.OutOfMemoryError,是JDK裏自帶的異常,顧名思義,說的就是內存溢出,當咱們的系統內存嚴重不足的時候就會拋出這個異常(PS:注意這是一個Error,不是一個Exception,因此當咱們要catch異常的時候要注意哦),這個異常說常見也常見,說不常見其實也見得很少,不過做爲Java程序員至少應該都聽過吧,若是你對jvm不是很熟,或者對OutOfMemoryError這個異常瞭解不是很深的話,這篇文章確定仍是能夠給你帶來一些驚喜的,經過這篇文章你至少能夠了解到以下幾點:web

OutOfMemoryError必定會被加載嗎緩存

何時拋出OutOfMemoryError性能優化

會建立無數OutOfMemoryError實例嗎框架

爲何大部分OutOfMemoryError異常是無堆棧的jvm

咱們如何去分析這樣的異常工具

OutOfMemoryError類加載oop

既然要說OutOfMemoryError,那就得從這個類的加載提及來,那這個類何時被加載呢?你或許會不假思索地說,根據java類的延遲加載機制,這個類通常狀況下不會被加載,除非當咱們拋出OutOfMemoryError這個異常的時候纔會第一次被加載,若是咱們的系統一直不拋出這個異常,那這個類將一直不會被加載。提及來好像挺對,不過我這裏首先要糾正這個說法,要明確的告訴你這個類在jvm啓動的時候就已經被加載了,不信你就執行java -verbose:class -version打印JDK版本看看,看是否有OutOfMemoryError這個類被加載,再輸出裏你將能找到下面的內容:post

[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之。———————————————【寒泉子】目前在阿里從事JVM相關工做,爲各業務系統作性能優化,性能問題分析,以前主要從事支付寶框架容器的開發

相關文章
相關標籤/搜索