1. 程式人生 > >死磕JVM-如何構造JVM記憶體溢位和棧溢位

死磕JVM-如何構造JVM記憶體溢位和棧溢位

為什麼要寫這個題目?我記得我在面試阿里的時候面試官問了我這個問題,當時沒能答得很好,只說了些概念的東西,很是心虛,於是下定決心要把這個問題搞懂,現在終於把這個問題懟清楚了,分享給大家,希望你們以後面試問到這種問題能有所準備。

Java虛擬機器中描述了兩種異常:

1、如果執行緒請求的棧深度大於虛擬機器所允許的最大深度,將丟擲StackOverflowError異常;

2、如果在虛擬機器中無法申請到足夠多的記憶體空間,將丟擲OutOfMemoryError異常。

我們都知道Java虛擬機器各個記憶體區域(除了程式計數器)都有發生記憶體溢位的可能,但到底什麼樣的操作或程式才會導致記憶體溢位或棧溢位的異常呢?我們分不同的記憶體區域來解釋這個問題。

一、對於Java堆記憶體區域

Java堆中只會產生OutOfMemoryError異常。

先搞清楚Java堆記憶體放的是什麼,還不清楚的可以回顧下這篇文章《死磕JVM-Java記憶體模型》,從這篇文章裡我們知道Java堆記憶體存放的是物件例項,所以原理上只要我們不斷建立物件,並且保證GC Roots到物件之間有可達路徑來避免垃圾回收機制清楚這些物件,也就是說當Eden區滿的時候,GC被觸發時,讓GC誤以為記憶體中的物件還存活著,那麼在物件數量達到最大堆容量限制的時候就會產生記憶體溢位的異常。如下程式碼就會產生記憶體溢位的異常:

public class 堆溢位 {

     static class OOMError{}

     public static void main(String[] args) {
          List<OOMError> list = new ArrayList<OOMError>();
          while (true) {
               list.add(new OOMError());
          }
     }
}

執行結果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
     at java.util.Arrays.copyOf(Arrays.java:3210)
     at java.util.Arrays.copyOf(Arrays.java:3181)
     at java.util.ArrayList.grow(ArrayList.java:261)
     at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
     at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
     at java.util.ArrayList.add(ArrayList.java:458)
     at com.intelligentler.jvm.堆溢位.main(堆溢位.java:13)

“Java heap space”提示著產生OutOfMemoryError異常的Java虛擬機器的記憶體區域,也就是Java堆記憶體。

如何解決發生在Java堆記憶體的OutOfMemoryError異常呢?

首先我們要分清楚產生OutOfMemoryError異常的原因是記憶體洩露還是記憶體溢位,如果記憶體中的物件確實都必須存活著而不像上面那樣不斷地建立物件例項卻不使用該物件,則是記憶體溢位,而像上面程式碼中的情況則是記憶體洩露。

如果是記憶體洩露,我們可以通過一些記憶體檢視工具來檢視洩露物件到GC Roots的引用鏈,找到洩露物件是通過怎樣的路徑與GC Roots相關聯並導致GC無法自動回收這些洩露物件,掌握了這些資訊,我們就能比較準確地定位出洩露程式碼的位置。

如果不是記憶體洩露,也就是說記憶體中的物件確實都還必須存活,那麼應該檢查虛擬機器的堆引數,看看是否還可以將機器實體記憶體調大,同時在程式碼上檢查是否存在某些物件生命週期過長、持有狀態時間過長的情況。

二、對於虛擬機器棧和本地方法棧

在這一部分記憶體區域,可能產生OutOfMemoryError異常和StackOverflowError異常。

如果定義大量的本地變數,增大此方法幀中本地變量表的長度或者設定-Xss引數減少棧記憶體容量,這兩種操作都會丟擲StackOverflowError異常,如下面的程式碼:

public class 棧溢位 {

     private int stackLength = 1;

     public void addStackLength(){
          stackLength++;
          addStackLength();
     }

     public static void main(String[] args) throws Throwable{
          棧溢位 oom = new 棧溢位();
          try {
               oom.addStackLength();
          } catch (Throwable e) {
               System.out.println("stack length:" + oom.stackLength);
               throw e;
          }
     }

}

執行結果:

stack length:18388
Exception in thread "main" java.lang.StackOverflowError
     at com.intelligentler.jvm.棧溢位.addStackLength(棧溢位.java:9)
     at com.intelligentler.jvm.棧溢位.addStackLength(棧溢位.java:9)
     at com.intelligentler.jvm.棧溢位.addStackLength(棧溢位.java:9)

所以,如果在單執行緒的情況下,無論是棧幀太大還是虛擬機器棧容量太小,當記憶體無法再分配的時候,虛擬機器丟擲的是StackOverflowError異常。

如果在多執行緒下,不斷地建立執行緒可能會產生OutOfMemoryError異常。

三、對於方法區

方法區中只會產生OutOfMemoryError異常。

由於執行時常量池是方法區的一部分,我們可以通過String.intern()方法來構建一個執行時常量池的OutOfMemoryError異常。

String.intern()是一個Native方法,它的作用是:如果字串常量池中已經包含了一個等於該String物件的字串,則返回這個String物件,否則,將此String物件包含的字串新增到常量池中,並返回這個字串的String物件的引用。如下面程式碼:

public class 方法區溢位 {

     public static void main(String[] args) {
          List<String> list = new ArrayList<String>();
          int i = 0;
          while (true) {
               list.add(String.valueOf(i++).intern());
          }
     }

}

執行結果:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    at java.lang.String.intern(Native Method)

PermGen space的全稱是Permanent Generation space,是指記憶體的永久儲存區域,也就是說執行時常量池屬於方法區(也就是虛擬機器永久代)中的一部分。

另外,方法區是存放Class的相關資訊的,執行時如果有大量的類來填滿方法區,就會產生OutOfMemoryError異常。


作者: 死磕自己的研究僧 
連結:http://www.imooc.com/article/18073
來源:慕課網