JVM優(yōu)化:實(shí)戰(zhàn)OutOfMemoryError異常
一、Java堆溢出
堆內(nèi)存中主要存放對(duì)象、數(shù)組等,只要不斷地創(chuàng)建這些對(duì)象,并且保證 GC Roots 到對(duì)象之間有可達(dá)路徑來(lái)避免垃 圾收集回收機(jī)制清除這些對(duì)象,當(dāng)這些對(duì)象所占空間超過(guò)最大堆容量時(shí),就會(huì)產(chǎn)生 OutOfMemoryError 的異常。堆 內(nèi)存異常示例如下:
運(yùn)行后會(huì)報(bào)異常,在堆棧信息中可以看到
java.lang.OutOfMemoryError: Java heap space 的信息,說(shuō)明在堆內(nèi)存空間產(chǎn)生內(nèi)存溢出的異常。
新產(chǎn)生的對(duì)象最初分配在新生代,新生代滿后會(huì)進(jìn)行一次 Minor GC ,如果 Minor GC 后空間不足會(huì)把該對(duì)象和 新生代滿足條件的對(duì)象放入老年代,老年代空間不足時(shí)會(huì)進(jìn)行 Full GC ,之后如果空間還不足以存放新對(duì)象則拋 出 OutOfMemoryError 異常。
常見(jiàn)原因:
- 內(nèi)存中加載的數(shù)據(jù)過(guò)多,如一次從數(shù)據(jù)庫(kù)中取出過(guò)多數(shù)據(jù);
- 集合對(duì)對(duì)象引用過(guò)多且使用完后沒(méi)有清空;
- 代碼中存在死循環(huán)或循環(huán)產(chǎn)生過(guò)多重復(fù)對(duì)象;
- 堆內(nèi)存分配不合理
二、虛擬機(jī)棧和本地方法棧溢出
由于HotSpot虛擬機(jī)中并不區(qū)分虛擬機(jī)棧和本地方法棧, 因此對(duì)于HotSpot來(lái)說(shuō), -Xoss參數(shù)(設(shè)置本地方法棧大 小) 雖然存在, 但實(shí)際上是沒(méi)有任何效果的, 棧容量只能由-Xss參數(shù)來(lái)設(shè)定。 關(guān)于虛擬機(jī)棧和本地方法棧, 在 《Java虛擬機(jī)規(guī)范》 中描述了兩種異常:
1) 如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度, 將拋出StackOverflowError異常。
2) 如果虛擬機(jī)的棧內(nèi)存允許動(dòng)態(tài)擴(kuò)展, 當(dāng)擴(kuò)展棧容量無(wú)法申請(qǐng)到足夠的內(nèi)存時(shí), 將拋出 OutOfMemoryError異 常。
《Java虛擬機(jī)規(guī)范》 明確允許Java虛擬機(jī)實(shí)現(xiàn)自行選擇是否支持棧的動(dòng)態(tài)擴(kuò)展, 而HotSpot虛擬機(jī)的選擇是不支持 擴(kuò)展, 所以除非在創(chuàng)建線程申請(qǐng)內(nèi)存時(shí)就因無(wú)法獲得足夠內(nèi)存而出現(xiàn) OutOfMemoryError異常, 否則在線程運(yùn)行時(shí) 是不會(huì)因?yàn)閿U(kuò)展而導(dǎo)致內(nèi)存溢出的, 只會(huì)因?yàn)闂H萘繜o(wú)法容納新的棧幀而導(dǎo)致StackOverflowError異常。
為了驗(yàn)證 這點(diǎn), 我們可以做兩個(gè)實(shí)驗(yàn), 先將實(shí)驗(yàn)范圍限制在單線程中操作, 嘗試下面兩種行為是 否能讓HotSpot虛擬機(jī)產(chǎn) 生OutOfMemoryError異常: 使用-Xss參數(shù)減少棧內(nèi)存容量。 結(jié)果: 拋出StackOverflowError異常, 異常出現(xiàn)時(shí)輸出 的堆棧深度相應(yīng)縮小。 定義了大量的本地變量, 增大此方法幀中本地變量表的長(zhǎng)度。 結(jié)果: 拋出 StackOverflowError異常, 異常出現(xiàn)時(shí)輸出的堆棧深度相應(yīng)縮小。
三、 運(yùn)行時(shí)常量池和方法區(qū)溢出
由于運(yùn)行時(shí)常量池是方法區(qū)的一部分, 所以這兩個(gè)區(qū)域的溢出測(cè)試可以放到一起進(jìn)行。前面曾經(jīng)提到HotSpot從 JDK 7開(kāi)始逐步“去永久代”的計(jì)劃, 并在JDK 8中完全使用元空間來(lái)代替永久代的背景故事, 在此我們就以測(cè)試代碼 來(lái)觀察一下, 使用“永久代”還是“元空間”來(lái)實(shí)現(xiàn)方法區(qū), 對(duì)程序有什么 實(shí)際的影響。
String::intern()是一個(gè)本地方法, 它的作用是如果字符串常量池中已經(jīng)包含一個(gè)等于此String對(duì)象的 字符串, 則返 回代表池中這個(gè)字符串的String對(duì)象的引用; 否則, 會(huì)將此String對(duì)象包含的字符串添加到常量池中, 并且返回此 String對(duì)象的引用。 在JDK 6或更早之前的HotSpot虛擬機(jī)中, 常量池都是分配在永久代中, 我們可以通過(guò)-XX: PermSize和-XX: MaxPermSize限制永久代的大小, 即可間接限制其中常量池的容量。
方法區(qū)內(nèi)存溢出
方法區(qū)的其他部分的內(nèi)容, 方法區(qū)的主要職責(zé)是用于存放類(lèi)型的相關(guān)信息, 如類(lèi)名、 訪問(wèn)修飾符、 常量池、 字段 描述、 方法描述等。 對(duì)于這部分區(qū)域的測(cè)試, 基本的思路是運(yùn)行時(shí)產(chǎn)生大量的類(lèi)去填滿方法區(qū), 直到溢出為止。
四、直接內(nèi)存溢出
直接內(nèi)存(Direct Memory) 的容量大小可通過(guò)-XX: MaxDirectMemorySize參數(shù)來(lái)指定, 如果不去指定, 則默認(rèn)與 Java堆最大值(由-Xmx指定) 一致, 越過(guò)了DirectByteBuer類(lèi)直接通 過(guò)反射獲取Unsafe實(shí)例進(jìn)行內(nèi)存分配 (Unsafe類(lèi)的getUnsafe()方法指定只有引導(dǎo)類(lèi)加載器才會(huì)返回實(shí)例, 體現(xiàn)了設(shè)計(jì)者希望只有虛擬機(jī)標(biāo)準(zhǔn)類(lèi)庫(kù)里面的 類(lèi)才能使用Unsafe的功能,在JDK 10時(shí)才將Unsafe 的部分功能通過(guò)VarHandle開(kāi)放給外部使用) ,
因?yàn)殡m然使用 DirectByteBuer分配內(nèi)存也會(huì)拋出內(nèi)存溢出異常, 但它拋出異常時(shí)并沒(méi)有真正向操作系統(tǒng)申請(qǐng)分配內(nèi)存, 而是通 過(guò)計(jì)算得知內(nèi)存無(wú)法分配就會(huì) 在代碼里手動(dòng)拋出溢出異常, 真正申請(qǐng)分配內(nèi)存的方法是Unsafe::allocateMemory()。