百度二面:一個(gè)線程OOM了,其它線程還能運(yùn)行嗎?
由于面試官僅提到OOM,但 Java 的OOM又分很多類型:- 堆溢出(“java.lang.OutOfMemoryError: Java heap space”)
- 永久代溢出(“java.lang.OutOfMemoryError:Permgen space”)
- 不能創(chuàng)建線程(“java.lang.OutOfMemoryError:Unable to create new native thread”)
- 通過(guò)代碼驗(yàn)證《Java虛擬機(jī)規(guī)范》中描述的各個(gè)運(yùn)行時(shí)區(qū)域儲(chǔ)存的內(nèi)容
- 在工作中遇到實(shí)際的內(nèi)存溢出異常時(shí),能根據(jù)異常的提示信息迅速得知是哪個(gè)區(qū)域的內(nèi)存溢出,知道怎樣的代碼可能會(huì)導(dǎo)致這些區(qū)域內(nèi)存溢出,以及出現(xiàn)這些異常后該如何處理。
Java堆用于儲(chǔ)存對(duì)象實(shí)例,只要不斷地創(chuàng)建對(duì)象,并且保證GC Roots到對(duì)象之間有可達(dá)路徑來(lái)避免GC機(jī)制清除這些對(duì)象,則隨對(duì)象數(shù)量增加,總?cè)萘坑|及最大堆的容量限制后就會(huì)產(chǎn)生內(nèi)存溢出異常。限制Java堆的大小20MB,不可擴(kuò)展
-XX: HeapDumpOnOutOf-MemoryError可以讓虛擬機(jī)在出現(xiàn)內(nèi)存溢出異常的時(shí)候Dump出當(dāng)前的內(nèi)存堆轉(zhuǎn)儲(chǔ)快照。案例1


Java堆內(nèi)存的OOM是實(shí)際應(yīng)用中最常見(jiàn)的內(nèi)存溢出異常場(chǎng)景。出現(xiàn)Java堆內(nèi)存溢出時(shí),異常堆棧信息“java.lang.OutOfMemoryError”會(huì)跟隨進(jìn)一步提示“Java heap space”。那既然發(fā)生了,如何解決這個(gè)內(nèi)存區(qū)域的異常呢?
一般先通過(guò)內(nèi)存映像分析工具(如jprofile)對(duì)Dump出來(lái)的堆轉(zhuǎn)儲(chǔ)快照進(jìn)行分析。
第一步首先確認(rèn)內(nèi)存中導(dǎo)致OOM的對(duì)象是否是必要的,即先分清楚:
- 內(nèi)存泄漏(Memory Leak)
- 內(nèi)存溢出(Memory Overflow)
若是內(nèi)存泄漏,可查看泄漏對(duì)象到GC Roots的引用鏈,找到泄漏對(duì)象是通過(guò)怎樣的引用路徑、與哪些GC Roots相關(guān)聯(lián),才導(dǎo)致垃圾收集器無(wú)法回收它們。根據(jù)泄漏對(duì)象的類型信息以及它到GC Roots引用鏈的信息,一般可以比較準(zhǔn)確地定位到這些對(duì)象創(chuàng)建的位置,進(jìn)而找出產(chǎn)生內(nèi)存泄漏的代碼的具體位置。若不是內(nèi)存泄漏,即就是內(nèi)存中的對(duì)象確實(shí)都必須存活,則應(yīng):- 檢查JVM堆參數(shù)(-Xmx與-Xms)的設(shè)置,與機(jī)器內(nèi)存對(duì)比,看是否還有向上調(diào)整的空間
- 再檢查代碼是否存在某些對(duì)象生命周期過(guò)長(zhǎng)、持有狀態(tài)時(shí)間過(guò)長(zhǎng)、存儲(chǔ)結(jié)構(gòu)設(shè)計(jì)不合理等情況,盡量減少程序運(yùn) 行期的內(nèi)存消耗
案例 2

JVM啟動(dòng)參數(shù)設(shè)置:
-Xms5m -Xmx10m -XX: HeapDumpOnOutOfMemoryError
JVM堆空間的變化
堆的使用大小,突然抖動(dòng)!說(shuō)明當(dāng)一個(gè)線程拋OOM后,它所占據(jù)的內(nèi)存資源會(huì)全部被釋放掉,而不會(huì)影響其他線程的正常運(yùn)行!
所以一個(gè)線程溢出后,進(jìn)程里的其他線程還能照常運(yùn)行。
發(fā)生OOM的線程一般情況下會(huì)死亡,也就是會(huì)被終結(jié)掉,該線程持有的對(duì)象占用的heap都會(huì)被gc了,釋放內(nèi)存。因?yàn)榘l(fā)生OOM之前要進(jìn)行g(shù)c,就算其他線程能夠正常工作,也會(huì)因?yàn)轭l繁gc產(chǎn)生較大的影響。
2 虛擬機(jī)棧/本地方法棧溢出
由于HotSpot JVM并不區(qū)分虛擬機(jī)棧和本地方法棧,因此HotSpot的
-Xoss參數(shù)(設(shè)置本地方法棧的大?。╇m然存在,但無(wú)任何效果,棧容量只能由-Xss參數(shù)設(shè)定。關(guān)于虛擬機(jī)棧和本地方法棧,《Java虛擬機(jī)規(guī)范》描述如下異常:- 若線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError異常
- 若虛擬機(jī)的棧內(nèi)存允許動(dòng)態(tài)擴(kuò)展,當(dāng)擴(kuò)展棧容量無(wú)法申請(qǐng)到足夠的內(nèi)存時(shí),將拋出 OutOfMemoryError異常
如何驗(yàn)證呢?
做倆實(shí)驗(yàn),先在單線程操作,嘗試下面兩種行為是否能讓HotSpot OOM:使用-Xss減少棧內(nèi)存容量
示例
結(jié)果
拋StackOverflowError異常,異常出現(xiàn)時(shí)輸出的堆棧深度相應(yīng)縮小。
不同版本的Java虛擬機(jī)和不同的操作系統(tǒng),棧容量最小值可能會(huì)有所限制,這主要取決于操作系統(tǒng)內(nèi)存分頁(yè)大小。譬如上述方法中的參數(shù)-Xss160k可以正常用于62位macOS系統(tǒng)下的JDK 8,但若用于64位Windows系統(tǒng)下的JDK 11,則會(huì)提示棧容量最小不能低于180K,而在Linux下這個(gè)值則可能是228K,如果低于這個(gè)最小限制,HotSpot虛擬器啟動(dòng)時(shí)會(huì)給出如下提示:
The stack size specified is too small, Specify at定義大量局部變量,增大此方法幀中本地變量表的長(zhǎng)度
示例:
結(jié)果:
所以無(wú)論是由于棧幀太或虛擬機(jī)棧容量太小,當(dāng)新的棧幀內(nèi)存無(wú)法分配時(shí), HotSpot 都拋SOF??扇粼谠试S動(dòng)態(tài)擴(kuò)展棧容量大小的虛擬機(jī)上,相同代碼則會(huì)導(dǎo)致不同情況。若測(cè)試時(shí)不限于單線程,而是不斷新建線程,在HotSpot上也會(huì)產(chǎn)生OOM。但這樣產(chǎn)生OOM和??臻g是否足夠不存在直接的關(guān)系,主要取決于os本身內(nèi)存使用狀態(tài)。甚至說(shuō)這種情況下,給每個(gè)線程的棧分配的內(nèi)存越大,反而越容易產(chǎn)生OOM。不難理解,os分配給每個(gè)進(jìn)程的內(nèi)存有限制,比如32位Windows的單個(gè)進(jìn)程最大內(nèi)存限制為2G。HotSpot提供參數(shù)可以控制Java堆和方法區(qū)這兩部分的內(nèi)存的最大值,那剩余的內(nèi)存即為2G(os限制)減去最大堆容量,再減去最大方法區(qū)容量,由于程序計(jì)數(shù)器消耗內(nèi)存很小,可忽略,若把直接內(nèi)存和虛擬機(jī)進(jìn)程本身耗費(fèi)的內(nèi)存也去掉,剩下的內(nèi)存就由虛擬機(jī)棧和本地方法棧來(lái)分配了。因此為每個(gè)線程分配到的棧內(nèi)存越大,可以建立的線程數(shù)量越少,建立線程時(shí)就越容易把剩下的內(nèi)存耗盡:示例:
結(jié)果:Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread出現(xiàn)SOF時(shí),會(huì)有明確錯(cuò)誤堆棧可供分析,相對(duì)容易定位問(wèn)題。如果使用HotSpot虛擬機(jī)默認(rèn)參數(shù),棧深度在大多數(shù)情況下(因?yàn)槊總€(gè)方法壓入棧的幀大小并不是一樣的)到達(dá)1000~2000沒(méi)有問(wèn)題,對(duì)于正常的方法調(diào)用(包括不能做尾遞歸優(yōu)化的遞歸調(diào)用),這個(gè)深度應(yīng)該完全夠用。但如果是建立過(guò)多線程導(dǎo)致的內(nèi)存溢出,在不能減少線程數(shù)量或者更換64位虛擬機(jī)的情況下,就只能通過(guò)減少最大堆和減少棧容量換取更多的線程。這種通過(guò)“減少內(nèi)存”手段解決內(nèi)存溢出的方式,如果沒(méi)有這方面處理經(jīng)驗(yàn),一般比較難以想到。也是由于這種問(wèn)題較為隱蔽,從 JDK 7起,以上提示信息中“unable to create native thread”后面,虛擬機(jī)會(huì)特別注明原因可能是“possibly
#define OS_NATIVE_THREAD_CREATION_FAILED_MSG "unable to create native thread: possibly out of memory or process/resource limits reached"3 方法區(qū)和運(yùn)行時(shí)常量池溢出運(yùn)行時(shí)常量池是方法區(qū)的一部分,所以這兩個(gè)區(qū)域的溢出測(cè)試可以放到一起。HotSpot從JDK 7開(kāi)始逐步“去永久代”,在JDK 8中完全使用元空間代替永久代。那么方法區(qū)使用“永久代”還是“元空間”來(lái)實(shí)現(xiàn),對(duì)程序有何影響呢?
String::intern()

