JVM:內存溢出問題

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來解決這種問題。下面我們通過如下的代碼來演示一下此種情況的溢出:

package test;

import java.util.ArrayList;
import java.util.List;

public class OOMTest {

    static class OOMObject {  
    }  

    public static void main(String[] args) {  
        List<OOMObject> list = new ArrayList<OOMObject>();  

        while (true) {  
            list.add(new OOMObject());  
            /*System.out.println("total(k):"+Runtime.getRuntime().totalMemory()/1024+ 
                    "  freeMemory(k):"+Runtime.getRuntime().freeMemory()/1024+ 
                    "  maxMemory(k):"+Runtime.getRuntime().maxMemory()/1024+ 
                    "  availableProcessors:"+Runtime.getRuntime().availableProcessors());*/  
        }  
    }  
}

運行上面的代碼,會拋出如下的異常:

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 test.OOMTest.main(OOMTest.java:15)

持久帶溢出(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來達到創建更多線程的目的。

總結:
棧內存溢出:程序所要求的棧深度過大導致。

堆內存溢出: 分清 內存泄露還是 內存容量不足。泄露則看對象如何被 GC Root 引用。不足則通過 調大 -Xms,-Xmx參數。

持久帶內存溢出:Class對象未被釋放,Class對象佔用信息過多,有過多的Class對象。

無法創建本地線程:總容量不變,堆內存,非堆內存設置過大,會導致能給線程的內存不足。

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