天天看點

Java JVM:記憶體溢出(棧溢出,堆溢出,持久代溢出以及 nable to create native thread)

轉載自https://github.com/pzxwhc/MineKnowContainer/issues/25

包括:

1. 棧溢出(StackOverflowError)

2. 堆溢出(OutOfMemoryError:java heap space)

3. 永久代溢出(OutOfMemoryError: PermGen space)

4. OutOfMemoryError:unable to create native thread

Java虛拟機規範規定JVM的記憶體分為了好幾塊,比如堆,棧,程式計數器,方法區等,而Hotspot jvm的實作中,将堆記憶體分為了兩部:新生代,老年代。在堆記憶體之外,還有永久代,其中永久代實作了規範中規定的方法區,而記憶體模型中不同的部分都會出現相應的OOM錯誤,接下來我們就分開來讨論一下。

棧溢出(StackOverflowError)

棧溢出抛出StackOverflowError錯誤,出現此種情況是因為方法運作的時候棧的深度超過了虛拟機容許的最大深度所緻。出現這種情況,一般情況下是程式錯誤所緻的,比如寫了一個死遞歸,就有可能造成此種情況。 下面我們通過一段代碼來模拟一下此種情況的記憶體溢出。

import java.util.*;    
import java.lang.*;    
public class OOMTest{     
    public void stackOverFlowMethod(){    
        stackOverFlowMethod();    
    }    
    public static void main(String... args){    
        OOMTest oom = new OOMTest();    
        oom.stackOverFlowMethod();    
    }    
}    
           

運作上面的代碼,會抛出如下的異常:

Exception in thread "main" java.lang.StackOverflowError    
        at OOMTest.stackOverFlowMethod(OOMTest.java:6)    
           

對于棧記憶體溢出,根據《Java 虛拟機規範》中文版:如果線程請求的棧容量超過棧允許的最大容量的話,Java 虛拟機将抛出一個StackOverflow異常;如果Java虛拟機棧可以動态擴充,并且擴充的動作已經嘗試過,但是無法申請到足夠的記憶體去完成擴充,或者在建立立線程的時候沒有足夠的記憶體去建立對應的虛拟機棧,那麼Java虛拟機将抛出一個OutOfMemory 異常。

堆溢出(OutOfMemoryError:java heap space)

堆記憶體溢出的時候,虛拟機會抛出java.lang.OutOfMemoryError:java heap space,出現此種情況的時候,我們需要根據記憶體溢出的時候産生的dump檔案來具體分析(需要增加-XX:+HeapDumpOnOutOfMemoryErrorjvm啟動參數)。出現此種問題的時候有可能是記憶體洩露,也有可能是記憶體溢出了。

  • 如果記憶體洩露,我們要找出洩露的對象是怎麼被GC ROOT引用起來,然後通過引用鍊來具體分析洩露的原因。
  • 如果出現了記憶體溢出問題,這往往是程式本生需要的記憶體大于了我們給虛拟機配置的記憶體,這種情況下,我們可以采用調大-Xmx來解決這種問題。下面我們通過如下的代碼來示範一下此種情況的溢出:
import java.util.*;    
import java.lang.*;    
public class OOMTest{    
        public static void main(String... args){    
                List<byte[]> buffer = new ArrayList<byte[]>();    
                buffer.add(new byte[10*1024*1024]);    
        }    

}    
           

我們通過如下的指令運作上面的代碼:

java -verbose:gc -Xmn10M -Xms20M -Xmx20M -XX:+PrintGC OOMTest

程式輸出如下的資訊:

[GC 1180K->366K(19456K), 0.0037311 secs]    
[Full GC 366K->330K(19456K), 0.0098740 secs]    
[Full GC 330K->292K(19456K), 0.0090244 secs]    
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space    
        at OOMTest.main(OOMTest.java:7)    
           

從運作結果可以看出,JVM進行了一次Minor gc和兩次的Major gc,從Major gc的輸出可以看出,gc以後old區使用率為134K,而位元組數組為10M,加起來大于了old generation的空間,是以抛出了異常,如果調整-Xms21M,-Xmx21M,那麼就不會觸發gc操作也不會出現異常了。

