堆溢位
Java堆唯一的作用就是儲存物件例項,只要保證不斷建立物件並且物件不被回收,那麼物件數量達到最大堆容量限制後就會產生記憶體溢位異常了。所以測試的時候把堆的大小固定住並且讓堆不可擴充套件即可。測試程式碼如下
1 package com.xrq.test; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 /** 7 * 測試內容:堆溢位 8 * 9 * 虛擬機器引數:-Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError 10 */ 11 public class HeapOverflowTest 12 { 13 public static void main(String[] args) 14 { 15 List<HeapOverflowTest> list = new ArrayList<HeapOverflowTest>(); 16 while (true) 17 { 18 list.add(new HeapOverflowTest()); 19 } 20 } 21 }
執行結果
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid8876.hprof ... Heap dump file created [15782068 bytes in 0.217 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:2760) at java.util.Arrays.copyOf(Arrays.java:2734) at java.util.ArrayList.ensureCapacity(ArrayList.java:167) at java.util.ArrayList.add(ArrayList.java:351) at com.xrq.test.HeapOverflowTest.main(HeapOverflowTest.java:18)
這種異常很常見,也很好發現,因為都提示了“Java heap space”了,定位問題的話,根據異常堆疊分析就好了,行號都有指示。解決方案的話,可以調大堆的大小或者從程式碼上檢視是否存在某些物件生命週期過長、持有狀態時間過長的情況,長時間少程式執行期間的記憶體消耗。
棧溢位
Java虛擬機器規範中描述瞭如果執行緒請求的棧深度太深(換句話說方法呼叫的深度太深),就會產生棧溢位了。那麼,我們只要寫一個無限呼叫自己的方法,自然就會出現方法呼叫的深度太深的場景了。測試程式碼如下
1 package com.xrq.test; 2 3 /** 4 * 測試內容:棧溢位測試(遞迴呼叫導致棧深度不斷增加) 5 * 6 * 虛擬機器引數:-Xss128k 7 */ 8 public class StackOverflowTest 9 { 10 private int stackLength = 1; 11 12 public void stackLeak() 13 { 14 stackLength++; 15 stackLeak(); 16 } 17 18 public static void main(String[] args) throws Throwable 19 { 20 StackOverflowTest stackOverflow = new StackOverflowTest(); 21 try 22 { 23 stackOverflow.stackLeak(); 24 } 25 catch (Throwable e) 26 { 27 System.out.println("stack length:" + stackOverflow.stackLength); 28 throw e; 29 } 30 } 31 }
執行結果:
stack length:1006 Exception in thread "main" java.lang.StackOverflowError at com.xrq.test.StackOverflowTest.stackLeak(StackOverflowTest.java:14) at com.xrq.test.StackOverflowTest.stackLeak(StackOverflowTest.java:15) at com.xrq.test.StackOverflowTest.stackLeak(StackOverflowTest.java:15) at com.xrq.test.StackOverflowTest.stackLeak(StackOverflowTest.java:15) at com.xrq.test.StackOverflowTest.stackLeak(StackOverflowTest.java:15) at com.xrq.test.StackOverflowTest.stackLeak(StackOverflowTest.java:15)
...
後面都是一樣的,忽略。通過不斷建立執行緒的方式可以產生OutOfMemoryError,因為每個執行緒都有自己的棧空間。不過這個操作有危險就不做了,原因是Windows平臺下,Java的執行緒是直接對映到作業系統的核心執行緒上的,如果寫個死迴圈無限產生執行緒,那麼可能會造成作業系統的假死。
上面無限產生執行緒的場景,從另外一個角度說,就是為每個執行緒的棧分配的記憶體空間越大,反而越容易產生記憶體溢位。其實這也很好理解,作業系統分配給程式的記憶體是有限制的,比如32位的Windows限制為2GB。虛擬機器提供了了引數來控制Java堆和方法區這兩部分記憶體的最大值,剩餘記憶體為2GB-最大堆容量-最大方法區容量,程式計數器很小就忽略了,虛擬機器程式本身的耗費也不算,剩下的記憶體就是棧的了。每個執行緒分配到的棧容量越大,可建立的執行緒數自然就越少,建立執行緒時就越容易把剩下的記憶體耗盡。
StackOverFlowError這個異常,有錯誤堆疊可以閱讀,比較好定位。而且如果使用虛擬機器預設引數,棧深度在大多數情況下,達到1000~2000完全沒有問題,正常方法的呼叫這個深度應該是完全夠了。但是如果建立過多執行緒導致的OutOfMemoryError,在不能減少執行緒數或者更換64位虛擬機器的情況下,就只能通過減小最大堆容量和減小棧容量來換取更多的執行緒了。
方法區和執行時常量池溢位
執行時常量池也是方法區的一部分,所以這兩個區域一起看就可以了。這個區域的OutOfMemoryError可以利用String.intern()方法來產生。這是一個Native方法,意思是如果常量池中有一個String物件的字串就返回池中的這個字串的String物件;否則,將此String物件包含的字串新增到常量池中去,並且返回此String物件的引用。測試程式碼如下
1 package com.xrq.test; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 /** 7 * 測試內容:常量池溢位(這個例子也可以說明執行時常量池為方法區的一部分) 8 * 9 * 虛擬機器引數-XX:PermSize=10M -XX:MaxPermSize=10M 10 */ 11 public class ConstantPoolOverflowTest 12 { 13 public static void main(String[] args) 14 { 15 List<String> list = new ArrayList<String>(); 16 int i = 0; 17 while (true) 18 { 19 list.add(String.valueOf(i++).intern()); 20 } 21 } 22 }
執行結果
Exception in thread "Reference Handler" Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.String.intern(Native Method) at com.xrq.test.ConstantPoolOverflowTest.main(ConstantPoolOverflowTest.java:19) java.lang.OutOfMemoryError: PermGen space at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:123)
之前有講過,對於HotSpot而言,方法區=永久代,這裡看到OutOfMemoryError的區域是“PermGen space”,即永久代,那其實也就是方法區溢位了。注意一下JDK1.7下是不會有這個異常的,while迴圈將一直下去,因為JDK1.7之後溢位了永久代並採用Native Memory來實現方法區的規劃了。