個人職業生涯中見過數以千計的內存溢出異常均與下文中的8種狀況相關。本文分析什麼狀況會致使這些異常出現,提供示例代碼的同時爲您提供解決指南。
Nikita Salnikov-Tarnovski
Plumbr Co-Founder and VP of Engineering
本文內容來源於Plumbr,對原文內容有刪減和補充java
這也許是目前最爲完整的Java OOM異常的解決指南。mysql
java.lang.OutOfMemoryError:Java heap space
Java應用程序在啓動時會指定所須要的內存大小,它被分割成兩個不一樣的區域:Heap space(堆空間)
和Permgen(永久代)
:linux
JVM內存模型示意圖web
這兩個區域的大小能夠在JVM(Java虛擬機)啓動時經過參數-Xmx
和-XX:MaxPermSize
設置,若是你沒有顯式設置,則將使用特定平臺的默認值。算法
當應用程序試圖向堆空間添加更多的數據,但堆卻沒有足夠的空間來容納這些數據時,將會觸發java.lang.OutOfMemoryError: Java heap space
異常。須要注意的是:即便有足夠的物理內存可用,只要達到堆空間設置的大小限制,此異常仍然會被觸發。sql
觸發java.lang.OutOfMemoryError: Java heap space
最多見的緣由就是應用程序須要的堆空間是XXL號的,可是JVM提供的倒是S號。解決方法也很簡單,提供更大的堆空間便可。除了前面的因素還有更復雜的成因:數據庫
java.lang.OutOfMemoryError: Java heap space
異常。java.lang.OutOfMemoryError: Java heap space
錯誤。①、簡單示例編程
首先看一個很是簡單的示例,下面的代碼試圖建立2 x 1024 x 1024個元素的整型數組,當你嘗試編譯並指定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中,當開發者建立一個新對象(好比:new Integer(5)
)時,不須要本身開闢內存空間,而是把它交給JVM。在應用程序整個生命週期類,JVM負責檢查哪些對象可用,哪些對象未被使用。未使用對象將被丟棄,其佔用的內存也將被回收,這一過程被稱爲垃圾回收。JVM負責垃圾回收的模塊集合被稱爲垃圾回收器(GC
)。
Java的內存自動管理機制依賴於GC按期查找未使用對象並刪除它們。Java中的內存泄漏是因爲GC沒法識別一些已經再也不使用的對象,而這些未使用的對象一直留在堆空間中,這種堆積最終會致使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循環,會將10000個元素添加到緩存中。後面的while循環中,因爲key已經存在於緩存中,緩存的大小將一直會維持在10000。但事實真的如此嗎?因爲Key
實體沒有實現equals()
方法,致使for循環中每次執行m.containsKey(new Key(i))
結果均爲false
,其結果就是HashMap
中的元素將一直增長。
隨着時間的推移,愈來愈多的Key
對象進入堆空間且不能被垃圾收集器回收(m爲局部變量,GC會認爲這些對象一直可用,因此不會回收),直到全部的堆空間被佔用,最後拋出java.lang.OutOfMemoryError:Java heap space
。
上面的代碼直接運行可能好久也不會拋出異常,能夠在啓動時使用-Xmx參數,設置堆內存大小,或者在for循環後打印HashMap的大小,執行後會發現HashMap的size一直再增加。
解決方法也很是簡單,只要Key
實現本身的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
錯誤出現的時間而已,並未解決這個隱患。除此以外,垃圾收集器在GC時,應用程序會中止運行直到GC完成,而增長堆空間也會致使GC時間延長,進而影響程序的吞吐量。
若是你想徹底解決這個問題,那就好好提高本身的編程技能吧,固然運用好Debuggers, profilers, heap dump analyzers
等工具,可讓你的程序最大程度的避免內存泄漏問題。
java.lang.OutOfMemoryError:GC overhead limit exceeded
Java運行時環境(JRE
)包含一個內置的垃圾回收進程,而在許多其餘的編程語言中,開發者須要手動分配和釋放內存。
Java應用程序只須要開發者分配內存,每當在內存中特定的空間再也不使用時,一個單獨的垃圾收集進程會清空這些內存空間。垃圾收集器怎樣檢測內存中的某些空間再也不使用已經超出本文的範圍,但你只須要相信GC能夠作好這些工做便可。
默認狀況下,當應用程序花費超過98%的時間用來作GC而且回收了不到2%的堆內存時,會拋出java.lang.OutOfMemoryError:GC overhead limit exceeded
錯誤。具體的表現就是你的應用幾乎耗盡全部可用內存,而且GC屢次均未能清理乾淨。
java.lang.OutOfMemoryError:GC overhead limit exceeded
錯誤是一個信號,示意你的應用程序在垃圾收集上花費了太多時間但卻沒有什麼卵用。默認超過98%的時間用來作GC卻回收了不到2%的內存時將會拋出此錯誤。那若是沒有此限制會發生什麼呢?GC進程將被重啓,100%的CPU將用於GC,而沒有CPU資源用於其餘正常的工做。若是一個工做原本只須要幾毫秒便可完成,如今卻須要幾分鐘才能完成,我想這種結果誰都沒有辦法接受。
因此java.lang.OutOfMemoryError:GC overhead limit exceeded
也能夠看作是一個fail-fast(快速失敗)
實戰的實例。
下面的代碼初始化一個map
並在無限循環中不停的添加鍵值對,運行後將會拋出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"); } } }
正如你所預料的那樣,程序不能正常的結束,事實上,當咱們使用以下參數啓動程序時:
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"
錯誤已經被默認的異常處理程序捕獲,而且沒有任何錯誤的堆棧信息輸出。
以上這些變化能夠說明,在資源有限的狀況下,你根本沒法沒法預測你的應用是怎樣掛掉的,何時會掛掉,因此在開發時,你不能僅僅保證本身的應用程序在特定的環境下正常運行。
首先是一個毫無誠意的解決方案,若是你僅僅是不想看到java.lang.OutOfMemoryError:GC overhead limit exceeded
的錯誤信息,能夠在應用程序啓動時添加以下JVM參數:
-XX:-UseGCOverheadLimit
可是強烈建議不要使用這個選項,由於這樣並無解決任何問題,只是推遲了錯誤出現的時間,錯誤信息也變成了咱們更熟悉的java.lang.OutOfMemoryError: Java heap space
而已。
另外一個解決方案,若是你的應用程序確實內存不足,增長堆內存會解決GC overhead limit
問題,就以下面這樣,給你的應用程序1G的堆內存:
java -Xmx1024m com.yourcompany.YourClass
但若是你想確保你已經解決了潛在的問題,而不是掩蓋java.lang.OutOfMemoryError: GC overhead limit exceeded
錯誤,那麼你不該該僅止步於此。你要記得還有profilers
和memory dump analyzers
這些工具,你須要花費更多的時間和精力來查找問題。還有一點須要注意,這些工具在Java運行時有顯著的開銷,所以不建議在生產環境中使用。
java.lang.OutOfMemoryError:Permgen space
Java中堆空間是JVM管理的最大一塊內存空間,能夠在JVM啓動時指定堆空間的大小,其中堆被劃分紅兩個不一樣的區域:新生代(Young)和老年代(Tenured),新生代又被劃分爲3個區域:Eden
、From Survivor
、To Survivor
,以下圖所示。
圖片來源:併發編程網
java.lang.OutOfMemoryError: PermGen space
錯誤就代表持久代所在區域的內存已被耗盡。
要理解java.lang.OutOfMemoryError: PermGen space
出現的緣由,首先須要理解Permanent Generation Space
的用處是什麼。持久代主要存儲的是每一個類的信息,好比:類加載器引用、運行時常量池(全部常量、字段引用、方法引用、屬性)、字段(Field)數據、方法(Method)數據、方法代碼、方法字節碼等等。咱們能夠推斷出,PermGen
的大小取決於被加載類的數量以及類的大小。
所以,咱們能夠得出出現java.lang.OutOfMemoryError: PermGen space
錯誤的緣由是:太多的類或者太大的類被加載到permanent generation
(持久代)。
①、最簡單的示例
正如前面所描述的,PermGen
的使用與加載到JVM類的數量有密切關係,下面是一個最簡單的示例:
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參數:-XX:MaxPermSize=5m
,值越小越好。須要注意的是JDK8已經徹底移除持久代空間,取而代之的是元空間(Metaspace
),因此示例最好的JDK1.7或者1.6下運行。
代碼在運行時不停的生成類並加載到持久代中,直到撐滿持久代內存空間,最後拋出java.lang.OutOfMemoryError:Permgen space
。代碼中類的生成使用了javassist
庫。
②、Redeploy-time
更復雜和實際的一個例子就是Redeploy(從新部署,你能夠想象一下你開發時,點擊eclipse的reploy按鈕或者使用idea時按ctrl + F5時的過程)。在從服務器卸載應用程序時,當前的classloader
以及加載的class
在沒有實例引用的狀況下,持久代的內存空間會被GC清理並回收。若是應用中有類的實例對當前的classloader
的引用,那麼Permgen
區的class
將沒法被卸載,致使Permgen
區的內存一直增長直到出現Permgen space
錯誤。
不幸的是,許多第三方庫以及糟糕的資源處理方式(好比:線程、JDBC驅動程序、文件系統句柄)使得卸載之前使用的類加載器變成了一件不可能的事。反過來就意味着在每次從新部署過程當中,應用程序全部的類的先前版本將仍然駐留在Permgen
區中,你的每次部署都將生成幾十甚至幾百M的垃圾。
就以線程和JDBC驅動來講說。不少人都會使用線程來處理一下週期性或者耗時較長的任務,這個時候必定要注意線程的生命週期問題,你須要確保線程不能比你的應用程序活得還長。不然,若是應用程序已經被卸載,線程還在繼續運行,這個線程一般會維持對應用程序的classloader
的引用,形成的結果就再也不多說。多說一句,開發者有責任處理好這個問題,特別是若是你是第三方庫的提供者的話,必定要提供線程關閉接口來處理清理工做。
讓咱們想象一個使用JDBC驅動程序鏈接到關係數據庫的示例應用程序。當應用程序部署到服務器上的時:服務器建立一個classloader
實例來加載應用全部的類(包含相應的JDBC驅動)。根據JDBC規範,JDBC驅動程序(好比:com.mysql.jdbc.Driver
)會在初始化時將本身註冊到java.sql.DriverManager
中。該註冊過程當中會將驅動程序的一個實例存儲在DriverManager的靜態字段內,代碼能夠參考:
// 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(); } }
如今,當從服務器上卸載應用程序的時候,java.sql.DriverManager
仍將持有那個驅動程序的引用,進而持有用於加載應用程序的classloader
的一個實例的引用。這個classloader
如今仍然引用着應用程序的全部類。若是此程序啓動時須要加載2000個類,佔用約10MB永久代(PermGen)內存,那麼只須要5~10次從新部署,就會將默認大小的永久代(PermGen)塞滿,而後就會觸發java.lang.OutOfMemoryError: PermGen space
錯誤並崩潰。
① 解決初始化時的OutOfMemoryError
當在應用程序啓動期間觸發因爲PermGen
耗盡引發的OutOfMemoryError
時,解決方案很簡單。 應用程序須要更多的空間來加載全部的類到PermGen
區域,因此咱們只須要增長它的大小。 爲此,請更改應用程序啓動配置,並添加(或增長,若是存在)-XX:MaxPermSize參數,相似於如下示例:
java -XX:MaxPermSize=512m com.yourcompany.YourClass
② 解決Redeploy
時的OutOfMemoryError
分析dump文件:首先,找出引用在哪裏被持有;其次,給你的web應用程序添加一個關閉的hook,或者在應用程序卸載後移除引用。你可使用以下命令導出dump文件:
jmap -dump:format=b,file=dump.hprof <process-id>
若是是你本身代碼的問題請及時修改,若是是第三方庫,請試着搜索一下是否存在"關閉"接口,若是沒有給開發者提交一個bug或者issue吧。
③ 解決運行時OutOfMemoryError
首先你須要檢查是否容許GC從PermGen
卸載類,JVM的標準配置至關保守,只要類一建立,即便已經沒有實例引用它們,其仍將保留在內存中,特別是當應用程序須要動態建立大量的類但其生命週期並不長時,容許JVM卸載類對應用大有助益,你能夠經過在啓動腳本中添加如下配置參數來實現:
-XX:+CMSClassUnloadingEnabled
默認狀況下,這個配置是未啓用的,若是你啓用它,GC將掃描PermGen
區並清理已經再也不使用的類。但請注意,這個配置只在UseConcMarkSweepGC
的狀況下生效,若是你使用其餘GC算法,好比:ParallelGC
或者Serial GC
時,這個配置無效。因此使用以上配置時,請配合:
-XX:+UseConcMarkSweepGC
若是你已經確保JVM能夠卸載類,可是仍然出現內存溢出問題,那麼你應該繼續分析dump文件,使用如下命令生成dump文件:
jmap -dump:file=dump.hprof,format=b <process-id>
當你拿到生成的堆轉儲文件,並利用像Eclipse Memory Analyzer Toolkit這樣的工具來尋找應該卸載卻沒被卸載的類加載器,而後對該類加載器加載的類進行排查,找到可疑對象,分析使用或者生成這些類的代碼,查找產生問題的根源並解決它。
java.lang.OutOfMemoryError:Metaspace
前文已經提過,PermGen
區域用於存儲類的名稱和字段,類的方法,方法的字節碼,常量池,JIT優化等,但從Java8開始,Java中的內存模型發生了重大變化:引入了稱爲Metaspace
的新內存區域,而刪除了PermGen
區域。請注意:不是簡單的將PermGen
區所存儲的內容直接移到Metaspace
區,PermGen
區中的某些部分,已經移動到了普通堆裏面。
OOM-example-metaspace,圖片來源:Plumbr
Java8作出如此改變的緣由包括但不限於:
PermGen
區大小很難預測,設置過小會觸發PermGen OutOfMemoryError
錯誤,過分設置致使資源浪費。PermGen
中的類的元數據信息。從PermGen
分離類的元數據信息到Metaspace
,因爲Metaspace
的分配具備和Java Heap
相同的地址空間,所以Metaspace
和Java Heap
能夠無縫的管理,並且簡化了FullGC
的過程,以致未來能夠並行的對元數據信息進行垃圾收集,而沒有GC暫停。正如你所看到的,元空間大小的要求取決於加載的類的數量以及這種類聲明的大小。 因此很容易看到java.lang.OutOfMemoryError: Metaspace
主要緣由:太多的類或太大的類加載到元空間。
正如上文中所解釋的,元空間的使用與加載到JVM中的類的數量密切相關。 下面的代碼是最簡單的例子:
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
區,直到空間被徹底佔用而且拋出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
區空間,那麼應該增長其大小,更改啓動配置增長以下參數:
// 告訴JVM:Metaspace容許增加到512,而後才能拋出異常 -XX:MaxMetaspaceSize = 512m
另外一個方法就是刪除此參數來徹底解除對Metaspace
大小的限制(默認是沒有限制的)。默認狀況下,對於64位服務器端JVM,MetaspaceSize默認大小是21M(初始限制值),一旦達到這個限制值,FullGC將被觸發進行類卸載,而且這個限制值將會被重置,新的限制值依賴於Metaspace
的剩餘容量。若是沒有足夠空間被釋放,這個限制值將會上升,反之亦然。在技術上Metaspace
的尺寸能夠增加到交換空間,而這個時候本地內存分配將會失敗(更具體的分析,能夠參考:Java PermGen 去哪裏了?)。
你能夠經過修改各類啓動參數來「快速修復」這些內存溢出錯誤,但你須要正確區分你是否只是推遲或者隱藏了java.lang.OutOfMemoryError
的症狀。若是你的應用程序確實存在內存泄漏或者原本就加載了一些不合理的類,那麼全部這些配置都只是推遲問題出現的時間而已,實際也不會改善任何東西。
java.lang.OutOfMemoryError:Unable to create new native thread
一個思考線程的方法是將線程看着是執行任務的工人,若是你只有一個工人,那麼他同時只能執行一項任務,但若是你有十幾個工人,就能夠同時完成你幾個任務。就像這些工人都在物理世界,JVM中的線程完成本身的工做也是須要一些空間的,當有足夠多的線程卻沒有那麼多的空間時就會像這樣:
圖片來源:Plumbr
出現java.lang.OutOfMemoryError:Unable to create new native thread
就意味着Java應用程序已達到其能夠啓動線程數量的極限了。
當JVM向OS請求建立一個新線程時,而OS卻沒法建立新的native線程時就會拋出Unable to create new native thread
錯誤。一臺服務器能夠建立的線程數依賴於物理配置和平臺,建議運行下文中的示例代碼來測試找出這些限制。整體上來講,拋出此錯誤會通過如下幾個階段:
Unable to create new native thread
錯誤將被拋出下面的示例不能的建立並啓動新的線程。當代碼運行時,很快達到OS的線程數限制,並拋出Unable to create new native thread
錯誤。
while(true){ new Thread(new Runnable(){ public void run() { try { Thread.sleep(10000000); } catch(InterruptedException e) { } } }).start(); }
有時,你能夠經過在OS級別增長線程數限制來繞過這個錯誤。若是你限制了JVM可在用戶空間建立的線程數,那麼你能夠檢查並增長這個限制:
// macOS 10.12上執行 $ ulimit -u 709
當你的應用程序產生成千上萬的線程,並拋出此異常,表示你的程序已經出現了很嚴重的編程錯誤,我不以爲應該經過修改參數來解決這個問題,無論是OS級別的參數仍是JVM啓動參數。更可取的辦法是分析你的應用是否真的須要建立如此多的線程來完成任務?是否可使用線程池或者說線程池的數量是否合適?是否能夠更合理的拆分業務來實現.....
java.lang.OutOfMemoryError:Out of swap space?
Java應用程序在啓動時會指定所須要的內存大小,能夠經過-Xmx
和其餘相似的啓動參數來指定。在JVM請求的總內存大於可用物理內存的狀況下,操做系統會將內存中的數據交換到磁盤上去。
圖片來源:plumbr
Out of swap space?
表示交換空間也將耗盡,而且因爲缺乏物理內存和交換空間,再次嘗試分配內存也將失敗。
當應用程序向JVM native heap請求分配內存失敗而且native heap也即將耗盡時,JVM會拋出Out of swap space
錯誤。該錯誤消息中包含分配失敗的大小(以字節爲單位)和請求失敗的緣由。
Native Heap Memory是JVM內部使用的Memory,這部分的Memory能夠經過JDK提供的JNI的方式去訪問,這部分Memory效率很高,可是管理須要本身去作,若是沒有把握最好不要使用,以防出現內存泄露問題。JVM 使用Native Heap Memory用來優化代碼載入(JTI代碼生成),臨時對象空間申請,以及JVM內部的一些操做。
這個問題每每發生在Java進程已經開始交換的狀況下,現代的GC算法已經作得足夠好了,當時當面臨因爲交換引發的延遲問題時,GC暫停的時間每每會讓大多數應用程序不能容忍。
java.lang.OutOfMemoryError:Out of swap space?
每每是由操做系統級別的問題引發的,例如:
還有多是本地內存泄漏致使應用程序失敗,好比:應用程序調用了native code連續分配內存,但卻沒有被釋放。
解決這個問題有幾個辦法,一般最簡單的方法就是增長交換空間,不一樣平臺實現的方式會有所不一樣,好比在Linux下能夠經過以下命令實現:
# 原做者使用,因爲我手裏並無Linux環境,因此並未測試 # 建立並附加一個大小爲640MB的新交換文件 swapoff -a dd if=/dev/zero of=swapfile bs=1024 count=655360 mkswap swapfile swapon swapfile
Java GC會掃描內存中的數據,若是是對交換空間運行垃圾回收算法會使GC暫停的時間增長几個數量級,所以你應該慎重考慮使用上文增長交換空間的方法。
若是你的應用程序部署在JVM須要同其餘進程激烈競爭獲取資源的物理機上,建議將服務隔離到單獨的虛擬機中
但在許多狀況下,您惟一真正可行的替代方案是:
當您轉向優化路徑時,使用內存轉儲分析程序來檢測內存中的大分配是一個好的開始。
java.lang.OutOfMemoryError:Requested array size exceeds VM limit
Java對應用程序能夠分配的最大數組大小有限制。不一樣平臺限制有所不一樣,但一般在1到21億個元素之間。
圖片來源:plumbr
當你遇到Requested array size exceeds VM limit
錯誤時,意味着你的應用程序試圖分配大於Java虛擬機能夠支持的數組。
該錯誤由JVM中的native code
拋出。 JVM在爲數組分配內存以前,會執行特定於平臺的檢查:分配的數據結構是否在此平臺中是可尋址的。
你不多見到這個錯誤是由於Java數組的索引是int類型。 Java中的最大正整數爲2 ^ 31 - 1 = 2,147,483,647。 而且平臺特定的限制能夠很是接近這個數字,例如:個人環境上(64位macOS,運行Jdk1.8)能夠初始化數組的長度高達2,147,483,645(Integer.MAX_VALUE-2)。若是再將數組的長度增長1到Integer.MAX_VALUE-1會致使熟悉的OutOfMemoryError:
Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
可是,在使用OpenJDK 6的32位Linux上,在分配具備大約11億個元素的數組時,您將遇到Requested array size exceeded VM limit
的錯誤。 要理解你的特定環境的限制,運行下文中描述的小測試程序。
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(); } }
該示例重複四次,並在每一個回合中初始化一個長原語數組。 該程序嘗試初始化的數組的大小在每次迭代時增長1,最終達到Integer.MAX_VALUE。 如今,當使用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)
注意,在出現Requested array size exceeded VM limit
以前,出現了更熟悉的java.lang.OutOfMemoryError: Java heap space
。 這是由於初始化2 ^ 31-1個元素的數組須要騰出8G的內存空間,大於JVM使用的默認值。
java.lang.OutOfMemoryError:Requested array size exceeds VM limit
可能會在如下任一狀況下出現:
Integer.MAX_INT
之間2 ^ 31-1
個元素的數組在第一種狀況下,檢查你的代碼庫,看看你是否真的須要這麼大的數組。也許你能夠減小數組的大小,或者將數組分紅更小的數據塊,而後分批處理數據。
在第二種狀況下,記住Java數組是由int索引的。所以,當在平臺中使用標準數據結構時,數組不能超過2 ^ 31-1個元素。事實上,在編譯時就會出錯:error:integer number too large
。
Out of memory:Kill process or sacrifice child
爲了理解這個錯誤,咱們須要補充一點操做系統的基礎知識。操做系統是創建在進程的概念之上,這些進程在內核中做業,其中有一個很是特殊的進程,名叫「內存殺手(Out of memory killer)」。當內核檢測到系統內存不足時,OOM killer被激活,而後選擇一個進程殺掉。哪個進程這麼倒黴呢?選擇的算法和想法都很樸實:誰佔用內存最多,誰就被幹掉。若是你對OOM Killer感興趣的話,建議你閱讀參考資料2中的文章。
OOM Killer,圖片來源:plumbr
當可用虛擬虛擬內存(包括交換空間)消耗到讓整個操做系統面臨風險時,就會產生Out of memory:Kill process or sacrifice child
錯誤。在這種狀況下,OOM Killer會選擇「流氓進程」並殺死它。
默認狀況下,Linux內核容許進程請求比系統中可用內存更多的內存,但大多數進程實際上並無使用完他們所分配的內存。這就跟現實生活中的寬帶運營商相似,他們向全部消費者出售一個100M的帶寬,遠遠超過用戶實際使用的帶寬,一個10G的鏈路能夠很是輕鬆的服務100個(10G/100M)用戶,但實際上寬帶運行商每每會把10G鏈路用於服務150人或者更多,以便讓鏈路的利用率更高,畢竟空閒在那兒也沒什麼意義。
Linux內核採用的機制跟寬帶運營商差很少,通常狀況下都沒有問題,但當大多數應用程序都消耗完本身的內存時,麻煩就來了,由於這些應用程序的內存需求加起來超出了物理內存(包括 swap)的容量,內核(OOM killer)必須殺掉一些進程才能騰出空間保障系統正常運行。就如同上面的例子中,若是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的系統日誌中/var/log/kern.log
會出現如下日誌:
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環境,因此並未測試 swapoff -a dd if=/dev/zero of=swapfile bs=1024 count=655360 mkswap swapfile swapon swapfile
解決這個問題最有效也是最直接的方法就是升級內存,其餘方法諸如:調整OOM Killer配置、水平擴展應用,將內存的負載分攤到若干小實例上..... 咱們不建議的作法是增長交換空間,具體緣由已經在前文說過。參考資料②中詳細的介紹了怎樣微調OOM Killer配置以及OOM Killer選擇進程算法的實現,建議你參考閱讀。
① 想要了解更多PermGen與Metaspace的內容推薦你閱讀:
② 若是你對OOM Killer感興趣的話,強烈建議你閱讀這篇文章:
做者:CHEN川 連接:https://www.jianshu.com/p/2fdee831ed03