一個(gè)本地方法:若字符串常量池中已經(jīng)包含一個(gè)等于此String對(duì)象的字符串,則返回代表池中這個(gè)字符串的String對(duì)象的引用;否則,會(huì)將此String對(duì)象包含的字符串添加到常量池,并且返回此String對(duì)象的引用。在JDK6或之前HotSpot虛擬機(jī),常量池都是分配在永久代,可以通過(guò)如下兩個(gè)參數(shù):

限制永久代的大小,即可間接限制其中常量池的容量,實(shí)例
結(jié)果:Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.String.intern(Native Method) at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18)可見(jiàn),運(yùn)行時(shí)常量池溢出時(shí),在OutOfMemoryError異常后面跟隨的提示信息是“PermGen space”,說(shuō)明運(yùn)行時(shí)常量池的確是屬于方法區(qū)(即JDK 6的HotSpot虛擬機(jī)中的永久代)的 一部分。而使用JDK 7或更高版本的JDK來(lái)運(yùn)行這段程序并不會(huì)得到相同的結(jié)果,無(wú)論是在JDK 7中繼續(xù)使 用-XX:MaxPermSize參數(shù)或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize參數(shù)把方法區(qū)容量同樣限制在6MB,也都不會(huì)重現(xiàn)JDK 6中的溢出異常,循環(huán)將一直進(jìn)行下去,永不停歇。
這種變化是因?yàn)樽訨DK 7起,原本存放在永久代的字符串常量池被移至Java堆,所以在JDK 7及以上版 本,限制方法區(qū)的容量對(duì)該測(cè)試用例來(lái)說(shuō)是毫無(wú)意義。這時(shí)候使用-Xmx參數(shù)限制最大堆到6MB就能看到以下兩種運(yùn)行結(jié)果之一,具體取決于哪里的對(duì)象分配時(shí)產(chǎn)生了溢出:
// OOM異常一:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.base/java.lang.Integer.toString(Integer.java:440) at java.base/java.lang.String.valueOf(String.java:3058) at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:12)
// OOM異常二:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.base/java.util.HashMap.resize(HashMap.java:699) at java.base/java.util.HashMap.putVal(HashMap.java:658) at java.base/java.util.HashMap.put(HashMap.java:607) at java.base/java.util.HashSet.add(HashSet.java:220) at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java from InputFile-Object:14)字符串常量池的實(shí)現(xiàn)位置還有很多趣事:

