整理自《深入理解 Java 虛擬機》。
Java 內存區域
Java 虛擬機在執行 Java 程序的過程中會把它所管理的內存劃分爲各個不同的數據區域,包括以下幾個部分:
1. 程序計數器
線程私有,是當前線程所執行的字節碼的行號指示器。字節碼解釋器通過改變這個計數器的值來選取下一條需要執行的字節碼指令。如果線程正在執行的是一個 Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址,如果正在執行的是 Native 方法,則計數器值爲空。
2. 虛擬機棧
線程私有,生命週期與線程相同。描述的是 Java 方法執行的內存模型,每個方法在執行的同時會創建一個棧幀用於儲存局部變量表、操作數棧、動態鏈接、方法出口等信息。
3. 本地方法棧
線程私有,與虛擬機棧發揮的作用類似,虛擬機棧是爲執行 Java 方法服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。
4. 堆
所有線程共享的一段內存區域,此區域唯一目的是存放對象實例。
5. 方法區
各個線程共享,用於儲存已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。其中的運行時常量池相對於 Class 文件常量池而言具備動態性,運行期間也可將新的常量放入池中。
運行時常量池是方法區的一部分,Class 文件中的常量池信息用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。
內存溢出
1. 堆溢出
示例:
-Xms 參數設置堆的最小值。
-Xmx 參數設置堆的最大值。
-XX:+HeapDumpOnOutOfMemoryError 參數讓虛擬機在出現內存溢出異常時 Dump 出當前內存堆轉儲快照以便進行事後分析。
/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author zzm
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
運行結果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid6468.hprof …
Heap dump file created [28156524 bytes in 0.483 secs]
要解決堆溢出異常,一般先通過內存映像分析工具對 Dump 出來的堆轉儲快照進行分析,確認內存中的對象是否是必要的,也就是分清到底是出現了內存泄露(無法釋放已申請的內存空間)還是內存溢出(申請內存時,沒有足夠的空間供使用)。
如果是內存泄露,可進一步查看泄露對象到 GC Roots 的引用鏈,找出泄露對象無法被回收的原因。
如果不存在泄露,那首先檢查虛擬機堆參數,看是否可調大。再者,從代碼上檢查是否存在某些對象生命週期過長、持有時間過長的情況,嘗試減少程序運行期的內存消耗。
2. 虛擬機棧和本地方法棧溢出
當線程請求的棧深度大於虛擬機所允許的最大深度,將出現 StackOverflowError 異常。
示例:
-Xss 參數減少棧內存容量
/**
* VM Args:-Xss128k
* @author zzm
*/
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;
}
}
}
運行結果:
Exception in thread “main” java.lang.StackOverflowError
stack length:999
爲每個線程的棧分配的內存越大,可以建立的線程數就越少,在建立過多線程時會導致內存溢出,出現 OutOfMemoryError 異常。
示例:
/**
* VM Args:-Xss2M (這時候不妨設大些)
* @author zzm
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) throws Throwable {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
上面程序在 windows 環境下運行後造成系統假死,因爲在 windows 平臺的虛擬機中,Java 線程是映射到操作系統內核線程上的。
運行結果:
Exception in thread “main” java.lang.OutOfMemoryError: unable to create new native thread
這種情況下要減少最大堆和減少棧容量來換取更多的線程。
3. 方法區和運行時常量池溢出
運行時常量池在 JDK1.6 及之前的版本中分配在永久代中,很少回收,所以會出現運行時常量池溢出的情況,JDK1.7 開始逐步“去永久代”。
運行時產生大量的類去填滿方法區就會出現方法區溢出。
示例:
藉助 CGLib 直接操作字節碼運行時產生大量的動態類。
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* @author zzm
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}
方法區溢出也是一種常見的內存溢出異常,一個類要被垃圾收集器回收掉,判定條件是比較苛刻的。在經常動態生成大量 Class 的應用中,需要特別注意類的回收狀況。
4. 本機直接內存溢出
直接內存不是虛擬機運行時數據區的一部分,但也被頻繁使用。
DirectMemory 容量可通過 -XX:MaxDirectMemorySize 指定,默認與 -Xmx 值一樣。
示例:
/**
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
* @author zzm
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
運行結果:
Exception in thread “main” java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
由 DirectMemory 導致的內存溢出,一個明顯的特徵是在 Heap Dump 文件中不會看見明顯的異常,如果發現 OOM 之後 Dump 文件很小,而程序中又直接或間接使用了 NIO,就可以考慮是這方面的原因。NIO 可以使用 Native 函數庫直接分配堆外內存,然後通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作爲這塊內存的引用進行操作。這樣能顯著提高性能,因爲避免了在 Java 堆和 Native 堆中來回複製數據。