<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          百度二面:一個(gè)線程OOM了,其它線程還能運(yùn)行嗎?

          共 7531字,需瀏覽 16分鐘

           ·

          2021-07-06 11:35


            點(diǎn)擊上方“JavaEdge”,關(guān)注公眾號(hào)

          設(shè)為“星標(biāo)”,好文章不錯(cuò)過!

          由于面試官僅提到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”)

          OOM在《Java虛擬機(jī)規(guī)范》里,除程序計(jì)數(shù)器,虛擬機(jī)內(nèi)存的其他幾個(gè)運(yùn)行時(shí)區(qū)域都可能發(fā)生OOM,那本文的目的是啥呢?

          • 通過代碼驗(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)這些異常后該如何處理。

          本文代碼均由筆者在基于OpenJDK 8中的HotSpot虛擬機(jī)上進(jìn)行過實(shí)際測試。

          1 Java堆溢出



          Java堆用于儲(chǔ)存對象實(shí)例,只要不斷地創(chuàng)建對象,并且保證GC Roots到對象之間有可達(dá)路徑來避免GC機(jī)制清除這些對象,則隨對象數(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


          不久后報(bào)錯(cuò)!

          Java堆內(nèi)存的OOM是實(shí)際應(yīng)用中最常見的內(nèi)存溢出異常場景。出現(xiàn)Java堆內(nèi)存溢出時(shí),異常堆棧信息“java.lang.OutOfMemoryError”會(huì)跟隨進(jìn)一步提示“Java heap space”。

          那既然發(fā)生了,如何解決這個(gè)內(nèi)存區(qū)域的異常呢?
          一般先通過內(nèi)存映像分析工具(如jprofile)對Dump出來的堆轉(zhuǎn)儲(chǔ)快照進(jìn)行分析。
          第一步首先確認(rèn)內(nèi)存中導(dǎo)致OOM的對象是否是必要的,即先分清楚:

          • 內(nèi)存泄漏(Memory Leak)

          • 內(nèi)存溢出(Memory Overflow)

          使用 jprofile打開的堆轉(zhuǎn)儲(chǔ)快照文件(java_pid44526.hprof)

          若是內(nèi)存泄漏,可查看泄漏對象到GC Roots的引用鏈,找到泄漏對象是通過怎樣的引用路徑、與哪些GC Roots相關(guān)聯(lián),才導(dǎo)致垃圾收集器無法回收它們。根據(jù)泄漏對象的類型信息以及它到GC Roots引用鏈的信息,一般可以比較準(zhǔn)確地定位到這些對象創(chuàng)建的位置,進(jìn)而找出產(chǎn)生內(nèi)存泄漏的代碼的具體位置。

          若不是內(nèi)存泄漏,即就是內(nèi)存中的對象確實(shí)都必須存活,則應(yīng):

          1. 檢查JVM堆參數(shù)(-Xmx與-Xms)的設(shè)置,與機(jī)器內(nèi)存對比,看是否還有向上調(diào)整的空間

          2. 再檢查代碼是否存在某些對象生命周期過長、持有狀態(tài)時(shí)間過長、存儲(chǔ)結(jié)構(gòu)設(shè)計(jì)不合理等情況,盡量減少程序運(yùn) 行期的內(nèi)存消耗

          以上是處理Java堆內(nèi)存問題的簡略思路。



          案例 2


          JVM啟動(dòng)參數(shù)設(shè)置:

          -Xms5m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError


          JVM堆空間的變化

          堆的使用大小,突然抖動(dòng)!說明當(dāng)一個(gè)線程拋OOM后,它所占據(jù)的內(nèi)存資源會(huì)全部被釋放掉,而不會(huì)影響其他線程的正常運(yùn)行!
          所以一個(gè)線程溢出后,進(jìn)程里的其他線程還能照常運(yùn)行。
          發(fā)生OOM的線程一般情況下會(huì)死亡,也就是會(huì)被終結(jié)掉,該線程持有的對象占用的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然存在,但無任何效果,棧容量只能由-Xss參數(shù)設(shè)定。

          關(guān)于虛擬機(jī)棧和本地方法棧,《Java虛擬機(jī)規(guī)范》描述如下異常:

          1. 若線程請求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError異常

          2. 若虛擬機(jī)的棧內(nèi)存允許動(dòng)態(tài)擴(kuò)展,當(dāng)擴(kuò)展棧容量無法申請到足夠的內(nèi)存時(shí),將拋出 OutOfMemoryError異常

          《Java虛擬機(jī)規(guī)范》明確允許JVM實(shí)現(xiàn)自行選擇是否支持棧的動(dòng)態(tài)擴(kuò)展,而HotSpot虛擬機(jī)的選擇是不支持?jǐn)U展,所以除非在創(chuàng)建線程申請內(nèi)存時(shí)就因無法獲得足夠內(nèi)存而出現(xiàn)OOM,否則在線程運(yùn)行時(shí)是不會(huì)因?yàn)閿U(kuò)展而導(dǎo)致內(nèi)存溢出的,只會(huì)因?yàn)闂H萘繜o法容納新的棧幀而導(dǎo)致StackOverflowError。

          如何驗(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)存分頁大小。譬如上述方法中的參數(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

          定義大量局部變量,增大此方法幀中本地變量表的長度

          示例:

          結(jié)果:
          所以無論是由于棧幀太或虛擬機(jī)棧容量太小,當(dāng)新的棧幀內(nèi)存無法分配時(shí), HotSpot 都拋SOF??扇粼谠试S動(dòng)態(tài)擴(kuò)展棧容量大小的虛擬機(jī)上,相同代碼則會(huì)導(dǎo)致不同情況。

          若測試時(shí)不限于單線程,而是不斷新建線程,在HotSpot上也會(huì)產(chǎn)生OOM。但這樣產(chǎn)生OOM和??臻g是否足夠不存在直接的關(guān)系,主要取決于os本身內(nèi)存使用狀態(tài)。甚至說這種情況下,給每個(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ī)棧和本地方法棧來分配了。因此為每個(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ò)誤堆??晒┓治?,相對容易定位問題。如果使用HotSpot虛擬機(jī)默認(rèn)參數(shù),棧深度在大多數(shù)情況下(因?yàn)槊總€(gè)方法壓入棧的幀大小并不是一樣的)到達(dá)1000~2000沒有問題,對于正常的方法調(diào)用(包括不能做尾遞歸優(yōu)化的遞歸調(diào)用),這個(gè)深度應(yīng)該完全夠用。但如果是建立過多線程導(dǎo)致的內(nèi)存溢出,在不能減少線程數(shù)量或者更換64位虛擬機(jī)的情況下,就只能通過減少最大堆和減少棧容量換取更多的線程。這種通過“減少內(nèi)存”手段解決內(nèi)存溢出的方式,如果沒有這方面處理經(jīng)驗(yà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ū)域的溢出測試可以放到一起。

          HotSpot從JDK 7開始逐步“去永久代”,在JDK 8中完全使用元空間代替永久代。

          那么方法區(qū)使用“永久代”還是“元空間”來實(shí)現(xiàn),對程序有何影響呢?



          String::intern()


          一個(gè)本地方法:若字符串常量池中已經(jīng)包含一個(gè)等于此String對象的字符串,則返回代表池中這個(gè)字符串的String對象的引用;否則,會(huì)將此String對象包含的字符串添加到常量池,并且返回此String對象的引用。

          在JDK6或之前HotSpot虛擬機(jī),常量池都是分配在永久代,可以通過如下兩個(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)


          可見,運(yùn)行時(shí)常量池溢出時(shí),在OutOfMemoryError異常后面跟隨的提示信息是“PermGen space”,說明運(yùn)行時(shí)常量池的確是屬于方法區(qū)(即JDK 6的HotSpot虛擬機(jī)中的永久代)的 一部分。

          而使用JDK 7或更高版本的JDK來運(yùn)行這段程序并不會(huì)得到相同的結(jié)果,無論是在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ū)的容量對該測試用例來說是毫無意義。

          這時(shí)候使用-Xmx參數(shù)限制最大堆到6MB就能看到以下兩種運(yùn)行結(jié)果之一,具體取決于哪里的對象分配時(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)建的字符串對象實(shí)例在 Java 堆,所以不可能是同一個(gè)引用,結(jié)果將返回false。

          JDK 7及以后的intern()無需再拷貝字符串的實(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)過了,字符串常量池中已經(jīng)有它的引用,不符合intern()方法要求“首次遇到”的原則,而“計(jì)算機(jī)軟件”這個(gè)字符串則是首次 出現(xiàn)的,因此結(jié)果返回true!

          對于方法區(qū)的測試,基本的思路是運(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對類進(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)語言(例如Groovy)通常都會(huì)持續(xù)創(chuàng)建新類型來支撐語言的動(dòng)態(tài)性,隨著這類動(dòng)態(tà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)情況下,對應(yīng)一個(gè)64位的服務(wù)端JVM來說,其默認(rèn)的-XX:MetaspaceSize值為21MB,這就是初始的高水位線,一旦元空間的大小觸及這個(gè)高水位線,就會(huì)觸發(fā)Full GC并會(huì)卸載沒有用的類,然后高水位線的值將會(huì)被重置。
          4.從第3點(diǎn)可以知道,如果初始化的高水位線設(shè)置過低,會(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)類的場景,就應(yīng)該特別關(guān)注這些類的回收狀況。
          這類場景除了之前提到的程序使用了CGLib字節(jié)碼增強(qiáng)和動(dòng)態(tài)語言外,常見的還有:

          • 大量JSP或動(dòng)態(tài)產(chǎn)生JSP 文件的應(yīng)用(JSP第一次運(yùn)行時(shí)需要編譯為Java類)

          • 基于OSGi的應(yīng)用(即使是同一個(gè)類文件,被不同的加載器加載也會(huì)視為不同的類)

          JDK8后,永久代完全廢棄,而使用元空間作為其替代者。在默認(rèn)設(shè)置下,前面列舉的那些正常的動(dòng)態(tài)創(chuàng)建新類型的測試用例已經(jīng)很難再迫使虛擬機(jī)產(chǎn)生方法區(qū)OOM。
          為了讓使用者有預(yù)防實(shí)際應(yīng)用里出現(xiàn)類似于如上代碼那樣的破壞性操作,HotSpot還是提供了一些參數(shù)作為元空間的防御措施:

          • -XX:MetaspaceSize
            指定元空間的初始空間大小,以字節(jié)為單位,達(dá)到該值就會(huì)觸發(fā)垃圾收集進(jìn)行類型卸載,同時(shí)收集器會(huì)對該值進(jìn)行調(diào)整。如果釋放了大量的空間,就適當(dāng)降低該值,如果釋放了很少空間,則在不超過-XX:MaxMetaspaceSize(如果設(shè)置了的話)的情況下,適當(dāng)提高該值

          • -XX:MaxMetaspaceSize
            設(shè)置元空間最大值,默認(rèn)-1,即不限制,或者說只受限于本地內(nèi)存的大小

          • -XX:MinMetaspaceFreeRatio
            在GC后控制最小的元空間剩余容量的百分比,可減少因?yàn)樵臻g不足導(dǎo)致的GC頻率

          • -XX:Max-MetaspaceFreeRatio
            控制最大的元空間剩余容量的百分比

          4 本機(jī)直接內(nèi)存溢出



          直接內(nèi)存(Direct Memory)的容量大小可通過-XX:MaxDirectMemorySize指定,若不指定,則默認(rèn)與Java堆最大值(-Xmx)一致。

          這里越過DirectByteBuffer類,直接通過反射獲取Unsafe實(shí)例進(jìn)行內(nèi)存分配。
          Unsafe類的getUnsafe()指定只有引導(dǎo)類加載器才會(huì)返回實(shí)例,體現(xiàn)了設(shè)計(jì)者希望只有虛擬機(jī)標(biāo)準(zhǔn)類庫里面的類才能使用Unsafe,JDK10時(shí)才將Unsafe的部分功能通過VarHandle開放給外部。
          因?yàn)殡m然使用DirectByteBuffer分配內(nèi)存也會(huì)拋OOM,但它拋異常時(shí)并未真正向os申請分配內(nèi)存,而是通過計(jì)算得知內(nèi)存無法分配,就在代碼里手動(dòng)拋了OOM,真正申請分配內(nèi)存的方法是Unsafe::allocateMemory()

          使用unsafe分配本機(jī)內(nèi)存:

          結(jié)果:

          由直接內(nèi)存導(dǎo)致的內(nèi)存溢出,一個(gè)明顯的特征是在Heap Dump文件中不會(huì)看見有什么明顯異常,若發(fā)現(xiàn)內(nèi)存溢出之后產(chǎn)生的Dump文件很小,而程序中又直接或間接使用了 DirectMemory(比如使用NIO),則該考慮直接內(nèi)存了。

          往期推薦


          由于不知線程池的bug,某Java程序員叕被祭天

          程序員因重復(fù)記錄日志撐爆ELK被辭退!

          擁抱Kubernetes,再見了Spring Cloud

          和阿里P8大佬面試互懟了半小時(shí)的Fork/Join原理!


          目前交流群已有 800+人,旨在促進(jìn)技術(shù)交流,可關(guān)注公眾號(hào)添加筆者微信邀請進(jìn)群



          喜歡文章,點(diǎn)個(gè)“在看、點(diǎn)贊、分享”素質(zhì)三連支持一下~

          瀏覽 43
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲AA一| 操女人网站 | 久草视频免费播放 | 大香蕉精品在线了 | 黄色日逼免费网站 |