1. 程式人生 > >《JVM筆記》之一:Java記憶體區域與記憶體溢位異常

《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記憶體溢位了。