JVM 內存溢出詳解(棧溢出,堆溢出,持久代溢出、無法創建本地線程)


出處:  http://www.jianshu.com/p/cd705f88cf2a

 

1、內存溢出和內存泄漏的區別

  內存溢出 (Out Of Memory):是指程序在申請內存時,沒有足夠的內存空間供其使用,出現Out Of Memory。

  內存泄露 (Memory Leak):是指程序在申請內存后,由於某種原因無法釋放已申請的內存空間,導致這塊內存無法再次被利用,造成系統內存的浪費。

  memory leak會最終會導致out of memory。

  

2、內存溢出分類

2.1 棧內存溢出(StackOverflowError):

  程序所要求的棧深度過大導致,可以寫一個死遞歸程序觸發。

2.2 堆內存溢出(OutOfMemoryError : java heap space)

需要分清是 內存溢出 還是 內存泄漏:
(1)如果是內存溢出,則通過 調大 -Xms,-Xmx參數。
(2)如果是內存泄露,則看對象如何被 GC Root 引用。

2.3 持久帶內存溢出(OutOfMemoryError: PermGen space)

持久帶中包含方法區,方法區包含常量池。

因此持久帶溢出有可能是(1) 運行時常量池溢出,也有可能是(2)方法區中保存的Class對象沒有被及時回收掉或者Class信息占用的內存超過了我們配置。

用String.intern()觸發常量池溢出。
Class對象未被釋放,Class對象占用信息過多,有過多的Class對象。可以導致持久帶內存溢出。

2.4 無法創建本地線程

Caused by: java.lang.OutOfMemoryError:unable to create new native thread

系統內存的總容量不變,堆內存、非堆內存設置過大,會導致能給線程分配的內存不足。

 

3、內存溢出詳解

3.1 棧溢出(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 異常。

3.2 堆溢出(OutOfMemoryError:java heap space)

堆內存溢出的時候,虛擬機會拋出 java.lang.OutOfMemoryError:java heap space。

出現此種情況的時候,我們需要根據內存溢出的時候產生的 dump 文件來具體分析(需要增加 -XX:+HeapDumpOnOutOfMemoryError jvm啟動參數)。出現此種問題的時候有可能是內存泄漏,也有可能是內存溢出了。

1、配置方法

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${目錄}。
2、參數說明

(1)-XX:+HeapDumpOnOutOfMemoryError參數表示當JVM發生OOM時,自動生成DUMP文件。

(2)-XX:HeapDumpPath=${目錄}參數表示生成DUMP文件的路徑,也可以指定文件名稱,例如:-XX:HeapDumpPath=${目錄}/java_heapdump.hprof。如果不指定文件名,默認為:java_<pid><date><time>_heapDump.hprof。

如果是內存泄漏,我們要找出內存泄漏的對象是怎么被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操作也不會出現異常了。

  通過上面的實驗其實也從側面驗證了一個結論:對象大於新生代剩余內存的時候,將直接放入老年代,當老年代剩余內存還是無法放下的時候,觸發垃圾收集,收集后還是不能放下就會拋出內存溢出異常了。

3.3 持久帶溢出(OutOfMemoryError: PermGen space)

我們知道 Hotspot jvm 通過持久帶實現了Java虛擬機規范中的方法區,而運行時的常量池就是保存在方法區中。

因此持久帶溢出有可能是:

(1) 運行時常量池溢出
(2)方法區中保存Class對象沒有被及時回收掉或者Class信息占用的內存超過了我們配置

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

  • 使用一些應用服務器的熱部署的時候,我們就會遇到熱部署幾次以后發現內存溢出了,這種情況就是因為每次熱部署的后,原來的Class沒有被卸載掉。

  • 如果應用程序本身比較大,涉及的類庫比較多,但是我們分配給持久帶的內存(通過-XX:PermSize和-XX:MaxPermSize來設置)比較小的時候也可能出現此種問題。

  • 一些第三方框架,比如spring、hibernate都是通過字節碼生成技術(比如CGLib)來實現一些增強的功能,這種情況可能需要更大的方法區來存儲動態生成的Class文件。

  • 我們知道Java中字符串常量是放在常量池中的,String.intern()這個方法運行的時候,會檢查常量池中是否存和本字符串相等的對象,如果存在直接返回常量池中對象的引用,不存在的話,先把此字符串加入常量池,然后再返回字符串的引用。

那么我們就可以通過String.intern方法來模擬一下運行時常量區的溢出(JDK6下,因為jdk6中常量池位於PremGen區,jdk7之后將常量池移到了Java堆區)。下面我們通過如下的代碼來模擬此種情況:

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通過持久帶來實現方法區的說法。

3.4 無法創建本地線程

java.lang.OutOfMemoryError: unable to create new native thread

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

  • 程序創建的線程數超過了操作系統的限制。對於Linux系統,我們可以通過 ulimit -u 來查看此限制。

  • 給虛擬機分配的內存過大,導致創建線程的時候需要的native內存太少。

建立每個線程,都需要給這個線程分配棧空間。

我們都知道操作系統對每個進程的內存是有限制的,我們啟動jvm,相當於啟動了一個進程,假如我們一個進程占用了4G的內存,那么通過下面的公式計算出來的剩余內存就是建立線程棧的時候可以用的內存。

線程棧總可用內存 = 4G -(-Xmx的值)- (-XX:MaxPermSize的值)- 程序計數器占用的內存

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM