程序小白在寫代碼的過程中,經常會不經意間寫出發生內存溢出異常的代碼。很多時候這類異常如何產生的都傻傻弄不清楚,如果能故意寫出讓jvm發生內存溢出的代碼,有時候看來也並非一件容易的事。最近通過學習《深入理解java虛擬機-JVM高級特性與最佳實踐》這本書,終於初步了解了一下java虛擬機的內存模型。本文通過寫出使jvm發生內存溢出異常的代碼來對自己的學習結果進行總結,同時也提醒自己以後寫代碼時候不要再跳進這個坑啦。
java的內存管理是由java虛擬機自動進行管理的,並不需要程序員過多的手動干預,這也就導致了初學java的人在不了解java內存模型的情況下也能愉快的進行coding。不過一旦涉及了內存洩露或者內存溢出以及垃圾回收(GC)方面的問題,如果不了解虛擬機是怎麼管理內存的,那麼排查問題,定位錯誤地點就顯得無從下手了。首先上圖看一下java虛擬機運行時數據區是什麼樣子(圖片來源於網絡):
從上圖我們可以獲得以下信息:
本文主要講述如何讓JVM發生內存溢出異常,有關JVM內存模型將會在另一篇文章中詳細講解,這裡簡單介紹各分區的作用:
在jvm規范中,除了程序計數器內存區域沒有規定任何內存溢出異常情形外,其他四個內存區域都會有相應的內存溢出異常發生的可能,所以jvm內存溢出異常發生在不同的內存區域具有不同的異常發生原因,知道一內存異常產生的位置,對於定位錯誤地點就很有指向性了。
下面就通過實例來展示,如何通過代碼指定讓不同的內存區域發生內存溢出異常。
一、java堆發生內存溢出:
java堆是用來存儲對象實例以及數組的,使java堆發生內存溢出的要旨是:
java虛擬機的內存大小是可以人為設置的,通過設置限制內存大小,可以很方便的實現內存溢出,節約了時間。
設置java堆內存大小的虛擬機參數為:-Xms堆的初始大小 -Xmx堆可擴展的最大值
1 package Text.JVM; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 /** 7 * JVM堆中存儲java對象實例和數組 8 * VM Args: -Xms20m -Xmx20m (限制堆的大小不可擴展) 9 * @author Administrator 10 * 11 */ 12 public class HeapOutOfMemoryError { 13 14 public static class OOMObject { 15 } 16 public static void main(String[] args) { 17 List<Object> list=new ArrayList<>(); 18 // 不斷創建對象,並保證GC Roots到對象之間有可達路徑,避免垃圾回收清除創建的對象 19 while (true) { 20 list.add(new OOMObject()); 21 System.out.println(System.currentTimeMillis()); 22 } 23 } 24 25 }View Code
jvm虛擬機啟動參數設置:
運行結果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:2245) at java.util.Arrays.copyOf(Arrays.java:2219) at java.util.ArrayList.grow(ArrayList.java:242) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208) at java.util.ArrayList.add(ArrayList.java:440) at Text.JVM.HeapOutOfMemoryError.main(HeapOutOfMemoryError.java:20)View Code
注意運行結果第一行末尾:java.lang.OutOfMemoryError: Java heap space 。java heap space明確的指出了異常發生的區域:堆。
二、虛擬機棧發生內存溢出異常:
能夠使虛擬機棧發生內存溢出異常的情形有兩種:
使java虛擬機棧發生內存溢出異常的要旨:
使用遞歸導致虛擬機棧內存溢出異常的示例:
1 package Text.JVM; 2 3 4 /* 5 * 線程請求的棧深度超過虛擬機所允許的最大深度,將拋出StackOverflowError異常 6 * 最常見引起此類異常的情形時使用不合理的遞歸調用 7 * VM Args:-Xss256k 8 * 9 * @author Administrator 10 * 11 */ 12 public class StackOverflowError { 13 14 // 記錄內存溢出時的棧深度 15 private int stackLength = 1; 16 17 // 遞歸調用的方法 18 public void stackLeak() { 19 stackLength++; 20 stackLeak(); 21 } 22 23 public static void main(String[] args) { 24 25 StackOverflowError oomError = new StackOverflowError(); 26 try { 27 oomError.stackLeak(); 28 } catch (Throwable e) { 29 System.out.println("棧深度為:" + oomError.stackLength); 30 throw e; 31 } 32 } 33 34 }View Code
程序運行結果:
棧深度為:2491 Exception in thread "main" java.lang.StackOverflowError at Text.JVM.StackOverflowError.stackLeak(StackOverflowError.java:19) at Text.JVM.StackOverflowError.stackLeak(StackOverflowError.java:20) at Text.JVM.StackOverflowError.stackLeak(StackOverflowError.java:20) at Text.JVM.StackOverflowError.stackLeak(StackOverflowError.java:20) ......View Code
異常信息中的:java.lang.StackOverflowError表明了內存溢出區域為虛擬機棧。
通過創建線程導致虛擬機棧內存溢出異常的示例:
1 package Text.JVM; 2 3 /* 4 * 通過不斷創建活躍線程,消耗虛擬機棧資源 5 * VM Args:-Xss256k 6 */ 7 public class StackOutOfMemoryError { 8 9 // 線程任務,每個線程任務一直在運行 10 private void wontStop() { 11 while (true) { 12 System.out.println(System.currentTimeMillis()); 13 } 14 } 15 16 // 不斷地創建線程 17 public void stackLeadByThread() { 18 while (true) { 19 Thread thread = new Thread(new Runnable() { 20 21 @Override 22 public void run() { 23 wontStop(); 24 } 25 }); 26 thread.start(); 27 } 28 } 29 30 public static void main(String[] args) { 31 StackOutOfMemoryError oomError=new StackOutOfMemoryError(); 32 oomError.stackLeadByThread(); 33 } 34 35 }View Code
理論上本段代碼的運行結果應該是: Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread ,但是由於windows平台虛擬機java線程映射到操作系統的內核線程上,執行上述代碼有較大風險可能會導致操作系統假死(我運行完是真的死了),所以運行需謹慎!!!(寫了這麼長的博客有一半沒保存,重啟後丟了這我會亂說???都是淚啊)。
三、方法區和運行時常量池內存溢出異常
方法區的作用是存儲 Java 類的結構信息,當我們創建對象實例後,對象的類型信息存儲在方法區之中,實例數據存放在堆中;實例數據指的是在 Java 中創建的各種實例對象以及它們的值,類型信息指的是定義在 Java 代碼中的常量、靜態變量、以及在類中聲明的各種方法、方法字段等等;同時可能包括即時編譯器編譯後產生的代碼數據。通過在運行時產生大量的類,或者工程本身具有大量的類,而方法區分配的空間不足以容納如此多的類信息的時候就會產生方法區內存溢出異常。
運行時常量池是方法取得一部分,程序中使用到的String類型字面量以及基本數據類型的一部分數據會存儲在常量池中。我們使用String.intern()方法來測試,是運行時常量池發生內存溢出。此方法的作用是:如果字符串常量池中不包含一個等於此String對象的字符串,則將此對象包含的字符串添加到常量池中,並返回此對象的引用。
使方法區發生內存溢出的要旨:
常量池內存溢出示例代碼(僅在jdk6之前的版本中有效):
1 package Text.JVM; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 /* 7 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M(限制常量池容量) 8 * 9 */ 10 public class RunTimeConstantPoolOutOfMemoryError { 11 12 public static void main(String[] args) { 13 // 使用list保持常量池引用,避免常量池內的數據被垃圾回收清除 14 List<String> list = new ArrayList<>(); 15 long i = 0; 16 while (true) { 17 String string = (i++) + ""; 18 list.add(string.intern()); 19 } 20 } 21 22 }View Code
據說此段代碼在jdk6之前的版本中運行時會產生:Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 其中PermGen space指示內存溢出發生在運行時常量池中。
但是,我在jdk7的環境中運行得到的結果卻是: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 指示內存溢出發生在堆中而不是方法區中的常量池!!!都說實踐是檢驗真理的唯一標准,果真是沒錯的。因為在 JDK1.2 ~ JDK6 的實現中,HotSpot 使用永久代實現方法區,而從 JDK7 開始 Oracle HotSpot 開始移除永久代,JDK7中符號表被移動到 Native Heap中,字符串常量和類引用被移動到 Java Heap中。在 JDK8 中,永久代已完全被元空間(Meatspace)所取代。關於常量池的存放位置還有待進一步研究,不過上段代碼是可以引起常量池的內存溢出的。
通過運行時動態產生大量的類產生方法區內存溢出示例這裡就不提供了,書中提供了使用CGLib使方法區出現內存異常的示例:
1 /** 2 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M 3 * @author zzm 4 */ 5 public class JavaMethodAreaOOM { 6 7 public static void main(String[] args) { 8 while (true) { 9 Enhancer enhancer = new Enhancer(); 10 enhancer.setSuperclass(OOMObject.class); 11 enhancer.setUseCache(false); 12 enhancer.setCallback(new MethodInterceptor() { 13 public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { 14 return proxy.invokeSuper(obj, args); 15 } 16 }); 17 enhancer.create(); 18 } 19 } 20 21 static class OOMObject { 22 23 } 24 }View Code
運行結果:
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) ... 8 moreView Code
我並沒有運行過這段代碼,因為我本人對於CGLib並不了解,這裡只要知道發生內存溢出時顯示:PermGen space那就是方法區內存溢出准沒錯啦。
四、直接內存不足導致內存溢出異常
直接內存不是虛擬機運行時的數據區的一部分,也不是在java虛擬機規范中定義的區域,但是這部分區域也被頻繁使用,也會導致OutOfMemoryError
直接內存應用於NIO,直接內存區域默認為對內存的最大值,通過-XX:MaxDirectMemorySize可以顯式的指定直接內存大小,如果忽略直接內存,容易使各個內存區域總和大於物理內存限制,從而導致動態擴展時出現內存溢出現象。
具體的代碼示例也就不貼了,因為平時的學習過程中還沒有使用過或者還沒有意識到自己使用過直接內存區。
(注:以上內容完全是為了記錄學習結果,所有內容皆為原創,如果覺得對您有用歡迎轉載,但請注明出處,尊重原創,如果文內有內容不對的地方還請多多指教)