螞蟻金服寒泉子:JVM原始碼分析之臨門一腳的OutOfMemoryError完全解讀
原文出自【聽雲技術部落格】: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之。
相關文章
- JVM原始碼分析之javaagent原理完全解讀JVM原始碼Java
- [從原始碼學設計]螞蟻金服SOFARegistry 之 ChangeNotifier原始碼
- [從原始碼學設計]螞蟻金服SOFARegistry之配置資訊原始碼
- [從原始碼學設計]螞蟻金服SOFARegistry之服務上線原始碼
- JVM原始碼分析之Attach機制實現完全解讀JVM原始碼
- JVM原始碼分析之堆外記憶體完全解讀JVM原始碼記憶體
- 螞蟻金服面試經歷-臨場發揮面試
- [從原始碼學設計]螞蟻金服SOFARegistry之推拉模型原始碼模型
- [從原始碼學設計]螞蟻金服SOFARegistry之延遲操作原始碼
- JVM原始碼分析之Object.wait/notify(All)完全解讀JVM原始碼ObjectAI
- [從原始碼學設計]螞蟻金服SOFARegistry之時間輪的使用原始碼
- 螞蟻金服RPC框架結構分析RPC框架
- (螞蟻金服mPaaS)統一儲存
- [從原始碼學設計]螞蟻金服SOFARegistry 之 LocalDataServerChangeEvent及資料同步原始碼LDAServer
- [從原始碼學設計]螞蟻金服SOFARegistry之續約和驅逐原始碼
- [從原始碼學設計]螞蟻金服SOFARegistry之訊息匯流排原始碼
- [從原始碼學設計]螞蟻金服SOFARegistry之Data節點變更原始碼
- 【工作】螞蟻金服招DBA
- [從原始碼學設計]螞蟻金服SOFARegistry 之 如何與Meta Server互動原始碼Server
- [從原始碼學設計]螞蟻金服SOFARegistry網路操作之連線管理原始碼
- 原始碼剖析 | 螞蟻金服 mPaaS 框架下的 RPC 呼叫歷程原始碼框架RPC
- 螞蟻金服通訊框架SOFABolt解析|編解碼機制框架
- 9.9螞蟻金服二三輪面試面試
- 螞蟻金服楊軍:螞蟻資料分析平臺的演進及資料分析方法的應用
- 螞蟻金服 Service Mesh 深度實踐
- 螞蟻金服 Service Mesh 實踐探索
- 螞蟻金服技術長--程立
- 重磅!螞蟻金服開源機器學習工具SQLFlow,技術架構獨家解讀機器學習SQL架構
- 阿里同意入股螞蟻金服33%股權 為螞蟻上市鋪路阿里
- [從原始碼學設計]螞蟻金服SOFARegistry之訊息匯流排非同步處理原始碼非同步
- 螞蟻金服 DB Mesh 的探索與實踐
- 一文讀懂螞蟻金服自研技術的發展和實踐
- 螞蟻金服分散式鏈路跟蹤元件取樣策略和原始碼 | 剖析分散式元件原始碼
- 螞蟻金服!前端實習生!內推!提前批!前端
- 螞蟻金服面試經歷-前期準備面試
- 招聘貼:螞蟻金服招Java研發Java
- 招聘貼:螞蟻金服招前端開發前端
- 【北京】Golang技術專家--螞蟻金服Golang