JDK 6中運(yùn)行,結(jié)果是兩個(gè)false
JDK 7中運(yùn)行,一個(gè)true和一個(gè)false

因?yàn)镴DK6的intern()會(huì)把首次遇到的字符串實(shí)例復(fù)制到永久代的字符串常量池中,返回的也是永久代里這個(gè)字符串實(shí)例的引用,而由StringBuilder創(chuàng)建的字符串對(duì)象實(shí)例在 Java 堆,所以不可能是同一個(gè)引用,結(jié)果將返回false。JDK 7及以后的intern()無(wú)需再拷貝字符串的實(shí)例到永久代,字符串常量池已移到Java堆,只需在常量池里記錄一下首次出現(xiàn)的實(shí)例引用,因此intern()返回的引用和由StringBuilder創(chuàng)建的那個(gè)字符串實(shí)例是同一個(gè)。str2比較返回false,這是因?yàn)椤癹ava”這個(gè)字符串在執(zhí)行String-Builder.toString()之前就已經(jīng)出現(xiàn)過(guò)了,字符串常量池中已經(jīng)有它的引用,不符合intern()方法要求“首次遇到”的原則,而“計(jì)算機(jī)軟件”這個(gè)字符串則是首次 出現(xiàn)的,因此結(jié)果返回true!對(duì)于方法區(qū)的測(cè)試,基本的思路是運(yùn)行時(shí)產(chǎn)生大量類去填滿方法區(qū),直到溢出。雖然直接使用Java SE API也可動(dòng)態(tài)產(chǎn)生類(如反射時(shí)的 GeneratedConstructorAccessor和動(dòng)態(tài)代理),但操作麻煩。
借助了CGLib直接操作字節(jié)碼運(yùn)行時(shí)生成大量動(dòng)態(tài)類。當(dāng)前的很多主流框架,如Spring、Hibernate對(duì)類進(jìn)行增強(qiáng)時(shí),都會(huì)使用到 CGLib字節(jié)碼增強(qiáng),當(dāng)增強(qiáng)的類越多,就需要越大的方法區(qū)以保證動(dòng)態(tài)生成的新類型可以載入內(nèi)存。
很多運(yùn)行于JVM的動(dòng)態(tài)語(yǔ)言(例如Groovy)通常都會(huì)持續(xù)創(chuàng)建新類型來(lái)支撐語(yǔ)言的動(dòng)態(tài)性,隨著這類動(dòng)態(tài)語(yǔ)言的流行,與如下代碼相似的溢出場(chǎng)景也越來(lái)越容易遇到在JDK 7中的運(yùn)行結(jié)果:
Caused by: java.lang.OutOfMemoryError: PermGen space at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632) at java.lang.ClassLoader.defineClass(ClassLoader.java:616)JDK8及以后:可以使用
-XX:MetaspaceSize=10M-XX:MaxMetaspaceSize=10M設(shè)置元空間初始大小以及最大可分配大小。
1.如果不指定元空間的大小,默認(rèn)情況下,元空間最大的大小是系統(tǒng)內(nèi)存的大小,元空間一直擴(kuò)大,虛擬機(jī)可能會(huì)消耗完所有的可用系統(tǒng)內(nèi)存。
2.如果元空間內(nèi)存不夠用,就會(huì)報(bào)OOM。
3.默認(rèn)情況下,對(duì)應(yīng)一個(gè)64位的服務(wù)端JVM來(lái)說(shuō),其默認(rèn)的-XX:MetaspaceSize值為21MB,這就是初始的高水位線,一旦元空間的大小觸及這個(gè)高水位線,就會(huì)觸發(fā)Full GC并會(huì)卸載沒(méi)有用的類,然后高水位線的值將會(huì)被重置。
4.從第3點(diǎn)可以知道,如果初始化的高水位線設(shè)置過(guò)低,會(huì)頻繁的觸發(fā)Full GC,高水位線會(huì)被多次調(diào)整。所以為了避免頻繁GC以及調(diào)整高水位線,建議將-XX:MetaspaceSize設(shè)置為較高的值,而-XX:MaxMetaspaceSize不進(jìn)行設(shè)置。JDK8 運(yùn)行結(jié)果:

