因爲面試官僅提到OOM,但 Java 的OOM又分不少類型的呀:java
堆溢出(「java.lang.OutOfMemoryError: Java heap space」)面試
永久代溢出(「java.lang.OutOfMemoryError:Permgen space」)bash
不能建立線程(「java.lang.OutOfMemoryError:Unable to create new native thread」)markdown
OOM在《Java虛擬機規範》裏,除程序計數器,虛擬機內存的其餘幾個運行時區域均可能發生OOM,那本文的目的是啥呢?多線程
經過代碼驗證《Java虛擬機規範》中描述的各個運行時區域儲存的內容框架
在工做中遇到實際的內存溢出異常時,能根據異常的提示信息迅速得知是哪一個區域的內存溢出,知道怎樣的代碼可能會致使這些區域內存溢出,以及出現這些異常後該如何處理。工具
本文代碼均由筆者在基於OpenJDK 8中的HotSpot虛擬機上進行過實際測試。測試
Java堆用於儲存對象實例,只要不斷地建立對象,而且保證GC Roots到對象之間有可達路徑來避免GC機制清除這些對象,則隨對象數量增長,總容量觸及最大堆的容量限制後就會產生內存溢出異常。優化
限制Java堆的大小20MB,不可擴展ui
-XX:+HeapDumpOnOutOf-MemoryError
複製代碼
可讓虛擬機在出現內存溢出異常的時候Dump出當前的內存堆轉儲快照。
Java堆內存的OOM是實際應用中最多見的內存溢出異常場景。出現Java堆內存溢出時,異常堆棧信息「java.lang.OutOfMemoryError」會跟隨進一步提示「Java heap space」。
那既然發生了,如何解決這個內存區域的異常呢? 通常先經過內存映像分析工具(如jprofile)對Dump出來的堆轉儲快照進行分析。 第一步首先確認內存中致使OOM的對象是不是必要的,即先分清楚究竟是
內存泄漏(Memory Leak)
仍是內存溢出(Memory Overflow)
下圖是使用 jprofile打開的堆轉儲快照文件(java_pid44526.hprof)
如果內存泄漏,可查看泄漏對象到GC Roots的引用鏈,找到泄漏對象是經過怎樣的引用路徑、與哪些GC Roots相關聯,才致使垃圾收集器沒法回收它們,根據泄漏對象的類型信息以及它到GC Roots引用鏈的信息,通常能夠比較準確地定位到這些對象建立的位置,進而找出產生內存泄漏的代碼的具體位置。
若不是內存泄漏,即就是內存中的對象確實都必須存活,則應:
以上是處理Java堆內存問題的簡略思路。
JVM啓動參數設置:
-Xms5m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
複製代碼
堆的使用大小,忽然抖動!說明當一個線程拋OOM後,它所佔據的內存資源會所有被釋放掉,而不會影響其餘線程的正常運行! 因此一個線程溢出後,進程裏的其餘線程還能照常運行。 發生OOM的線程通常狀況下會死亡,也就是會被終結掉,該線程持有的對象佔用的heap都會被gc了,釋放內存。由於發生OOM以前要進行gc,就算其餘線程可以正常工做,也會由於頻繁gc產生較大的影響。
堆溢出和棧溢出,結論是同樣的。
因爲HotSpot JVM並不區分虛擬機棧和本地方法棧,所以HotSpot的-Xoss
參數(設置本地方法棧的大小)雖然存在,但無任何效果,棧容量只能由-Xss
參數設定。
關於虛擬機棧和本地方法棧,《Java虛擬機規範》描述以下異常:
《Java虛擬機規範》明確容許JVM實現自行選擇是否支持棧的動態擴展,而HotSpot虛擬機的選擇是不支持擴展,因此除非在建立線程申請內存時就因沒法得到足夠內存而出現OOM,不然在線程運行時是不會由於擴展而致使內存溢出的,只會由於棧容量沒法容納新的棧幀而致使StackOverflowError。
作倆實驗,先在單線程操做,嘗試下面兩種行爲是否能讓HotSpot OOM:
-Xss
減小棧內存容量拋StackOverflowError異常,異常出現時輸出的堆棧深度相應縮小。
不一樣版本的Java虛擬機和不一樣的操做系統,棧容量最小值可能會有所限制,這主要取決於操做系統內存分頁大小。譬如上述方法中的參數-Xss160k能夠正經常使用於62位macOS系統下的JDK 8,但若用於64位Windows系統下的JDK 11,則會提示棧容量最小不能低於180K,而在Linux下這個值則多是228K,若是低於這個最小限制,HotSpot虛擬器啓動時會給出以下提示:
The stack size specified is too small, Specify at
複製代碼
因此不管是因爲棧幀太或虛擬機棧容量過小,當新的棧幀內存沒法分配時, HotSpot 都拋SOF。可若在容許動態擴展棧容量大小的虛擬機上,相同代碼則會致使不一樣狀況。
若測試時不限於單線程,而是不斷新建線程,在HotSpot上也會產生OOM。但這樣產生OOM和棧空間是否足夠不存在直接的關係,主要取決於os自己內存使用狀態。甚至說這種狀況下,給每一個線程的棧分配的內存越大,反而越容易產生OOM。 不難理解,os分配給每一個進程的內存有限制,好比32位Windows的單個進程最大內存限制爲2G。HotSpot提供參數能夠控制Java堆和方法區這兩部分的內存的最大值,那剩餘的內存即爲2G(os限制)減去最大堆容量,再減去最大方法區容量,因爲程序計數器消耗內存很小,可忽略,若把直接內存和虛擬機進程自己耗費的內存也去掉,剩下的內存就由虛擬機棧和本地方法棧來分配了。所以爲每一個線程分配到的棧內存越大,能夠創建的線程數量越少,創建線程時就越容易把剩下的內存耗盡:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread
複製代碼
出現SOF時,會有明確錯誤堆棧可供分析,相對容易定位問題。若是使用HotSpot虛擬機默認參數,棧深度在大多數狀況下(由於每一個方法壓入棧的幀大小並非同樣的)到達1000~2000沒有問題,對於正常的方法調用(包括不能作尾遞歸優化的遞歸調用),這個深度應該徹底夠用。但若是是創建過多線程致使的內存溢出,在不能減小線程數量或者更換64位虛擬機的狀況下,就只能經過減小最大堆和減小棧容量換取更多的線程。這種經過「減小內存」手段解決內存溢出的方式,若是沒有這方面處理經驗,通常比較難以想到。也是因爲這種問題較爲隱蔽,從 JDK 7起,以上提示信息中「unable to create native thread」後面,虛擬機會特別註明緣由多是「possibly
#define OS_NATIVE_THREAD_CREATION_FAILED_MSG
"unable to create native thread: possibly out of memory or process/resource limits reached"
複製代碼
運行時常量池是方法區的一部分,因此這兩個區域的溢出測試能夠放到一塊兒。
HotSpot從JDK 7開始逐步「去永久代」,在JDK 8中徹底使用元空間代替永久代,那麼方法區使用「永久代」仍是「元空間」來實現,對程序有何影響呢。
String::intern()是一個本地方法:若字符串常量池中已經包含一個等於此String對象的字符串,則返回表明池中這個字符串的String對象的引用;不然,會將此String對象包含的字符串添加到常量池,而且返回此String對象的引用。
在JDK6或以前HotSpot虛擬機,常量池都是分配在永久代,能夠經過以下兩個參數: 限制永久代的大小,便可間接限制其中常量池的容量,
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)
複製代碼
可見,運行時常量池溢出時,在OutOfMemoryError異常後面跟隨的提示信息是「PermGen space」,說明運行時常量池的確是屬於方法區(即JDK 6的HotSpot虛擬機中的永久代)的 一部分。
而使用JDK 7或更高版本的JDK來運行這段程序並不會獲得相同的結果,不管是在JDK 7中繼續使 用-XX:MaxPermSize參數或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize參數把方法區容量一樣限制在6MB,也都不會重現JDK 6中的溢出異常,循環將一直進行下去,永不停歇。 這種變化是由於自JDK 7起,本來存放在永久代的字符串常量池被移至Java堆,因此在JDK 7及以上版 本,限制方法區的容量對該測試用例來講是毫無心義。
這時候使用-Xmx參數限制最大堆到6MB就能看到如下兩種運行結果之一,具體取決於哪裏的對象分配時產生了溢出:
// 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)
複製代碼
字符串常量池的實現位置還有不少趣事: JDK 6中運行,結果是兩個false JDK 7中運行,一個true和一個false 由於JDK6的intern()會把首次遇到的字符串實例複製到永久代的字符串常量池中,返回的也是永久代裏這個字符串實例的引用,而由StringBuilder建立的字符串對象實例在 Java 堆,因此不多是同一個引用,結果將返回false。
JDK 7及之後的intern()無需再拷貝字符串的實例到永久代,字符串常量池已移到Java堆,只需在常量池裏記錄一下首次出現的實例引用,所以intern()返回的引用和由StringBuilder建立的那個字符串實例是同一個。
str2比較返回false,這是由於「java」這個字符串在執行String-Builder.toString()以前就已經出現過了,字符串常量池中已經有它的引用,不符合intern()方法要求「首次遇到」的原則,而「計算機軟件」這個字符串則是首次 出現的,所以結果返回true!
對於方法區的測試,基本的思路是運行時產生大量類去填滿方法區,直到溢出。雖然直接使用Java SE API也可動態產生類(如反射時的 GeneratedConstructorAccessor和動態代理),但操做麻煩。 藉助了CGLib直接操做字節碼運行時生成大量動態類。 當前的不少主流框架,如Spring、Hibernate對類進行加強時,都會使用到 CGLib字節碼加強,當加強的類越多,就須要越大的方法區以保證動態生成的新類型能夠載入內存。 不少運行於JVM的動態語言(例如Groovy)一般都會持續建立新類型來支撐語言的動態性,隨着這類動態語言的流行,與以下代碼類似的溢出場景也愈來愈容易遇到
在JDK 7中的運行結果:
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
複製代碼
設置元空間初始大小以及最大可分配大小。 1.若是不指定元空間的大小,默認狀況下,元空間最大的大小是系統內存的大小,元空間一直擴大,虛擬機可能會消耗完全部的可用系統內存。 2.若是元空間內存不夠用,就會報OOM。 3.默認狀況下,對應一個64位的服務端JVM來講,其默認的-XX:MetaspaceSize值爲21MB,這就是初始的高水位線,一旦元空間的大小觸及這個高水位線,就會觸發Full GC並會卸載沒有用的類,而後高水位線的值將會被重置。 4.從第3點能夠知道,若是初始化的高水位線設置太低,會頻繁的觸發Full GC,高水位線會被屢次調整。因此爲了不頻繁GC以及調整高水位線,建議將-XX:MetaspaceSize設置爲較高的值,而-XX:MaxMetaspaceSize不進行設置。
JDK8 運行結果: 一個類若是要被gc,要達成的條件比較苛刻。在常常運行時生成大量動態類的場景,就應該特別關注這些類的回收情況。 這類場景除了以前提到的程序使用了CGLib字節碼加強和動態語言外,常見的還有:
JDK8後,永久代徹底廢棄,而使用元空間做爲其替代者。在默認設置下,前面列舉的那些正常的動態建立新類型的測試用例已經很難再迫使虛擬機產生方法區OOM。 爲了讓使用者有預防實際應用裏出現相似於如上代碼那樣的破壞性操做,HotSpot仍是提供了一些參數做爲元空間的防護措施:
指定元空間的初始空間大小,以字節爲單位,達到該值就會觸發垃圾收集進行類型卸載,同時收集器會對該值進行調整。若是釋放了大量的空間,就適當下降該值,若是釋放了不多空間,則在不超過-XX:MaxMetaspaceSize(若是設置了的話)的狀況下,適當提升該值
設置元空間最大值,默認-1,即不限制,或者說只受限於本地內存的大小
在GC後控制最小的元空間剩餘容量的百分比,可減小由於元空間不足致使的GC頻率
控制最大的元空間剩餘容量的百分比
直接內存(Direct Memory)的容量大小可經過-XX:MaxDirectMemorySize
指定,若不指定,則默認與Java堆最大值(-Xmx
)一致。
這裏越過DirectByteBuffer類,直接經過反射獲取Unsafe實例進行內存分配。 Unsafe類的getUnsafe()指定只有引導類加載器纔會返回實例,體現了設計者但願只有虛擬機標準類庫裏面的類才能使用Unsafe,JDK10時纔將Unsafe的部分功能經過VarHandle開放給外部。 由於雖然使用DirectByteBuffer分配內存也會拋OOM,但它拋異常時並未真正向os申請分配內存,而是經過計算得知內存沒法分配,就在代碼裏手動拋了OOM,真正申請分配內存的方法是Unsafe::allocateMemory()
由直接內存致使的內存溢出,一個明顯的特徵是在Heap Dump文件中不會看見有什麼明顯異常,若發現內存溢出以後產生的Dump文件很小,而程序中又直接或間接使用了 DirectMemory(好比使用NIO),則該考慮直接內存了。