作爲有個java程序員,我想大家對下面出現的這幾個場景並不陌生,倍感親切,深惡痛絕,抓心撓肝,一定會回過頭來問爲什麼爲什麼爲什麼會這樣,嘿嘿,讓我們看一下我們日常在開發過程中接觸內存溢出的異常:
Exception in thread "main" [Full GCjava.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Unknown Source) at java.util.Arrays.copyOf(Unknown Source) at java.util.ArrayList.grow(Unknown Source) at java.util.ArrayList.ensureExplicitCapacity(Unknown Source) at java.util.ArrayList.ensureCapacityInternal(Unknown Source) at java.util.ArrayList.add(Unknown Source) at oom.HeapOOM.main(HeapOOM.java:21)
Exception in thread "main" java.lang.StackOverflowError at java.nio.CharBuffer.arrayOffset(Unknown Source) at sun.nio.cs.UTF_8.updatePositions(Unknown Source) at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(Unknown Source) at sun.nio.cs.UTF_8$Encoder.encodeLoop(Unknown Source) at java.nio.charset.CharsetEncoder.encode(Unknown Source) at sun.nio.cs.StreamEncoder.implWrite(Unknown Source) at sun.nio.cs.StreamEncoder.write(Unknown Source) at java.io.OutputStreamWriter.write(Unknown Source) at java.io.BufferedWriter.flushBuffer(Unknown Source) at java.io.PrintStream.write(Unknown Source) at java.io.PrintStream.print(Unknown Source) at java.io.PrintStream.println(Unknown Source)
java.lang.OutOfMemoryError: PermGen space
Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at oom.DirectMemoryOOM.main(DirectMemoryOOM.java:23)
是不是有大家很熟悉的,遇見這樣的問題解決起來可能不簡單,但是如果現在讓大家寫個程序,故意讓程序出現下面的異常,估計能很快寫出來的也不是很多,這就要求開發人員對於java內存區域以及jvm規範有比較深的瞭解。
既然拋出了異常,首先我們肯定這些都是內存異常,只是內存異常中的不同種類,我們就試着瞭解一下爲什麼會出現以上的異常,可以看出有兩種異常狀況::
OutOfMemoryError
StackOverflowError
其中OutOfMemoryError是在程序無法申請到足夠的內存的時候拋出的異常,StackOverflowError是線程申請的棧深度大於虛擬機所允許的深度所拋出的異常。 可是從上面列出的異常內容也可以看出在OutOfMemoryError類型的一場中也存在這很多異常的可能。這是爲什麼?以爲是在內存的不同結構中出現的錯誤,所以拋出的異常也就形形色色,說道這我們不得不介紹一下java的內存結構,請看下圖(從網上摘的):
在運行時的內存區域有5個部分,Method Area(方法區),Java stack(java 虛擬機棧),Native MethodStack(本地方法棧),Heap(堆),Program Counter Regster(程序計數器)。從圖中看出方法區和堆用黃色標記,和其他三個區域的不同點就是,方法區和堆是線程共享的,所有的運行在jvm上的程序都能訪問這兩個區域,堆,方法區和虛擬機的生命週期一樣,隨着虛擬機的啓動而存在,而棧和程序計數器是依賴用戶線程的啓動和結束而建立和銷燬。
Program Counter Regster(程序計數器):每一個用戶線程對應一個程序計數器,用來指示當前線程所執行字節碼的行號。由程序計數器給文字碼解釋器提供嚇一條要執行的字節碼的的位置。根據jvm規範,在這個區域中不會拋出OutOfMemoryError的內存異常。
Java stack(java 虛擬機棧):這個區域是最容易出現內存異常的區域,每一個線程對應生成一個線程棧,線程每執行一個方法的時候,都會創建一個棧幀,用來存放方法的局部變量表,操作樹棧,動態連接,方法入口,這和C#是不一樣的,在C#CLR中沒有棧幀的概念,都是在線程棧中通過壓棧和出棧的方式進行數據的保存。jvm規範對這個區域定義了兩種內存異常,OutOfMemoryError,StackOverflowError。
Native MethodStack(本地方法棧):和虛擬機棧一樣,不同的是處理的對象不一樣,虛擬機棧處理java的字節碼,而本地棧則是處理的Native方法。其他方面一致。
Heap(堆):前面說了堆是所有線程都能訪問的,隨着虛擬機的啓動而存在,這塊區域很大,因爲所有的線程都在這個區域保存實例化的對象,因爲每一個類型中,每個接口實現類需要的內存不一樣,一個方法內的多個分支需要的內存也不盡相同,我們只有在運行的時候才能知道要創建多少對象,需要分配多大的地址空間。GC關注的正是這樣的部分內容,所以很多時候也將堆稱爲GC堆。堆中肯定不會拋出StackOverflowError類型的異常,所以只有OutOfMemoryError相關類型的異常。
Method Area(方法區):用於存放已被虛擬機加載的類信息,常量,靜態方法,即使編譯後的代碼。同樣只能拋出OutOfMemoryError相關類型的異常。
介紹完jvm內存結構中的常見區域,下面該是和我們主題呼應的時候了,在什麼情況下,在那個區域,如何才能復現開始提到的異常信息?從第一個開始,異常信息的內容爲:
Exception in thread "main" [Full GCjava.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Unknown Source) at java.util.Arrays.copyOf(Unknown Source) at java.util.ArrayList.grow(Unknown Source) at java.util.ArrayList.ensureExplicitCapacity(Unknown Source) at java.util.ArrayList.ensureCapacityInternal(Unknown Source) at java.util.ArrayList.add(Unknown Source) at oom.HeapOOM.main(HeapOOM.java:21)
可想而知是在堆中出現的問題,如何重現,由於是在堆中出現這個異常,那麼就要處理好,不能被垃圾回收器給回收了,設置一下jvm中堆的最大值(這樣才能夠更快的出現錯誤),設置jvm值的方法是通過-Xms(堆的最小值),-Xmx(堆的最大值)。下面動手試一下:
package oom; import java.util.ArrayList; import java.util.List; import testbean.UserBean; /*** * * @author Think * */ public class HeapOOM { static class OOMObject { } public static void main(String[] args) { List<UserBean> users = new ArrayList<UserBean>(); while (true) { users.add(new UserBean()); } } }
UserBean對象定義如下:
package testbean; public class UserBean { String name; int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public UserBean() { super(); } }
然後在運行的時候設置jvm參數,如下:
運行一下看看結果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Unknown Source) at java.util.Arrays.copyOf(Unknown Source) at java.util.ArrayList.grow(Unknown Source) at java.util.ArrayList.ensureExplicitCapacity(Unknown Source) at java.util.ArrayList.ensureCapacityInternal(Unknown Source) at java.util.ArrayList.add(Unknown Source) at oom.HeapOOM.main(HeapOOM.java:21)
成功在java虛擬機堆中溢出。
下面看第二個關於棧的異常,內容如下:
Exception in thread "main" java.lang.StackOverflowError at java.nio.CharBuffer.arrayOffset(Unknown Source) at sun.nio.cs.UTF_8.updatePositions(Unknown Source) at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(Unknown Source) at sun.nio.cs.UTF_8$Encoder.encodeLoop(Unknown Source) at java.nio.charset.CharsetEncoder.encode(Unknown Source) at sun.nio.cs.StreamEncoder.implWrite(Unknown Source) at sun.nio.cs.StreamEncoder.write(Unknown Source) at java.io.OutputStreamWriter.write(Unknown Source) at java.io.BufferedWriter.flushBuffer(Unknown Source) at java.io.PrintStream.write(Unknown Source) at java.io.PrintStream.print(Unknown Source) at java.io.PrintStream.println(Unknown Source)
因爲是與棧相關的話,那麼我們在重現異常的時候就要相應的將棧內存容量設置的小一些,設置棧大小的方法是設置-Xss參數,看如下實現:
package oom; import testbean.Recursion; /*** * * @author Think * */ public class VMStackOOM { public static void main(String[] args) { Recursion recursion = new Recursion(); try { recursion.recursionself(); } catch (Throwable e) { System.out.println("current value :" + recursion.currentValue); throw e; } } }
Recursion的定義如下:
package testbean; public class Recursion { public int currentValue = 0; public void recursionself() { currentValue += 1; recursionself(); } }
運行時jvm參數的設置如下:
運行結果如下:
current value :999 Exception in thread "main" java.lang.StackOverflowError at testbean.Recursion.recursionself(Recursion.java:7) at testbean.Recursion.recursionself(Recursion.java:8) at testbean.Recursion.recursionself(Recursion.java:8) at testbean.Recursion.recursionself(Recursion.java:8) at testbean.Recursion.recursionself(Recursion.java:8) at testbean.Recursion.recursionself(Recursion.java:8) 省略下面的異常信息
第三個異常是關於perm的異常內容,我們需要的是設置方法區的大小,實現方式是通過設置-XX:PermSize和-XX:MaxPermSize參數,內容如下:
java.lang.OutOfMemoryError: PermGen space
如果程序加載的類過多,例如tomcatweb容器,就會出現PermGen space異常,如果我將HeapOOM類的運行時的XX:PermSize設置爲2M,如下:
那麼程序就不會執行成功,執行的時候出現如下異常:
Error occurred during initialization of VM java.lang.OutOfMemoryError: PermGen space at sun.misc.Launcher$ExtClassLoader.getExtClassLoader(Unknown Source) at sun.misc.Launcher.<init>(Unknown Source) at sun.misc.Launcher.<clinit>(Unknown Source) at java.lang.ClassLoader.initSystemClassLoader(Unknown Source) at java.lang.ClassLoader.getSystemClassLoader(Unknown Source)
第四個異常估計遇到的人就不多了,是DirectMemory內存相關的,內容如下:
Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at oom.DirectMemoryOOM.main(DirectMemoryOOM.java:23)
DirectMemoruSize可以通過設置 -XX:MaxDirectMemorySize參數指定容量大小,如果不指定的話,那麼就跟堆的最大值一致,下面是代碼實現:
package oom; import java.lang.reflect.Field; import sun.misc.Unsafe; /*** * * @author Think * */ public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB); } } }
運行時設置的jvm參數如下:
很容易就複線了異常信息:
Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at oom.DirectMemoryOOM.main(DirectMemoryOOM.java:23)
參考:深入理解JAVA虛擬機 JVM高級特性與最佳實踐 機械工業出版社。