通過上面的實驗其實也從側面驗證了一個結論:當對象大于新生代剩餘記憶體的時候,将直接放入老年代,當老年代剩餘記憶體還是無法放下的時候,觸發垃圾收集,收集後還是不能放下就會抛出記憶體溢出異常了。

持久帶溢出(OutOfMemoryError: PermGen space)

我們知道Hotspot jvm通過持久帶實作了Java虛拟機規範中的方法區,而運作時的常量池就是儲存在方法區中的,是以持久帶溢出有可能是運作時常量池溢出,也有可能是方法區中儲存的class對象沒有被及時回收掉或者class資訊占用的記憶體超過了我們配置。

當持久帶溢出的時候抛出java.lang.OutOfMemoryError: PermGen space。可能在如下幾種場景下出現:

  1. 使用一些應用伺服器的熱部署的時候,我們就會遇到熱部署幾次以後發現記憶體溢出了,這種情況就是因為每次熱部署的後,原來的class沒有被解除安裝掉。
  2. 如果應用程式本身比較大,涉及的類庫比較多,但是我們配置設定給持久帶的記憶體(通過-XX:PermSize和-XX:MaxPermSize來設定)比較小的時候也可能出現此種問題。
  3. 一些第三方架構,比如spring,hibernate都通過位元組碼生成技術(比如CGLib)來實作一些增強的功能,這種情況可能需要更大的方法區來存儲動态生成的Class檔案。

我們知道Java中字元串常量是放在常量池中的,String.intern()這個方法運作的時候,會檢查常量池中是否存和本字元串相等的對象,如果存在直接傳回對常量池中對象的引用,不存在的話,先把此字元串加入常量池,然後再傳回字元串的引用。那麼我們就可以通過String.intern方法來模拟一下運作時常量區的溢出.下面我們通過如下的代碼來模拟此種情況:

import java.util.*;    
import java.lang.*;    
public class OOMTest{    
        public static void main(String... args){    
                List<String> list = new ArrayList<String>();    
                while(true){    
                        list.add(UUID.randomUUID().toString().intern());    
                }    
        }        
}    
           

我們通過如下的指令運作上面代碼:

java -verbose:gc -Xmn5M -Xms10M -Xmx10M -XX:MaxPermSize=1M -XX:+PrintGC OOMTest

運作後的輸入如下圖所示:

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

通過上面的代碼,我們成功模拟了運作時常量池溢出的情況,從輸出中的PermGen space可以看出确實是持久帶發生了溢出,這也驗證了,我們前面說的Hotspot jvm通過持久帶來實作方法區的說法。

OutOfMemoryError:unable to create native thread

最後我們在來看看java.lang.OutOfMemoryError:unable to create natvie thread這種錯誤。 出現這種情況的時候,一般是下面兩種情況導緻的:

1. 程式建立的線程數超過了作業系統的限制。對于Linux系統,我們可以通過ulimit -u來檢視此限制。

2. 給虛拟機配置設定的記憶體過大,導緻建立線程的時候需要的native記憶體太少。

我們都知道作業系統對每個程序的記憶體是有限制的,我們啟動Jvm,相當于啟動了一個程序,假如我們一個程序占用了4G的記憶體,那麼通過下面的公式計算出來的剩餘記憶體就是建立線程棧的時候可以用的記憶體。線程棧總可用記憶體=4G-(-Xmx的值)- (-XX:MaxPermSize的值)- 程式計數器占用的記憶體

通過上面的公式我們可以看出,-Xmx 和 MaxPermSize的值越大,那麼留給線程棧可用的空間就越小,在-Xss參數配置的棧容量不變的情況下,可以建立的線程數也就越小。是以如果是因為這種情況導緻的unable to create native thread,那麼要麼我們增大程序所占用的總記憶體,或者減少-Xmx或者-Xss來達到建立更多線程的目的。

總結:

    1. 棧記憶體溢出:程式所要求的棧深度過大導緻。
    2. 堆記憶體溢出: 厘清 記憶體洩露還是 記憶體容量不足。洩露則看對象如何被 GC Root 引用。不足則通過 調大 -Xms,-Xmx參數。
    3. 持久帶記憶體溢出:Class對象未被釋放,Class對象占用資訊過多,有過多的Class對象。
    4. 無法建立本地線程:總容量不變,堆記憶體,非堆記憶體設定過大,會導緻能給線程的記憶體不足。