一個(gè)類如果要被gc,要達(dá)成的條件比較苛刻。在經(jīng)常運(yùn)行時(shí)生成大量動(dòng)態(tài)類的場(chǎng)景,就應(yīng)該特別關(guān)注這些類的回收狀況。
這類場(chǎng)景除了之前提到的程序使用了CGLib字節(jié)碼增強(qiáng)和動(dòng)態(tài)語(yǔ)言外,常見(jiàn)的還有:JDK8后,永久代完全廢棄,而使用元空間作為其替代者。在默認(rèn)設(shè)置下,前面列舉的那些正常的動(dòng)態(tài)創(chuàng)建新類型的測(cè)試用例已經(jīng)很難再迫使虛擬機(jī)產(chǎn)生方法區(qū)OOM。
為了讓使用者有預(yù)防實(shí)際應(yīng)用里出現(xiàn)類似于如上代碼那樣的破壞性操作,HotSpot還是提供了一些參數(shù)作為元空間的防御措施:4 本機(jī)直接內(nèi)存溢出
直接內(nèi)存(Direct Memory)的容量大小可通過(guò)
-XX:MaxDirectMemorySize指定,若不指定,則默認(rèn)與Java堆最大值(-Xmx)一致。這里越過(guò)DirectByteBuffer類,直接通過(guò)反射獲取Unsafe實(shí)例進(jìn)行內(nèi)存分配。Unsafe類的getUnsafe()指定只有引導(dǎo)類加載器才會(huì)返回實(shí)例,體現(xiàn)了設(shè)計(jì)者希望只有虛擬機(jī)標(biāo)準(zhǔn)類庫(kù)里面的類才能使用Unsafe,JDK10時(shí)才將Unsafe的部分功能通過(guò)VarHandle開(kāi)放給外部。
因?yàn)殡m然使用DirectByteBuffer分配內(nèi)存也會(huì)拋OOM,但它拋異常時(shí)并未真正向os申請(qǐng)分配內(nèi)存,而是通過(guò)計(jì)算得知內(nèi)存無(wú)法分配,就在代碼里手動(dòng)拋了OOM,真正申請(qǐng)分配內(nèi)存的方法是Unsafe::allocateMemory()使用unsafe分配本機(jī)內(nèi)存:
結(jié)果:
由直接內(nèi)存導(dǎo)致的內(nèi)存溢出,一個(gè)明顯的特征是在Heap Dump文件中不會(huì)看見(jiàn)有什么明顯異常,若發(fā)現(xiàn)內(nèi)存溢出之后產(chǎn)生的Dump文件很小,而程序中又直接或間接使用了 DirectMemory(比如使用NIO),則該考慮直接內(nèi)存了。





