JVM記憶體溢位詳解(棧溢位,堆溢位,持久代溢位以及無法建立本地執行緒)
寫在前面
記憶體溢位和記憶體洩漏的區別:
記憶體溢位 out of memory,是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory;比如申請了一個integer,但給它存了long才能存下的數,那就是記憶體溢位。
記憶體洩露 memory leak,是指程式在申請記憶體後,無法釋放已申請的記憶體空間,一次記憶體洩露危害可以忽略,但記憶體洩露堆積後果很嚴重,無論多少記憶體,遲早會被佔光。
memory leak會最終會導致out of memory
記憶體溢位就是你要求分配的記憶體超出了系統能給你的,系統不能滿足需求,於是產生溢位。
記憶體洩漏是指你向系統申請分配記憶體進行使用(new),可是使用完了以後卻不歸還(delete),結果你申請到的那塊記憶體你自己也不能再訪問(也許你把它的地址給弄丟了),而系統也不能再次將它分配給需要的程式。一個盤子用盡各種方法只能裝4個果子,你裝了5個,結果掉倒地上不能吃了。這就是溢位!比方說棧,棧滿時再做進棧必定產生空間溢位,叫上溢,棧空時再做退棧也產生空間溢位,稱為下溢。就是分配的記憶體不足以放下資料項序列,稱為記憶體溢位.
全文簡短總結,具體內容可以看下文。
棧記憶體溢位(StackOverflowError):
程式所要求的棧深度過大導致,可以寫一個死遞迴程式觸發。
堆記憶體溢位(OutOfMemoryError:java heap space)
- 分清記憶體溢位還是記憶體洩漏
- 洩露則看物件如何被 GC Root 引用。
- 溢位則通過 調大 -Xms,-Xmx引數。
持久帶記憶體溢位(OutOfMemoryError: PermGen space)
- 持久帶中包含方法區,方法區包含常量池
- 因此持久帶溢位有可能是執行時常量池溢位,也有可能是方法區中儲存的class物件沒有被及時回收掉或者class資訊佔用的記憶體超過了我們配置
- 用String.intern()觸發常量池溢位
- Class物件未被釋放,Class物件佔用資訊過多,有過多的Class物件。可以導致持久帶記憶體溢位
無法建立本地執行緒
總容量不變,堆記憶體,非堆記憶體設定過大,會導致能給執行緒的記憶體不足。
以下是詳細內容
棧溢位(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。可能在如下幾種場景下出現:
- 使用一些應用伺服器的熱部署的時候,我們就會遇到熱部署幾次以後發現記憶體溢位了,這種情況就是因為每次熱部署的後,原來的class沒有被解除安裝掉。
- 如果應用程式本身比較大,涉及的類庫比較多,但是我們分配給持久帶的記憶體(通過-XX:PermSize和-XX:MaxPermSize來設定)比較小的時候也可能出現此種問題。
- 一些第三方框架,比如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這種錯誤。 出現這種情況的時候,一般是下面兩種情況導致的:
- 程式建立的執行緒數超過了作業系統的限制。對於Linux系統,我們可以通過ulimit -u來檢視此限制。
- 給虛擬機器分配的記憶體過大,導致建立執行緒的時候需要的native記憶體太少。
我們都知道作業系統對每個程序的記憶體是有限制的,我們啟動Jvm,相當於啟動了一個程序,假如我們一個程序佔用了4G的記憶體,那麼通過下面的公式計算出來的剩餘記憶體就是建立執行緒棧的時候可以用的記憶體。執行緒棧總可用記憶體=4G-(-Xmx的值)- (-XX:MaxPermSize的值)- 程式計數器佔用的記憶體
通過上面的公式我們可以看出,-Xmx 和 MaxPermSize的值越大,那麼留給執行緒棧可用的空間就越小,在-Xss引數配置的棧容量不變的情況下,可以建立的執行緒數也就越小。因此如果是因為這種情況導致的unable to create native thread,那麼要麼我們增大程序所佔用的總記憶體,或者減少-Xmx或者-Xss來達到建立更多執行緒的目的。