Java 內存溢出分析
一、前言
Java 的 JVM 的內存一般可分為 3 個區:堆(heap)、棧(stack)和方法區(method)。
1.1 堆區
1)存儲的全部是對象,每個對象都包含一個與之對應的 Class 的信息,Class 的目的是得到操作指令;
2)JVM 只有一個堆區(heap)被所有線程共享,堆中不存放基本類型和對象引用,只存放對象本身。
1.2 棧區
1)每個線程包含一個棧區,棧中只保存基礎數據類型的對象和自定義對象的引用(不是對象),對象都存放在堆區中;
2)每個棧中的數據(原始類型和對象引用)都是私有的,其他棧不能訪問;
3)棧分為3個部分:基本類型變量區、執行環境上下文、操作指令區(存放操作指令)。
1.3 方法區
1)又叫靜態區或永久代,跟堆一樣,被所有的線程共享。方法區包含所有的 Class 和 static 變量;
2)方法區中包含的都是在整個程序中永遠唯一的元素,如 Class,static 變量;
3)運行時常量池都分配在 Java 虛擬機的方法區之中,但是從 JDK 1.7 的 HotSpot 虛擬機開始中,已經把原本放在永久代的字符串常量池移出了。
二、內存溢出分析
以下代碼驗證,建議使用 JDK 1.6,因為從 JDK 1.7 開始,HotSpot 虛擬機改進了很多,特別是方法區(永久代)。
2.1 Java 堆溢出
Java堆是用於存儲對象實例的,因此,只要我們不斷創建對象,並且保證對象不被垃圾回收機制清除,那麽當堆中對象的大小超過了最大堆的容量限制,就會出現堆內存溢出。
堆溢出常見的異常是:java.lang.OutOfMemoryError: Java heap space
下面這段代碼,將Java堆的大小設置為 20MB,並且不可擴展(即將堆的最小值 -Xms 參數和最大值 -Xmx 參數設置為相等的 20MB,來避免堆自動擴展);通過參數 -XX:+HeapDumpOnOutOfMemoryError 可以讓虛擬機在出現內存溢出異常時生成當前內存堆的快照,以便事後進行分析;-verbose:gc 的作用是在虛擬機發生內存回收時在輸出設備顯示信息。
package com.java.error; import java.util.ArrayList; importjava.util.List; /** * VM Args:-verbose:gc -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * Error:java.lang.OutOfMemoryError: Java heap space * @author moonxy * */ public class HeapOOM { public static void main(String[] args) { List<HeapOOM> list = new ArrayList<HeapOOM>(); while(true) { list.add(new HeapOOM()); } } }
運行後顯示如下:
[GC (Allocation Failure) 5380K->3745K(19968K), 0.0042158 secs] [GC (Allocation Failure) 9287K->9710K(19968K), 0.0058399 secs] [Full GC (Ergonomics) 9710K->7589K(19968K), 0.1200134 secs] [Full GC (Ergonomics) 16387K->12869K(19968K), 0.1112792 secs] [Full GC (Ergonomics) 16428K->16382K(19968K), 0.1711686 secs] [Full GC (Allocation Failure) 16382K->16370K(19968K), 0.1371103 secs] java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid1204.hprof ... Heap dump file created [28024534 bytes in 0.077 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:261) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) at java.util.ArrayList.add(ArrayList.java:458) at com.java.error.OutOfMemoryJavaHeapSpaceTest.main(OutOfMemoryJavaHeapSpaceTest.java:19)
控制臺打印了OutOfMemoryError,並且提示是Java heap發生的內存溢出,同時生成了dump文件。對於Java堆溢出,一般通過內存映像分析工具(如Eclipse Memory Analyzer)對dump文件進行堆快照分析,確認內存中的對象是不是必要的:
如果對象不是必要的,那就屬於內存泄漏,需要分析為什麽對象沒有被回收;
如果對象確實有必要繼續存在,那就屬於內存溢出,需要通過將堆的大小調高(-Xms 和 -Xmx)來避免內存溢出。
此處比較一下內存泄漏和內存溢出:
內存泄漏 memory leak:是指程序在申請內存後,無法釋放已經申請的內存空間,一次內存泄露似乎不會有大的影響,但內存泄漏堆積後的後果就是內存溢出;
內存溢出 out of memory:是指程序申請內存後,沒有足夠的內存供申請者使用或將 long 類型的數據存儲到 int 類型的存儲空間時,就會出現 OOM 錯誤。
2.2 虛擬機棧和本地方法棧溢出
由於HotSpot虛擬機不區分虛擬機棧和本地方法棧,因此,對於 HotSpot 來說,-Xoss參數(設置本地方法棧大小)是無效的,棧容量只由 -Xss 設置。
在Java虛擬機規範中,這個區域有兩種異常情況:
如果線程運行時的棧幀的總大小超過虛擬機限制的大小,會拋出 StackOverflowError,這一點通常發生在遞歸運行時,棧溢出容易出現:java.lang.StackOverflowError;
如果虛擬機棧設置為可以動態擴展,並且在擴展時無法申請到足夠內存,則會拋出 OutOfMemoryError,此時容易出現:java.lang.OutOfMemoryError: unable to create native thread。
StackOverflowError
/** * VM Args:-Xss128k * Error:java.lang.StackOverflowError * @author moonxy */ 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; } } }
運行後顯示如下:
994 Exception in thread "main" java.lang.StackOverflowError at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:15) at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16) at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16) at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16) at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16) ……
OutOfMemoryError
不建議執行如下驗證,容易發生系統假死,因為在 Windows 平臺的虛擬機中,Java 線程是映射到操作系統的內核線程中的。
/** * VM Args:-verbose:gc -Xss2m * Error:java.lang.OutOfMemoryError: unable to create native thread * @author moonxy * */ 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) { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } }
運行後顯示如下:
java.lang.OutOfMemoryError: unable to create new native thread
線程需要消耗棧容量,而操作系統分配給每個進程的內存是有限制的。考慮如下場景:系統總內存 6G,JVM分配了 2G 內存,其中堆內存分配了 1G,方法區(永久代)分配 512M,忽略其他較小的內存分配,則剩余分配給虛擬機棧和本地方法棧的內存就只有512M,很有可能沒有足夠的可用內存創建很多的線程。就有可能出現 java.lang.OutOfMemoryError: unable to create new native thread。如果是這種情況,考慮減小堆內存大小或適當減少每個線程的棧分配大小。
線程會占用內存,如果每個線程都占用更多內存,整體上將消耗更多的內存。每個線程默認占用內存大小取決於 JVM 實現。可以利用 -Xss 參數限制線程內存大小,降低總內存消耗。例如,JVM 默認每個線程占用 1M 內存,應用有 500個 線程,那麽將消耗 500M 內存空間。如果實際上 256K 內存足夠線程正常運行,配置 -Xss256k,那麽 500 個線程將只需要消耗 125M 內存。(註意,如果 -Xss 設置的過低,又將會產生 java.lang.StackOverflowError 錯誤)
2.3 方法區溢出
方法區存儲的是虛擬機加載的類信息、常量、靜態變量、JIT 編譯器編譯後的代碼等數據,在 JDK1.7之前,HotSpot 都是使用 "永久代" 來管理這些數據,也就是說,方法區的數據,其實也是屬於堆內存的,會占用堆內存。
因此:方法區的數據大小 < -XX:MaxPermSize < -Xmx
我們只要限制一下永久代的大小(-XX:MaxPermSize),很容易就會發生方法區溢出,此時方法區容易出現:java.lang.OutOfMemoryError: PermGen space。可以通過設置 -XX:PermSize 和 -XX:MaxPermSize 來限制方法區大小。
/** * VM Args:-verbose:gc -XX:PermSize=10M -XX:MaxPermSize=10M * Error:java.lang.OutOfMemoryError: PermGen space * @author moonxy * */ public class OutOfMemoryPermGenSpaceTest { public static void main(String[] args) { List<String> oomPgsList = new ArrayList<String>(); int i = 0; while(true) { oomPgsList.add(String.valueOf(i++).intern()); } } }
運行後結果如下:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
如果我們在 JDK 1.7 的環境下執行上面同樣的代碼,會發現 while 循環會一直執行下去。原因是在 JDK 1.7 中,HotSpot 已經不再將常量放入永久代中進行管理,而是放到內存上限是本機內存的 Native Memory 中,這樣做的好處就是減少了內存溢出的幾率(沒有了-XX:MaxPermSize 的限制),同時常量不再占用堆的內存。這種 "去永久代" 的做法,從 JDK 1.7 已經開始進行。終於在 JDK 1.8 中,被徹底的執行了,永久代被徹底去掉了,同時 HotSpot 新增了一塊叫做 Mataspace 的區域,並提供了 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 參數,來設置運行 Java 虛擬機使用的 Metaspace 的初始容量和最大容量。
2.4 本機直接內存溢出
直接內存(Direct Memory)並不是虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。但是這部分內存也被頻繁的使用,而且也可能導致 OutOfMemoryError 出現,即:java.lang.OutOfMemory。
本機直接內存的容量可以通過 -XX:MaxDirectMemorySize 指定,如果不指定,默認與 Java 堆最大值(-Xmx指定)一樣。
本機直接內存溢出的一個明顯特征是,dump 文件很小,因為主要對象都在 direct memory了,並且異常信息也不會說明是在哪個區域發生內存溢出,就像這樣:java.lang.OutOfMemoryError
/** * * VM Args:-verbose:gc -XX:MaxDirectMemorySize * Error:java.lang.OutOfMemory * @author moonxy * */ 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
綜上,可以設置如下的 JVM 參數:
-Xms256m -Xmx1024m -XX:PermSize=256m -XX:MaxPermSize=512m
Java 內存溢出分析