JVM的OOM分為多種情況,下面會針對JAVA.lang.OutOfMemoryError: Java heap space這種情況講解一下發(fā)生的原因與解決方案。
在JAVA應(yīng)用啟動時,會限制應(yīng)用的使用空間。也就說,任何一個JAVA應(yīng)用,都只能使用有限的內(nèi)存空間。
JAVA的內(nèi)存空間在JDK7及以前劃分為堆與永久代。在JDK8之后移除了永久代,采用元空間來代替。
在啟動時,通過指定JVM參數(shù):`-Xmx` 來設(shè)置可使用的最大堆大小。如果沒有顯式的設(shè)置,則系統(tǒng)上默認(rèn)為物理內(nèi)存的1/4(根據(jù)物理內(nèi)存的不同情況有不同的分配規(guī)則。但是普遍可以認(rèn)為是1/4)。
發(fā)生java.lang.OutOfMemoryError: Java heap space異常時,代表著應(yīng)用嘗試從堆上申請一個區(qū)域時,堆沒有可配的空間。(注:可能有可使用的物理內(nèi)存,但是沒有已經(jīng)達(dá)到了JAVA應(yīng)用可分配的內(nèi)存大小)
JVM是很智能的,在即將發(fā)生OOM時,會進(jìn)行一次FullGC以回收可回收的對象來釋放空間。如果FullGC之后還是沒有可滿足大小的空間分配,才拋出java.lang.OutOfMemoryError: Java heap space。
java.lang.OutOfMemoryError: Java heap space正常是怎么發(fā)生的呢?
- 突發(fā)高峰期:程序在正常的用戶量和一定數(shù)據(jù)量時運(yùn)行正常。但是,在某個高峰時導(dǎo)致超出預(yù)期閾值,內(nèi)存存活對象使用空間的量超出最大堆,并且無法回收。
- 內(nèi)存泄露: 由于編程錯誤導(dǎo)致應(yīng)用程序不再需要的對象(數(shù)據(jù))一直被持有引用,導(dǎo)致無法被回收。隨著時間的推移,泄露的內(nèi)存對象占用了所有的可用堆空間。
分配合理的足夠內(nèi)存
最簡單的解決方法就給JVM分配足夠大的內(nèi)存來滿足運(yùn)行程序的需求。
但是,需要注意在內(nèi)存泄漏的情況下,分配再大的內(nèi)存也只是推遲了java.lang.OutOfMemoryError: Java heap space的發(fā)生。
而且,加大了JVM堆內(nèi)存,也會增加在GC時的暫停時間(STW),影響程序的吞吐量,增加延遲。
如何分配一個合理的內(nèi)存空間,是需要針對GC進(jìn)行優(yōu)化的。也就是常說的JVM調(diào)優(yōu)。
JVM調(diào)優(yōu)可以參考:「JVM」GC——調(diào)優(yōu)介紹
那么,如何調(diào)整通過分配JAVA堆空間來解決問題呢?
首先,需要了解以下這些問題:
- 哪些對象占用了大量的堆空間
- 在哪些代碼中創(chuàng)建了這些對象
上述的問題可以通過JVM自身的jmap來dump出運(yùn)行時的堆棧信息。然后通過如:MAT,JProfiler,jconsole等空間來進(jìn)行內(nèi)存對象占用的跟蹤。
MAT使用可以參考:[JVM] MAT進(jìn)階使用
當(dāng)然,這種方式是比較原始的方式。建議通過如:Plumbr等JVM監(jiān)控工具來跟蹤問題。

Plumbr的報告信息
以上圖的監(jiān)控舉例簡要說明一下如何適當(dāng)?shù)倪M(jìn)行堆空間的大小分配。
上圖所示中,可以得到如下信息:
- 所有相關(guān)對象的整個GCRoot引用
- 內(nèi)存消耗最多的對象:

- 這些對象在代碼中的分配位置:

根據(jù)上述的信息, 我們可以得到這樣的猜想:
這個程序的需要的運(yùn)行空間超過248MB,并且是無法在一定時間內(nèi)釋放被回收。那么,按JVM調(diào)優(yōu)的思路,建議分配的最大堆大小為:老年代活躍數(shù)據(jù)大小 * 3~4倍。
所以,我們第一次調(diào)整時,可以分配:248 * 4 = 992。
由于堆大小的無法確認(rèn),所以第一次調(diào)整直接調(diào)整為:-Xmx1024m。
單位:
-Xmx1024 即配置1024b = 1kb
-Xmx1024k 即配置1mb
-Xmx1024m 即配置1gb
-Xmx1g 即配置1gb
建議:
在配置-Xmx時,應(yīng)該將-Xms也配置成相同大小。避免JVM需要動態(tài)調(diào)整堆空間大小帶來的性能影響。