最近在看周志明的《深入理解Java虛擬機》,雖然剛剛開始看,但是覺得還是一本不錯的書。對于和我一樣對于JVM了解不深,有志進一步了解的人算是一本不錯的書。注明:不是書托,同樣是華章出的書,質量要比《深入剖析Tomcat》高好多,起碼排版上沒有那么多嚴重的失誤,停,等哪天心情不好再噴那本書。:)(還有一本書讓我看完覺得挺不爽的,當然不排除自身問題)
剛剛看了兩章,第一章我比較關注如何自己編譯openJdk,額,現在還沒搗騰成功,完成后再分享,暫且跳過;本篇文章的主要任務是記錄書中關于產生OutOfMemoryError異常的原因。代碼以及說明基本都是出自原書,寫這篇文章意在加深印象,同時分享給那些沒有讀過這本書的人。說句自己的一次經歷,不記得是在哪家公司面試來著,面試官曾經問過我都有哪些情況會造成OutOfMemoryError異常。很遺憾,當時我不會。
設置運行時參數
說下為什么加了這樣一節,說來慚愧,第一次設置運行時參數,找不到在哪里設置,找了半天才找對位置,怕有和我一樣小白的人存在,所以就增加了這樣一個小節。(IDE工具是eclipse)
按照如下三步設置即可,呈現一場代碼的注釋中會標注每種情形需要設置的運行時參數。
step1:

step2:

step3:

