天天看點

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

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

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

outofmemoryerror一定會被加載嗎

什麼時候抛出outofmemoryerror

會建立無數outofmemoryerror執行個體嗎

為什麼大部分outofmemoryerror異常是無堆棧的

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

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

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

上面的代碼其實就是在vm啟動過程中加載了outofmemoryerror這個類,并且建立了好幾個outofmemoryerror對象,每個outofmemoryerror對象代表了一種記憶體溢出的場景,比如說<code>java heap space</code>不足導緻的outofmemoryerror,抑或<code>metaspace</code>不足導緻的outofmemoryerror,上面的代碼來源于jdk8,是以能看到metaspace的内容,如果是jdk8之前,你将看到perm的outofmemoryerror,不過本文metaspace不是重點,是以不展開讨論,如果大家有興趣,可以專門寫一篇文章來介紹metsapce來龍去脈,說來這個坑填起來還挺大的。

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

這個問題或許看了後面的内容你會有所體會,先賣個關子。包括為什麼要預先建立這幾個執行個體對象後面也會解釋。

要抛出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的具體過程了。

想象有這麼一種場景,我們的代碼寫得足夠爛,并且存在記憶體洩漏,這意味着系統跑到一定程度之後,隻要我們建立對象要配置設定記憶體的時候就會進行gc,但是gc沒啥效果,進而抛出outofmemoryerror的異常,那意味着每發生此類情況就應該建立一個outofmemoryerror對象,并且抛出來,也就是說我們會看到一個帶有堆棧的outofmemoryerror異常被抛出,那事實是如此嗎?如果真是如此,那為什麼在vm啟動的時候會建立那幾個outofmemoryerror對象呢?

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

抛出outofmemoryerror異常的java方法其實隻是臨門一腳而已,導緻記憶體洩漏的不一定就是這個方法,當然也不排除可能是這個方法,不過這種情況的可能性真的非常小。是以你大可不必去關心抛出這個異常的堆棧。

既然可以不關心其異常堆棧,那意味着這個異常其實沒必要每次都建立一個不一樣的了,因為不需要堆棧的話,其他的東西都可以完全相同,這樣一來回到我們前面提到的那個問題,<code>為什麼要在vm啟動過程中加載這個類</code>,或許你已經有答案了,在vm啟動過程中我們把類加載起來,并建立幾個沒有堆棧的對象緩存起來,隻需要設定下不同的提示資訊即可,當需要抛出特定類型的outofmemoryerror異常的時候,就直接拿出緩存裡的這幾個對象就可以了。

是以outofmemoryerror的對象其實并不會太多,哪怕你代碼寫得再爛,當然,如果你代碼裡要不斷<code>new outofmemoryerror()</code>,那我就無話可說啦。

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

這個其實在我們上面貼的代碼裡也有展現,最後有一個for循環,這個循環裡會建立幾個outofmemoryerror對象,如果我們将<code>stacktraceinthrowable</code>設定為true的話(預設就是true的),意味着我們抛出來的異常正确情況下都将是有堆棧的,那根據<code>preallocatedoutofmemoryerrorcount</code>這個參數來決定預先建立幾個outofmemoryerror異常對象,但是這個參數除非在debug版本下可以被設定之外,正常release出來的版本其實是無法設定這個參數的,它會是一個常量,值為4,是以在jvm啟動的時候會預先建立4個outofmemoryerror異常對象,但是這幾個異常對象的堆棧,是可以動态設定的,比如說某個地方要抛出outofmemoryerror異常了,于是先從預存的outofmemoryerror裡取出一個(其他是預存的對象還有),将此時的堆棧填上,然後抛出來,并且這個對象的使用是一次性的,也就是這個對象被抛出之後将不會再次被利用,直到預設的這幾個outofmemoryerror對象被用完了,那接下來抛出的異常都将是一開始緩存的那幾個無棧的outofmemoryerror對象。

這就是我們看到的最多出現4次有堆棧的outofmemoryerror異常及大部分情況下都将看到沒有堆棧的outofmemoryerror對象的原因。

既然看堆棧也沒什麼意義,那隻能從提示上入手了,我們看到這類異常,首先要确定的到底是哪塊記憶體何種情況導緻的記憶體溢出,比如說是perm導緻的,那抛出來的異常資訊裡會帶有<code>perm</code>的關鍵資訊,那我們應該重點看perm的大小,以及perm裡的内容;如果是heap的,那我們就必須做記憶體dump,然後分析為什麼會發生這樣的情況,記憶體裡到底存了什麼對象,至于記憶體分析的最佳的分析工具自然是mat啦,不了解的請google之。