從內存溢出看Java 環境中的內存結構

  作爲有個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高級特性與最佳實踐  機械工業出版社。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章