可以這樣為每個含有main函數的類指定自己的運行時參數。
造成內存溢出之五大元兇
個人覺得程序員都要有”刨祖墳”的精神,文藝一點兒就是知其然,知其所以然。在日常的工作中更應該如此,不能說要實現一個功能就滿口答應,起碼要知道為什么需要這樣一個功能,解決什么問題,是否合理。如果連原因都不知道,真心不相信能把這個功能做好。也許這個也是好管理和不好管理程序員的分割線。如果說發生OutOfMemoryError跟我們無關,那我們為什么要知道發生的原因啊,美國打伊拉克我和程序員有毛關系啊。其實這個異常對大家來說應該都不陌生,之前我最愛的處理就是從新再運行一次,不行關閉eclipse,再不行重啟電腦。(殺手锏級別的解決方案).可是這樣不科學,科學的方式就要求我們知道為什么會發生這個異常,換句話說是發生這個異常的場景,然后通過打印出的異常信息快速定位發生內存溢出的區域,然后進行權衡,調整運行時參數來解決。
Java堆溢出
Java堆用于存儲對象實例,知道這一點就很容易呈現堆溢出,不斷的創建對象,并且保持有指向其的引用,防止為gc。
代碼如下:
import java.util.ArrayList;
import java.util.List;
/**
* VM Args:-Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
*
*/
public class HeapOOM {
static class OOMObject{
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while(true){
list.add(new OOMObject());
}
}
}
通過設置-Xms20M -Xmx20M都為20M意在防止堆大小自動擴展,更好的展現溢出。 執行結果如下:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
是不是很明顯啊,顯示堆空間發生OutOfMemoryError。
書中告訴我們發生了OutOfMemoryError后,通常是通過內存影像分析工具對dump出來的堆轉儲快照進行分析(這就是運行時參數中配置-XX:+HeapDumpOnOutOfMemoryError的原因),重點是確定是由內存泄露(Memory Leak)還是有內存溢出(Memory Overflow)引起的OutOfMemoryError。如果是內存泄露則找到泄露點,修正;如果確實是合理的存在,那么就增加堆空間。(內存分析這里我也木有做過,工具也木有使用過,在后續章節會有介紹,用熟了后再來一篇)
虛擬機棧和本地方法棧溢出
由于在HotSpot虛擬機中并不區分虛擬機棧和本地方法區棧,因此對于HotSpot來說,-Xoss(設置本地方法棧大小)參數是無效的,棧容量由-Xss參數設定。關于虛擬機棧和本地方法區棧,在Java虛擬機規范中描述了兩種異常:
- 如果線程請求的棧深度大于虛擬機所允許的最大深度,將拋出StackOverflowError異常
- 如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常
書中談到單線程的場景下只能浮現StackOverflowError,那我們就先來看看單線程場景下到底會是什么樣子。
/** * * VM Args:-Xss128k */ public class JavaVMStackSOF { private int stackLength = 1; private void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); throw e; } } }
通過-Xss128k設置虛擬機棧大小為128k,執行結果如下:
執行結果顯示,確實是發生了StackOverflowError異常。
通過不斷創建線程耗盡內存也可以呈現出OutOfMemoryError異常,但是在Windows平臺下模擬會使系統死機,我這里就不多說了。感興趣的可以自己去嘗試。
運行時常量池溢出
向運行時常量池中添加內容最簡單的方式就是使用String.intern()方法。由于常量池分配在方法區內,可以通過-XX:PermSize和-XX:MaxPermSize限制方法區的大小,從而間接限制其中常量池的容量。
代碼如下:
import java.util.ArrayList; import java.util.List; /** * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M * */ public class RuntimeConstantPoolOOM { public static void main(String[] args) { List<String> list = new ArrayList<String>(); int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } } }
這里有個小小的插曲,之前有聽說在jdk7中將永久區(方法區和常量池)給干掉了,沒有驗證過。永久區可以說是在堆之上的一個邏輯分區。如果jdk7中去掉了,那么這個示例應該會拋出堆空間的內存溢出,而非運行時常量池的內存溢出。所以在執行程序的時候分別用了jdk6和jdk7兩個版本。多說一句,如果jdk7去掉了方法區,那么-XX:PermSize=10M -XX:MaxPermSize=10M就不起作用了,所以在jdk7環境下運行時,堆大小為jvm默認的大小,要執行一會兒(半小時左右:( ))才能拋出異常。沒關系,再配置下運行時參數即可,注意要配置成不可擴展。以圖為據:
- jdk6環境下拋出運行時常量池內存溢出
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
顯而易見PermGen space,永久區。不解釋。
- jdk7環境下,運行時參數為:-XX:PermSize=10M -XX:MaxPermSize=10M
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
運行了好久好久,最終拋出堆內存溢出。Java heap space已經足夠說明問題了。
- jdk7環境下,運行時參數為:-verbose:gc -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
同樣也是堆內存溢出,不過速度就快了好多好多,因為堆大小被設置為不可擴展。
方法區溢出
方法區用于存放Class的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。測試這個區域只要在運行時產生大量的類填滿方法區,知道溢出。書中借助CGlib直接操作字節碼運行時,生成了大量的動態類。
當前主流的Spring和Hibernate對類進行增強時,都會使用到CGLib這類字節碼技術,增強的類越多,就需要越大的方法區來保證動態生成的Class可以加載到內存。
測試代碼如下:
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; /** * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M * */ public class JavaMethodAreaOOM { public static void main(String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable{ return proxy.invokeSuper(object, args); } }); enhancer.create(); } } static class OOMObject { } }
工程中要引入cglib-2.2.2.jar和asm-all-3.3.jar。
方法區的內存溢出問題同樣存在jdk6和jdk7版本之間的區別,同運行時常量池內存溢出。
方法區溢出也是一種常見的內存溢出異常,一個類如果要被垃圾收集器回收掉,判定條件是非常苛刻的。在經常動態生成大量Class的應用中,需要特別注意類的回收狀況。這類場景除了上面提到的程序使用了CGLib字節碼增強外,常見的還有:大量JSP或動態生成JSP文件的應用、基于OSGi的應用等。
本機直接內存溢出
DirectMemory容量可以通過-XX:MaxDirectMemorySize指定。
示例代碼如下:
import java.lang.reflect.Field; import sun.misc.Unsafe; /** * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * */ public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; /** * @param args * @throws IllegalAccessException * @throws IllegalArgumentException */ public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException { // TODO Auto-generated method stub Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while(true){ unsafe.allocateMemory(_1MB); } } }
運行結果如下圖:
拋出內存溢出異常。不解釋。