《JVM筆記》之一:Java記憶體區域與記憶體溢位異常
Java與C++之間有一堵由記憶體動態分配和垃圾收集技術所圍成的高牆,牆外面的人想進去,牆裡面的人卻想出來。
按照《Java虛擬機器規範(第2版)》的規定,Java虛擬機器所管理的記憶體將包括以下幾個執行時資料區域,來個圖更加直觀點,如下圖所示:
解釋下各個部分
程式計數器:
Program Counter Register是一塊較小的記憶體空間,它的作用可以看做是當前執行緒所執行的位元組碼的行號指示器。 每個執行緒都有一個獨立的程式計數器,各個執行緒之間計數器互不影響,獨立儲存。此記憶體區域是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。
Java虛擬機器棧:
也是執行緒私有的,它的生命週期與執行緒相同。每個方法被執行的時候會同時建立一個棧幀(Stack Frame)用於儲存區域性變量表、操作棧、動態連結、方法出口等資訊。每個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器中從入棧到出棧的過程。
如果執行緒請求棧深度大於虛擬機器所允許的深度,丟擲StackOverflowError
如果虛擬機器棧可以動態擴充套件,擴充套件時無法申請到足夠的記憶體時會丟擲OutOfMemoryError
本地方法棧:
Native Method Stacks與虛擬機器棧所發揮的作用是非常相似的,只不過一個是執行Java方法,一個是Nataive方法,HotSpot虛擬機器直接將兩者合二為一了
Java堆:
Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。Java堆是垃圾收集器管理的主要區域,很多時候稱為GC堆。
如果在堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,將會丟擲OutOfMemoryError
方法區:
Method Area與Java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、JIT編譯後的程式碼等資料。
當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError。
執行時常量池:
Runtime Constant Pool是方法區的一部分。用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類載入後存放到方法區的執行時常量池中。
當常量池無法再申請到記憶體時會丟擲OutOfMemoryError異常。
直接記憶體:
Direct Memory並不是虛擬機器執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域,但是這部分也是頻繁使用。在Java的NIO中使用到,伺服器管理員忽略直接記憶體後果是,各個記憶體區域總和大於實體記憶體限制,從而導致動態擴充套件時出現OutOfMemoryError異常。
實戰:OutOfMemoryError異常:
1,Java堆溢位:
Java堆用於儲存物件例項,我們只要不斷建立物件,並且保證GC Roots到物件之間有可達路徑來避免GC清除這些物件,就會在物件數量到達最大堆的容量限制後產生記憶體溢位異常。
VM Args: -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
XX:+HeapDumpOnOutOfMemoryError這個引數可以讓虛擬機器在出現記憶體溢位異常時Dump出當前的記憶體堆轉儲快照以便事後進行分析。
import java.util.ArrayList;
import java.util.List;
/**
* VM Args: -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
* @author Administrator
*
*/
public class HeapOOM {
static class OOMObject{
private String name;
public OOMObject(String name) {
this.name = name;
}
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
long i = 1;
while(true) {
list.add(new OOMObject("IpConfig..." + i++));
}
}
}
丟擲的異常:
Dumping heap to java_pid27828.hprof ...
Heap dump file created [14123367 bytes in 0.187 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.lang.AbstractStringBuilder.<init>(AbstractStringBuilder.java:45)
at java.lang.StringBuilder.<init>(StringBuilder.java:92)
at com.baoxian.HeapOOM.main(HeapOOM.java:22)
注:出現Java堆記憶體溢位時,異常堆疊資訊 java.lang.OutOfMemoryError 後面會緊跟著 Java heap space。
要解決這個異常,一般手段是首先通過記憶體映像分析工具比如Eclipse Memory Analyzer對dump出來的堆轉儲快照進行分析,重點是確認記憶體中物件是否是必要的,也就是要弄清楚到底是出現了記憶體洩露 Memory Leak還是記憶體溢位 Memory Overflow。
如果是記憶體洩露,可進一步通過工具檢視洩露物件到GC Roots的引用鏈。於是就能找到洩露物件時通過怎樣的路徑與GC Roots相關聯並導致垃圾收集器無法自動回收它們。掌握了洩露物件的型別資訊,以及GC Roots引用鏈的資訊,就可以比較準確的定位出洩露程式碼的位置了。
如果不存在洩露,那麼就該修改-Xms 和 -Xms堆引數看能否加大點。
2,虛擬機器棧和本地方法棧溢位
-Xoss引數設定本地方法棧大小,對於HotSpot沒用。棧容量只由-Xss引數設定。
/**
* VM Args: -Xss128k
* @author Administrator
*
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable{
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length: " + oom.stackLength);
throw e;
}
}
}
丟擲異常:
stack length: 1007
Exception in thread "main" java.lang.StackOverflowError
at com.baoxian.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
at com.baoxian.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
at com.baoxian.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
at com.baoxian.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)。。。。
3,執行時常量池溢位:
執行時常量池分配在方法區內,可以通過 -XX:PermSize和 -XX:MaxPermSize限制方法區大小,從而間接限制其中常量池的容量。
import java.util.ArrayList;
import java.util.List;
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* @author Administrator
*
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持著常量池引用,避免Full GC回收常量池行為
List<String> list = new ArrayList<String>();
// 10MB的PermSize在integer範圍內足夠產生OOM了
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)
at com.baoxian.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)
執行時常量池溢位,在java.lang.OutOfMemoryError後面緊跟著是PermGen space
4,方法區溢位:
方法區用於存放Class的相關資訊,如類名、訪問修飾符、常量池、欄位描述符、方法描述等。對於這個區域的測試,基本的思路是執行時產生大量的類去填滿方法區,直到溢位。比如動態代理會生成動態類。
使用CGLib技術直接操作位元組碼執行,生成大量的動態類。當前很多主流框架如Spring和Hibernate對類進行增強都會使用CGLib這類位元組碼技術,增強的類越多,就需要越大的方法區來保證動態生成的Class可以載入入記憶體。
異常:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
同樣,跟常量池一樣,都是PermGen space字串出現
方法區溢位也是一種常見的記憶體溢位異常,一個類如果要被垃圾收集器回收,判定條件是非常苛刻的。在經常動態生成大量Class的應用中,需要特別注意類的回收狀況。這類場景除了上面提到的程式使用GCLib位元組碼技術外,常見的還有: 大量JSP或動態產生的JSP檔案的應用(JSP第一次執行時需要編譯為Java類)、基於OSGi應用等。
5,本機直接記憶體溢位:
DirectMemory容量可以通過-XX:MaxDirectMemorySize指定,如果不指定,則預設與Java堆的最大值-Xmx指定一樣。
/**
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
* @author Administrator
*
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while(true) {
unsafe.allocateMemory(_1MB);
}
}
}
在OutOfMemoryError後面不會有任何東西了,這就是DirectMemory記憶體溢位了。