原文鏈接:https://plumbr.io/outofmemoryerror
我的職業(yè)生涯中見過數(shù)以千計的內存溢出異常均與下文中的8種情況相關乙帮。本文分析什么情況會導致這些異常出現(xiàn)盈简,提供示例代碼的同時為您提供解決指南粮坞。
Nikita Salnikov-Tarnovski
這也許是目前最為完整的Java OOM異常的解決指南置鼻。
一帘不、 java.lang.OutOfMemoryError:Java heap space
Java應用程序在啟動時會指定所需要的內存大小岁歉,它被分割成兩個不同的區(qū)域:Heap space(堆空間)
和Permgen(永久代)
:
這兩個區(qū)域的大小可以在JVM(Java虛擬機)啟動時通過參數(shù)
-Xmx
和-XX:MaxPermSize
設置,如果你沒有顯式設置姑蓝,則將使用特定平臺的默認值鹅心。當應用程序試圖向堆空間添加更多的數(shù)據(jù),但堆卻沒有足夠的空間來容納這些數(shù)據(jù)時纺荧,將會觸發(fā)java.lang.OutOfMemoryError: Java heap space
異常旭愧。需要注意的是:即使有足夠的物理內存可用,只要達到堆空間設置的大小限制宙暇,此異常仍然會被觸發(fā)输枯。
原因分析
觸發(fā)java.lang.OutOfMemoryError: Java heap space
最常見的原因就是應用程序需要的堆空間是XXL號的,但是JVM提供的卻是S號占贫。解決方法也很簡單用押,提供更大的堆空間即可。除了前面的因素還有更復雜的成因:
- 流量/數(shù)據(jù)量峰值:應用程序在設計之初均有用戶量和數(shù)據(jù)量的限制靶剑,某一時刻蜻拨,當用戶數(shù)量或數(shù)據(jù)量突然達到一個峰值,并且這個峰值已經(jīng)超過了設計之初預期的閾值桩引,那么以前正常的功能將會停止缎讼,并觸發(fā)
java.lang.OutOfMemoryError: Java heap space
異常。 - 內存泄漏:特定的編程錯誤會導致你的應用程序不停的消耗更多的內存坑匠,每次使用有內存泄漏風險的功能就會留下一些不能被回收的對象到堆空間中血崭,隨著時間的推移,泄漏的對象會消耗所有的堆空間厘灼,最終觸發(fā)
java.lang.OutOfMemoryError: Java heap space
錯誤夹纫。
示例
①、簡單示例
首先看一個非常簡單的示例设凹,下面的代碼試圖創(chuàng)建2 x 1024 x 1024個元素的整型數(shù)組舰讹,當你嘗試編譯并指定12M堆空間運行時(java -Xmx12m OOM)將會失敗并拋出java.lang.OutOfMemoryError: Java heap space
錯誤,而當你指定13M堆空間時闪朱,將正常的運行月匣。
class OOM {
static final int SIZE=2*1024*1024;
public static void main(String[] a) {
int[] i = new int[SIZE];
}
}
運行如下:
D:\>javac OOM.java
D:\>java -Xmx12m OOM
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at OOM.main(OOM.java:4)
D:\>java -Xmx13m OOM
②、內存泄漏示例
在Java中奋姿,當開發(fā)者創(chuàng)建一個新對象(比如:new Integer(5)
)時锄开,不需要自己開辟內存空間,而是把它交給JVM称诗。在應用程序整個生命周期類萍悴,JVM負責檢查哪些對象可用,哪些對象未被使用。未使用對象將被丟棄癣诱,其占用的內存也將被回收任岸,這一過程被稱為垃圾回收。JVM負責垃圾回收的模塊集合被稱為垃圾回收器(GC
)狡刘。
Java的內存自動管理機制依賴于GC定期查找未使用對象并刪除它們享潜。Java中的內存泄漏是由于GC無法識別一些已經(jīng)不再使用的對象,而這些未使用的對象一直留在堆空間中嗅蔬,這種堆積最終會導致java.lang.OutOfMemoryError: Java heap space
錯誤剑按。
我們可以非常容易的寫出導致內存泄漏的Java代碼:
public class KeylessEntry {
static class Key {
Integer id;
Key(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
}
public static void main(String[] args) {
Map<Key,String> m = new HashMap<Key,String>();
while(true) {
for(int i=0;i<10000;i++) {
if(!m.containsKey(new Key(i))) {
m.put(new Key(i), "Number:" + i);
}
}
}
}
}
代碼中HashMap
為本地緩存,第一次while循環(huán)澜术,會將10000個元素添加到緩存中艺蝴。后面的while循環(huán)中,由于key已經(jīng)存在于緩存中鸟废,緩存的大小將一直會維持在10000猜敢。但事實真的如此嗎?由于Key
實體沒有實現(xiàn)equals()
方法盒延,導致for循環(huán)中每次執(zhí)行m.containsKey(new Key(i))
結果均為false
缩擂,其結果就是HashMap
中的元素將一直增加。
隨著時間的推移添寺,越來越多的Key
對象進入堆空間且不能被垃圾收集器回收(m為局部變量胯盯,GC會認為這些對象一直可用,所以不會回收)计露,直到所有的堆空間被占用博脑,最后拋出java.lang.OutOfMemoryError:Java heap space
。
上面的代碼直接運行可能很久也不會拋出異常票罐,可以在啟動時使用-Xmx參數(shù)叉趣,設置堆內存大小,或者在for循環(huán)后打印HashMap的大小该押,執(zhí)行后會發(fā)現(xiàn)HashMap的size一直再增長疗杉。
解決方法也非常簡單,只要Key
實現(xiàn)自己的equals
方法即可:
Override
public boolean equals(Object o) {
boolean response = false;
if (o instanceof Key) {
response = (((Key)o).id).equals(this.id);
}
return response;
}
解決方案
第一個解決方案是顯而易見的沈善,你應該確保有足夠的堆空間來正常運行你的應用程序乡数,在JVM的啟動配置中增加如下配置:
-Xmx1024m
上面的配置分配1024M堆空間給你的應用程序椭蹄,當然你也可以使用其他單位闻牡,比如用G表示GB,K表示KB绳矩。下面的示例都表示最大堆空間為1GB:
java -Xmx1073741824 com.mycompany.MyClass
java -Xmx1048576k com.mycompany.MyClass
java -Xmx1024m com.mycompany.MyClass
java -Xmx1g com.mycompany.MyClass
然后罩润,更多的時候,單純地增加堆空間不能解決所有的問題翼馆。如果你的程序存在內存泄漏割以,一味的增加堆空間也只是推遲java.lang.OutOfMemoryError: Java heap space
錯誤出現(xiàn)的時間而已金度,并未解決這個隱患。除此之外严沥,垃圾收集器在GC時猜极,應用程序會停止運行直到GC完成,而增加堆空間也會導致GC時間延長消玄,進而影響程序的吞吐量跟伏。
如果你想完全解決這個問題,那就好好提升自己的編程技能吧翩瓜,當然運用好Debuggers, profilers, heap dump analyzers
等工具受扳,可以讓你的程序最大程度的避免內存泄漏問題。
二兔跌、java.lang.OutOfMemoryError:GC overhead limit exceeded
Java運行時環(huán)境(JRE
)包含一個內置的垃圾回收進程勘高,而在許多其他的編程語言中,開發(fā)者需要手動分配和釋放內存坟桅。
Java應用程序只需要開發(fā)者分配內存华望,每當在內存中特定的空間不再使用時,一個單獨的垃圾收集進程會清空這些內存空間仅乓。垃圾收集器怎樣檢測內存中的某些空間不再使用已經(jīng)超出本文的范圍立美,但你只需要相信GC可以做好這些工作即可。
默認情況下方灾,當應用程序花費超過98%的時間用來做GC并且回收了不到2%的堆內存時建蹄,會拋出java.lang.OutOfMemoryError:GC overhead limit exceeded
錯誤。具體的表現(xiàn)就是你的應用幾乎耗盡所有可用內存裕偿,并且GC多次均未能清理干凈洞慎。
原因分析
java.lang.OutOfMemoryError:GC overhead limit exceeded
錯誤是一個信號,示意你的應用程序在垃圾收集上花費了太多時間但卻沒有什么卵用嘿棘。默認超過98%的時間用來做GC卻回收了不到2%的內存時將會拋出此錯誤劲腿。那如果沒有此限制會發(fā)生什么呢?GC進程將被重啟鸟妙,100%的CPU將用于GC焦人,而沒有CPU資源用于其他正常的工作。如果一個工作本來只需要幾毫秒即可完成重父,現(xiàn)在卻需要幾分鐘才能完成花椭,我想這種結果誰都沒有辦法接受。
所以java.lang.OutOfMemoryError:GC overhead limit exceeded
也可以看做是一個fail-fast(快速失敺课纭)
實戰(zhàn)的實例矿辽。
示例
下面的代碼初始化一個map
并在無限循環(huán)中不停的添加鍵值對,運行后將會拋出GC overhead limit exceeded
錯誤:
public class Wrapper {
public static void main(String args[]) throws Exception {
Map map = System.getProperties();
Random r = new Random();
while (true) {
map.put(r.nextInt(), "value");
}
}
}
正如你所預料的那樣,程序不能正常的結束袋倔,事實上雕蔽,當我們使用如下參數(shù)啟動程序時:
java -Xmx100m -XX:+UseParallelGC Wrapper
我們很快就可以看到程序拋出java.lang.OutOfMemoryError: GC overhead limit exceeded
錯誤。但如果在啟動時設置不同的堆空間大小或者使用不同的GC算法宾娜,比如這樣:
java -Xmx10m -XX:+UseParallelGC Wrapper
我們將看到如下錯誤:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Hashtable.rehash(Unknown Source)
at java.util.Hashtable.addEntry(Unknown Source)
at java.util.Hashtable.put(Unknown Source)
at cn.moondev.Wrapper.main(Wrapper.java:12)
使用以下GC算法:-XX:+UseConcMarkSweepGC
或者-XX:+UseG1GC
批狐,啟動命令如下:
java -Xmx100m -XX:+UseConcMarkSweepGC Wrapper
java -Xmx100m -XX:+UseG1GC Wrapper
得到的結果是這樣的:
Exception: java.lang.OutOfMemoryError thrown from
the UncaughtExceptionHandler in thread "main"
錯誤已經(jīng)被默認的異常處理程序捕獲,并且沒有任何錯誤的堆棧信息輸出前塔。
以上這些變化可以說明贾陷,在資源有限的情況下,你根本無法無法預測你的應用是怎樣掛掉的嘱根,什么時候會掛掉髓废,所以在開發(fā)時,你不能僅僅保證自己的應用程序在特定的環(huán)境下正常運行该抒。
解決方案
首先是一個毫無誠意的解決方案慌洪,如果你僅僅是不想看到java.lang.OutOfMemoryError:GC overhead limit exceeded
的錯誤信息,可以在應用程序啟動時添加如下JVM參數(shù):
-XX:-UseGCOverheadLimit
但是強烈建議不要使用這個選項凑保,因為這樣并沒有解決任何問題冈爹,只是推遲了錯誤出現(xiàn)的時間,錯誤信息也變成了我們更熟悉的java.lang.OutOfMemoryError: Java heap space
而已欧引。
另一個解決方案频伤,如果你的應用程序確實內存不足,增加堆內存會解決GC overhead limit
問題芝此,就如下面這樣憋肖,給你的應用程序1G的堆內存:
java -Xmx1024m com.yourcompany.YourClass
但如果你想確保你已經(jīng)解決了潛在的問題,而不是掩蓋java.lang.OutOfMemoryError: GC overhead limit exceeded
錯誤婚苹,那么你不應該僅止步于此岸更。你要記得還有profilers
和memory dump analyzers
這些工具,你需要花費更多的時間和精力來查找問題膊升。還有一點需要注意怎炊,這些工具在Java運行時有顯著的開銷,因此不建議在生產環(huán)境中使用廓译。
三评肆、java.lang.OutOfMemoryError:Permgen space
Java中堆空間是JVM管理的最大一塊內存空間,可以在JVM啟動時指定堆空間的大小非区,其中堆被劃分成兩個不同的區(qū)域:新生代(Young)和老年代(Tenured)瓜挽,新生代又被劃分為3個區(qū)域:Eden
、From Survivor
院仿、To Survivor
秸抚,如下圖所示速和。
java.lang.OutOfMemoryError: PermGen space
錯誤就表明持久代所在區(qū)域的內存已被耗盡歹垫。
原因分析
要理解java.lang.OutOfMemoryError: PermGen space
出現(xiàn)的原因剥汤,首先需要理解Permanent Generation Space
的用處是什么。持久代主要存儲的是每個類的信息排惨,比如:類加載器引用吭敢、運行時常量池(所有常量、字段引用暮芭、方法引用鹿驼、屬性)、字段(Field)數(shù)據(jù)辕宏、方法(Method)數(shù)據(jù)畜晰、方法代碼、方法字節(jié)碼等等瑞筐。我們可以推斷出凄鼻,PermGen
的大小取決于被加載類的數(shù)量以及類的大小。
因此聚假,我們可以得出出現(xiàn)java.lang.OutOfMemoryError: PermGen space
錯誤的原因是:太多的類或者太大的類被加載到permanent generation
(持久代)块蚌。
示例
①、最簡單的示例
正如前面所描述的膘格,PermGen
的使用與加載到JVM類的數(shù)量有密切關系峭范,下面是一個最簡單的示例:
import javassist.ClassPool;
public class MicroGenerator {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100_000_000; i++) {
generate("cn.moondev.User" + i);
}
}
public static Class generate(String name) throws Exception {
ClassPool pool = ClassPool.getDefault();
return pool.makeClass(name).toClass();
}
}
運行時請設置JVM參數(shù):-XX:MaxPermSize=5m
,值越小越好瘪贱。需要注意的是JDK8已經(jīng)完全移除持久代空間纱控,取而代之的是元空間(Metaspace
),所以示例最好的JDK1.7或者1.6下運行菜秦。
代碼在運行時不停的生成類并加載到持久代中其徙,直到撐滿持久代內存空間,最后拋出java.lang.OutOfMemoryError:Permgen space
喷户。代碼中類的生成使用了javassist
庫唾那。
②、Redeploy-time
更復雜和實際的一個例子就是Redeploy(重新部署褪尝,你可以想象一下你開發(fā)時闹获,點擊eclipse的reploy按鈕或者使用idea時按ctrl + F5時的過程)。在從服務器卸載應用程序時河哑,當前的classloader
以及加載的class
在沒有實例引用的情況下避诽,持久代的內存空間會被GC清理并回收。如果應用中有類的實例對當前的classloader
的引用璃谨,那么Permgen
區(qū)的class
將無法被卸載沙庐,導致Permgen
區(qū)的內存一直增加直到出現(xiàn)Permgen space
錯誤鲤妥。
不幸的是,許多第三方庫以及糟糕的資源處理方式(比如:線程拱雏、JDBC驅動程序棉安、文件系統(tǒng)句柄)使得卸載以前使用的類加載器變成了一件不可能的事。反過來就意味著在每次重新部署過程中铸抑,應用程序所有的類的先前版本將仍然駐留在Permgen
區(qū)中贡耽,你的每次部署都將生成幾十甚至幾百M的垃圾。
就以線程和JDBC驅動來說說鹊汛。很多人都會使用線程來處理一下周期性或者耗時較長的任務蒲赂,這個時候一定要注意線程的生命周期問題,你需要確保線程不能比你的應用程序活得還長刁憋。否則滥嘴,如果應用程序已經(jīng)被卸載,線程還在繼續(xù)運行至耻,這個線程通常會維持對應用程序的classloader
的引用若皱,造成的結果就不再多說。多說一句有梆,開發(fā)者有責任處理好這個問題是尖,特別是如果你是第三方庫的提供者的話胚吁,一定要提供線程關閉接口來處理清理工作旁壮。
讓我們想象一個使用JDBC驅動程序連接到關系數(shù)據(jù)庫的示例應用程序谱姓。當應用程序部署到服務器上的時:服務器創(chuàng)建一個classloader
實例來加載應用所有的類(包含相應的JDBC驅動)捶惜。根據(jù)JDBC規(guī)范宋梧,JDBC驅動程序(比如:com.mysql.jdbc.Driver
)會在初始化時將自己注冊到java.sql.DriverManager
中召嘶。該注冊過程中會將驅動程序的一個實例存儲在DriverManager的靜態(tài)字段內蜻懦,代碼可以參考:
// com.mysql.jdbc.Driver源碼
package com.mysql.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can\'t register driver!");
}
}
}
// // // // // // // // // //
// 再看下DriverManager對應代碼
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
public static synchronized void registerDriver(java.sql.Driver driver,DriverAction da) throws SQLException {
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
throw new NullPointerException();
}
}
現(xiàn)在间螟,當從服務器上卸載應用程序的時候夸溶,java.sql.DriverManager
仍將持有那個驅動程序的引用逸吵,進而持有用于加載應用程序的classloader
的一個實例的引用。這個classloader
現(xiàn)在仍然引用著應用程序的所有類缝裁。如果此程序啟動時需要加載2000個類扫皱,占用約10MB永久代(PermGen)內存,那么只需要5~10次重新部署捷绑,就會將默認大小的永久代(PermGen)塞滿韩脑,然后就會觸發(fā)java.lang.OutOfMemoryError: PermGen space
錯誤并崩潰。
解決方案
① 解決初始化時的OutOfMemoryError
當在應用程序啟動期間觸發(fā)由于PermGen
耗盡引起的OutOfMemoryError
時粹污,解決方案很簡單段多。 應用程序需要更多的空間來加載所有的類到PermGen
區(qū)域,所以我們只需要增加它的大小壮吩。 為此进苍,請更改應用程序啟動配置加缘,并添加(或增加,如果存在)-XX:MaxPermSize參數(shù)觉啊,類似于以下示例:
java -XX:MaxPermSize=512m com.yourcompany.YourClass
② 解決Redeploy
時的OutOfMemoryError
分析dump文件:首先拣宏,找出引用在哪里被持有;其次柄延,給你的web應用程序添加一個關閉的hook蚀浆,或者在應用程序卸載后移除引用缀程。你可以使用如下命令導出dump文件:
jmap -dump:format=b,file=dump.hprof <process-id>
如果是你自己代碼的問題請及時修改搜吧,如果是第三方庫,請試著搜索一下是否存在"關閉"接口杨凑,如果沒有給開發(fā)者提交一個bug或者issue吧滤奈。
③ 解決運行時OutOfMemoryError
首先你需要檢查是否允許GC從PermGen
卸載類,JVM的標準配置相當保守撩满,只要類一創(chuàng)建蜒程,即使已經(jīng)沒有實例引用它們,其仍將保留在內存中伺帘,特別是當應用程序需要動態(tài)創(chuàng)建大量的類但其生命周期并不長時昭躺,允許JVM卸載類對應用大有助益,你可以通過在啟動腳本中添加以下配置參數(shù)來實現(xiàn):
-XX:+CMSClassUnloadingEnabled
默認情況下伪嫁,這個配置是未啟用的领炫,如果你啟用它,GC將掃描PermGen
區(qū)并清理已經(jīng)不再使用的類张咳。但請注意帝洪,這個配置只在UseConcMarkSweepGC
的情況下生效,如果你使用其他GC算法脚猾,比如:ParallelGC
或者Serial GC
時葱峡,這個配置無效。所以使用以上配置時龙助,請配合:
-XX:+UseConcMarkSweepGC
如果你已經(jīng)確保JVM可以卸載類砰奕,但是仍然出現(xiàn)內存溢出問題,那么你應該繼續(xù)分析dump文件提鸟,使用以下命令生成dump文件:
jmap -dump:file=dump.hprof,format=b <process-id>
當你拿到生成的堆轉儲文件军援,并利用像Eclipse Memory Analyzer Toolkit這樣的工具來尋找應該卸載卻沒被卸載的類加載器,然后對該類加載器加載的類進行排查沽一,找到可疑對象盖溺,分析使用或者生成這些類的代碼,查找產生問題的根源并解決它铣缠。
4烘嘱、java.lang.OutOfMemoryError:Metaspace
前文已經(jīng)提過昆禽,PermGen
區(qū)域用于存儲類的名稱和字段,類的方法蝇庭,方法的字節(jié)碼醉鳖,常量池,JIT優(yōu)化等哮内,但從Java8開始盗棵,Java中的內存模型發(fā)生了重大變化:引入了稱為Metaspace
的新內存區(qū)域,而刪除了PermGen
區(qū)域北发。請注意:不是簡單的將PermGen
區(qū)所存儲的內容直接移到Metaspace
區(qū)纹因,PermGen
區(qū)中的某些部分,已經(jīng)移動到了普通堆里面琳拨。
原因分析
Java8做出如此改變的原因包括但不限于:
- 應用程序所需要的
PermGen
區(qū)大小很難預測瞭恰,設置太小會觸發(fā)PermGen OutOfMemoryError
錯誤,過度設置導致資源浪費狱庇。 - 提升GC性能惊畏,在HotSpot中的每個垃圾收集器需要專門的代碼來處理存儲在
PermGen
中的類的元數(shù)據(jù)信息。從PermGen
分離類的元數(shù)據(jù)信息到Metaspace
密任,由于Metaspace
的分配具有和Java Heap
相同的地址空間颜启,因此Metaspace
和Java Heap
可以無縫的管理,而且簡化了FullGC
的過程浪讳,以至將來可以并行的對元數(shù)據(jù)信息進行垃圾收集缰盏,而沒有GC暫停。 - 支持進一步優(yōu)化驻债,比如:G1并發(fā)類的卸載乳规,也算為將來做準備吧
正如你所看到的,元空間大小的要求取決于加載的類的數(shù)量以及這種類聲明的大小合呐。 所以很容易看到java.lang.OutOfMemoryError: Metaspace
主要原因:太多的類或太大的類加載到元空間暮的。
示例
正如上文中所解釋的,元空間的使用與加載到JVM中的類的數(shù)量密切相關淌实。 下面的代碼是最簡單的例子:
public class Metaspace {
static javassist.ClassPool cp = javassist.ClassPool.getDefault();
public static void main(String[] args) throws Exception{
for (int i = 0; ; i++) {
Class c = cp.makeClass("eu.plumbr.demo.Generated" + i).toClass();
System.out.println(i);
}
}
}
程序運行中不停的生成新類冻辩,所有的這些類的定義將被加載到Metaspace
區(qū),直到空間被完全占用并且拋出java.lang.OutOfMemoryError:Metaspace
拆祈。當使用-XX:MaxMetaspaceSize = 32m
啟動時恨闪,大約加載30000多個類時就會死機。
31023
31024
Exception in thread "main" javassist.CannotCompileException: by java.lang.OutOfMemoryError: Metaspace
at javassist.ClassPool.toClass(ClassPool.java:1170)
at javassist.ClassPool.toClass(ClassPool.java:1113)
at javassist.ClassPool.toClass(ClassPool.java:1071)
at javassist.CtClass.toClass(CtClass.java:1275)
at cn.moondev.book.Metaspace.main(Metaspace.java:12)
.....
解決方案
第一個解決方案是顯而易見的放坏,既然應用程序會耗盡內存中的Metaspace
區(qū)空間咙咽,那么應該增加其大小,更改啟動配置增加如下參數(shù):
// 告訴JVM:Metaspace允許增長到512淤年,然后才能拋出異常
-XX:MaxMetaspaceSize = 512m
另一個方法就是刪除此參數(shù)來完全解除對Metaspace
大小的限制(默認是沒有限制的)钧敞。默認情況下蜡豹,對于64位服務器端JVM,MetaspaceSize默認大小是21M(初始限制值)溉苛,一旦達到這個限制值镜廉,F(xiàn)ullGC將被觸發(fā)進行類卸載,并且這個限制值將會被重置愚战,新的限制值依賴于Metaspace
的剩余容量娇唯。如果沒有足夠空間被釋放,這個限制值將會上升寂玲,反之亦然塔插。在技術上Metaspace
的尺寸可以增長到交換空間,而這個時候本地內存分配將會失敻易隆(更具體的分析佑淀,可以參考:Java PermGen 去哪里了?)留美。
你可以通過修改各種啟動參數(shù)來“快速修復”這些內存溢出錯誤彰檬,但你需要正確區(qū)分你是否只是推遲或者隱藏了java.lang.OutOfMemoryError
的癥狀。如果你的應用程序確實存在內存泄漏或者本來就加載了一些不合理的類谎砾,那么所有這些配置都只是推遲問題出現(xiàn)的時間而已逢倍,實際也不會改善任何東西。
5景图、java.lang.OutOfMemoryError:Unable to create new native thread
一個思考線程的方法是將線程看著是執(zhí)行任務的工人较雕,如果你只有一個工人,那么他同時只能執(zhí)行一項任務挚币,但如果你有十幾個工人亮蒋,就可以同時完成你幾個任務。就像這些工人都在物理世界妆毕,JVM中的線程完成自己的工作也是需要一些空間的慎玖,當有足夠多的線程卻沒有那么多的空間時就會像這樣:
出現(xiàn)java.lang.OutOfMemoryError:Unable to create new native thread
就意味著Java應用程序已達到其可以啟動線程數(shù)量的極限了。
原因分析
當JVM向OS請求創(chuàng)建一個新線程時笛粘,而OS卻無法創(chuàng)建新的native線程時就會拋出Unable to create new native thread
錯誤趁怔。一臺服務器可以創(chuàng)建的線程數(shù)依賴于物理配置和平臺,建議運行下文中的示例代碼來測試找出這些限制薪前∪笈總體上來說,拋出此錯誤會經(jīng)過以下幾個階段:
- 運行在JVM內的應用程序請求創(chuàng)建一個新的線程
- JVM向OS請求創(chuàng)建一個新的native線程
- OS嘗試創(chuàng)建一個新的native線程示括,這時需要分配內存給新的線程
- OS拒絕分配內存給線程铺浇,因為32位Java進程已經(jīng)耗盡內存地址空間(2-4GB內存地址已被命中)或者OS的虛擬內存已經(jīng)完全耗盡
-
Unable to create new native thread
錯誤將被拋出
示例
下面的示例不能的創(chuàng)建并啟動新的線程。當代碼運行時垛膝,很快達到OS的線程數(shù)限制鳍侣,并拋出Unable to create new native thread
錯誤裁着。
while(true){
new Thread(new Runnable(){
public void run() {
try {
Thread.sleep(10000000);
} catch(InterruptedException e) { }
}
}).start();
}
解決方案
有時,你可以通過在OS級別增加線程數(shù)限制來繞過這個錯誤拱她。如果你限制了JVM可在用戶空間創(chuàng)建的線程數(shù)二驰,那么你可以檢查并增加這個限制:
// macOS 10.12上執(zhí)行
$ ulimit -u
709
當你的應用程序產生成千上萬的線程,并拋出此異常秉沼,表示你的程序已經(jīng)出現(xiàn)了很嚴重的編程錯誤桶雀,我不覺得應該通過修改參數(shù)來解決這個問題,不管是OS級別的參數(shù)還是JVM啟動參數(shù)唬复。更可取的辦法是分析你的應用是否真的需要創(chuàng)建如此多的線程來完成任務矗积?是否可以使用線程池或者說線程池的數(shù)量是否合適?是否可以更合理的拆分業(yè)務來實現(xiàn).....
6敞咧、java.lang.OutOfMemoryError:Out of swap space?
Java應用程序在啟動時會指定所需要的內存大小棘捣,可以通過-Xmx
和其他類似的啟動參數(shù)來指定。在JVM請求的總內存大于可用物理內存的情況下休建,操作系統(tǒng)會將內存中的數(shù)據(jù)交換到磁盤上去乍恐。
Out of swap space?
表示交換空間也將耗盡,并且由于缺少物理內存和交換空間测砂,再次嘗試分配內存也將失敗茵烈。
原因分析
當應用程序向JVM native heap請求分配內存失敗并且native heap也即將耗盡時,JVM會拋出Out of swap space
錯誤砌些。該錯誤消息中包含分配失敗的大形赝丁(以字節(jié)為單位)和請求失敗的原因。
Native Heap Memory是JVM內部使用的Memory存璃,這部分的Memory可以通過JDK提供的JNI的方式去訪問仑荐,這部分Memory效率很高,但是管理需要自己去做纵东,如果沒有把握最好不要使用粘招,以防出現(xiàn)內存泄露問題。JVM 使用Native Heap Memory用來優(yōu)化代碼載入(JTI代碼生成)篮迎,臨時對象空間申請男图,以及JVM內部的一些操作。
這個問題往往發(fā)生在Java進程已經(jīng)開始交換的情況下甜橱,現(xiàn)代的GC算法已經(jīng)做得足夠好了逊笆,當時當面臨由于交換引起的延遲問題時,GC暫停的時間往往會讓大多數(shù)應用程序不能容忍岂傲。
java.lang.OutOfMemoryError:Out of swap space?
往往是由操作系統(tǒng)級別的問題引起的难裆,例如:
- 操作系統(tǒng)配置的交換空間不足。
- 系統(tǒng)上的另一個進程消耗所有內存資源。
還有可能是本地內存泄漏導致應用程序失敗乃戈,比如:應用程序調用了native code連續(xù)分配內存褂痰,但卻沒有被釋放。
解決方案
解決這個問題有幾個辦法症虑,通常最簡單的方法就是增加交換空間缩歪,不同平臺實現(xiàn)的方式會有所不同,比如在Linux下可以通過如下命令實現(xiàn):
# 原作者使用谍憔,由于我手里并沒有Linux環(huán)境匪蝙,所以并未測試
# 創(chuàng)建并附加一個大小為640MB的新交換文件
swapoff -a
dd if=/dev/zero of=swapfile bs=1024 count=655360
mkswap swapfile
swapon swapfile
Java GC會掃描內存中的數(shù)據(jù),如果是對交換空間運行垃圾回收算法會使GC暫停的時間增加幾個數(shù)量級习贫,因此你應該慎重考慮使用上文增加交換空間的方法逛球。
如果你的應用程序部署在JVM需要同其他進程激烈競爭獲取資源的物理機上,建議將服務隔離到單獨的虛擬機中
但在許多情況下苫昌,您唯一真正可行的替代方案是:
- 升級機器以包含更多內存
- 優(yōu)化應用程序以減少其內存占用
當您轉向優(yōu)化路徑時颤绕,使用內存轉儲分析程序來檢測內存中的大分配是一個好的開始。
7祟身、java.lang.OutOfMemoryError:Requested array size exceeds VM limit
Java對應用程序可以分配的最大數(shù)組大小有限制奥务。不同平臺限制有所不同,但通常在1到21億個元素之間月而。
當你遇到Requested array size exceeds VM limit
錯誤時汗洒,意味著你的應用程序試圖分配大于Java虛擬機可以支持的數(shù)組。
原因分析
該錯誤由JVM中的native code
拋出父款。 JVM在為數(shù)組分配內存之前,會執(zhí)行特定于平臺的檢查:分配的數(shù)據(jù)結構是否在此平臺中是可尋址的瞻凤。
你很少見到這個錯誤是因為Java數(shù)組的索引是int類型憨攒。 Java中的最大正整數(shù)為2 ^ 31 - 1 = 2,147,483,647。 并且平臺特定的限制可以非常接近這個數(shù)字阀参,例如:我的環(huán)境上(64位macOS肝集,運行Jdk1.8)可以初始化數(shù)組的長度高達2,147,483,645(Integer.MAX_VALUE-2)。如果再將數(shù)組的長度增加1到Integer.MAX_VALUE-1會導致熟悉的OutOfMemoryError:
Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
但是蛛壳,在使用OpenJDK 6的32位Linux上杏瞻,在分配具有大約11億個元素的數(shù)組時,您將遇到Requested array size exceeded VM limit
的錯誤衙荐。 要理解你的特定環(huán)境的限制捞挥,運行下文中描述的小測試程序。
示例
for (int i = 3; i >= 0; i--) {
try {
int[] arr = new int[Integer.MAX_VALUE-i];
System.out.format("Successfully initialized an array with %,d elements.\n", Integer.MAX_VALUE-i);
} catch (Throwable t) {
t.printStackTrace();
}
}
該示例重復四次忧吟,并在每個回合中初始化一個長原語數(shù)組砌函。 該程序嘗試初始化的數(shù)組的大小在每次迭代時增加1,最終達到Integer.MAX_VALUE。 現(xiàn)在讹俊,當使用Hotspot 7在64位Mac OS X上啟動代碼片段時垦沉,應該得到類似于以下內容的輸出:
java.lang.OutOfMemoryError: Java heap space
at eu.plumbr.demo.ArraySize.main(ArraySize.java:8)
java.lang.OutOfMemoryError: Java heap space
at eu.plumbr.demo.ArraySize.main(ArraySize.java:8)
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at eu.plumbr.demo.ArraySize.main(ArraySize.java:8)
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at eu.plumbr.demo.ArraySize.main(ArraySize.java:8)
注意,在出現(xiàn)Requested array size exceeded VM limit
之前仍劈,出現(xiàn)了更熟悉的java.lang.OutOfMemoryError: Java heap space
厕倍。 這是因為初始化2 ^ 31-1個元素的數(shù)組需要騰出8G的內存空間,大于JVM使用的默認值贩疙。
解決方案
java.lang.OutOfMemoryError:Requested array size exceeds VM limit
可能會在以下任一情況下出現(xiàn):
- 數(shù)組增長太大绑青,最終大小在平臺限制和
Integer.MAX_INT
之間 - 你有意分配大于
2 ^ 31-1
個元素的數(shù)組
在第一種情況下,檢查你的代碼庫屋群,看看你是否真的需要這么大的數(shù)組闸婴。也許你可以減少數(shù)組的大小,或者將數(shù)組分成更小的數(shù)據(jù)塊芍躏,然后分批處理數(shù)據(jù)邪乍。
在第二種情況下,記住Java數(shù)組是由int索引的对竣。因此庇楞,當在平臺中使用標準數(shù)據(jù)結構時,數(shù)組不能超過2 ^ 31-1個元素否纬。事實上吕晌,在編譯時就會出錯:error:integer number too large
。
8临燃、Out of memory:Kill process or sacrifice child
為了理解這個錯誤睛驳,我們需要補充一點操作系統(tǒng)的基礎知識。操作系統(tǒng)是建立在進程的概念之上膜廊,這些進程在內核中作業(yè)乏沸,其中有一個非常特殊的進程,名叫“內存殺手(Out of memory killer)”爪瓜。當內核檢測到系統(tǒng)內存不足時蹬跃,OOM killer被激活,然后選擇一個進程殺掉铆铆。哪一個進程這么倒霉呢蝶缀?選擇的算法和想法都很樸實:誰占用內存最多,誰就被干掉薄货。如果你對OOM Killer感興趣的話翁都,建議你閱讀參考資料2中的文章。
當可用虛擬虛擬內存(包括交換空間)消耗到讓整個操作系統(tǒng)面臨風險時菲驴,就會產生Out of memory:Kill process or sacrifice child
錯誤荐吵。在這種情況下,OOM Killer會選擇“流氓進程”并殺死它。
原因分析
默認情況下先煎,Linux內核允許進程請求比系統(tǒng)中可用內存更多的內存贼涩,但大多數(shù)進程實際上并沒有使用完他們所分配的內存。這就跟現(xiàn)實生活中的寬帶運營商類似薯蝎,他們向所有消費者出售一個100M的帶寬遥倦,遠遠超過用戶實際使用的帶寬,一個10G的鏈路可以非常輕松的服務100個(10G/100M)用戶占锯,但實際上寬帶運行商往往會把10G鏈路用于服務150人或者更多袒哥,以便讓鏈路的利用率更高,畢竟空閑在那兒也沒什么意義消略。
Linux內核采用的機制跟寬帶運營商差不多堡称,一般情況下都沒有問題,但當大多數(shù)應用程序都消耗完自己的內存時艺演,麻煩就來了却紧,因為這些應用程序的內存需求加起來超出了物理內存(包括 swap)的容量,內核(OOM killer)必須殺掉一些進程才能騰出空間保障系統(tǒng)正常運行胎撤。就如同上面的例子中晓殊,如果150人都占用100M的帶寬,那么總的帶寬肯定超過了10G這條鏈路能承受的范圍伤提。
示例
當你在Linux上運行如下代碼:
public static void main(String[] args){
List<int[]> l = new java.util.ArrayList();
for (int i = 10000; i < 100000; i++) {
try {
l.add(new int[100000000]);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
在Linux的系統(tǒng)日志中/var/log/kern.log
會出現(xiàn)以下日志:
Jun 4 07:41:59 plumbr kernel: [70667120.897649] Out of memory: Kill process 29957 (java) score 366 or sacrifice child
Jun 4 07:41:59 plumbr kernel: [70667120.897701] Killed process 29957 (java) total-vm:2532680kB, anon-rss:1416508kB, file-rss:0kB
注意:你可能需要調整交換文件和堆大小巫俺,否則你將很快見到熟悉的Java heap space
異常。在原作者的測試用例中肿男,使用-Xmx2g
指定的2g堆介汹,并具有以下交換配置:
# 注意:原作者使用,由于我手里并沒有Linux環(huán)境次伶,所以并未測試
swapoff -a
dd if=/dev/zero of=swapfile bs=1024 count=655360
mkswap swapfile
swapon swapfile
解決方案
解決這個問題最有效也是最直接的方法就是升級內存痴昧,其他方法諸如:調整OOM Killer配置、水平擴展應用冠王,將內存的負載分攤到若干小實例上..... 我們不建議的做法是增加交換空間,具體原因已經(jīng)在前文說過舌镶。參考資料②中詳細的介紹了怎樣微調OOM Killer配置以及OOM Killer選擇進程算法的實現(xiàn)柱彻,建議你參考閱讀。
參考資料:
① 想要了解更多PermGen與Metaspace的內容推薦你閱讀:
② 如果你對OOM Killer感興趣的話餐胀,強烈建議你閱讀這篇文章: