前言
上一篇我們介紹了JVM03–JVM垃圾收集機制的一些基本概念,這一篇介紹一下JVM中各種內存溢出及其處理方法。
本文會按照JVM中內存劃分來介紹各種內存溢出的例子。
一些基本的設置說明
爲了模擬出內存溢出的效果,我們需要手動設置內存區域的內存大小,下面就是設置值部分設置值及其說明。
分類 | 選項 | 說明 |
---|---|---|
虛擬機棧 | -Xss | 每個線程的棧大小 |
堆空間 | -Xms | 啓動JVM時的初始堆大小 |
堆空間 | -Xmx | 堆空間最大值 |
堆空間 | -Xmn | 新生代大小(1.4or lator) 注意:此處的大小是(eden+ 2 survivor space).與jmap -heap中顯示的New gen是不同的。整個堆大小=年輕代大小 + 年老代大小 + 持久代大小.增大年輕代後,將會減小年老代大小.此值對系統性能影響較大,Sun官方推薦配置爲整個堆的3/8 |
新生代空間 | -XX:NewRatio | 新生代與老年代的比例 |
新生代空間 | -XX:NewSize | 新生代大小 |
新生代空間 | -XX:SurvivorRation | Eden區域SurvivorRation區的比例 |
永久代空間 | -XX:PermSize | 啓動JVM時的初始永久代大小 |
永久代空間 | -XX:MaxPermSize | 永久代空間最大值 |
元空間 | -XX:MetaspaceSize | 指定元空間的初始空間大小,以字節爲單位,達到該值就會觸發垃圾收集進行類型卸載,同時收集器會對該值進行調整;如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過 -XX:MaxMetaspaceSize 的情況下,適當提高該值。 |
元空間 | -XX:MaxMetaspaceSize | 設置元空間最大值,默認是-1,即不限制,或者說只受限於本地內存大小 |
元空間 | -XX:MaxMetaspaceFreeRatio | 在垃圾收集之後控制最小的元空間剩餘容量的百分比,可減少因爲元空間不足導致的垃圾收集的頻率 |
堆內存溢出
Java堆用於存儲對象的實例,我們只要不斷地創建對象,並且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼隨着對象數量的增加,總容量觸及最大堆的容量限制後就會產生內存溢出。Java堆內存的OutOfMemoryError異常是實際應用中最常見的內存溢出異常情況。出現Java堆內存溢出時,異常堆棧信息“java.lang.OutOfMemoryError”會跟隨進一步提示“Java heap space”。下面舉個例子來模擬堆內存溢出。這裏將-Xms
和-Xmx
都設置成20M,保證了Java堆內存不可擴展。然後,通過-XX:HeapDumpPath
指定dump文件的保存位置。這裏通過while循環不斷的創建對象,然後保存到集合中。
/**
* Java堆內存異常
VM Args:
//這兩個參數保證了堆中的可分配內存固定爲20M
-Xms20m
-Xmx20m
-XX:+HeapDumpOnOutOfMemoryError //自動生成dump文件
//文件生成的位置,作爲生成在桌面的一個目錄
-XX:HeapDumpPath=D:\srv\dump
*
* @author xiang.wei
* @date 2020/5/27 13:35
*/
public class HeapOOM {
/**
* 創建一個內部類用於創建對象使用
*/
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
//無限的創建對象放在堆中
while (true) {
list.add(new OOMObject());
}
}
}
下面簡單的說一下在Idea中設置應用運行內存的方法,我們只需要在 Run---->Edit Configurations—>找到需要設置的主類,然後在VM options中添加
-Xms20M -Xmx20M -Xmn20M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\srv\dump
如下圖所示:
上面程序運行結果如下:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to D:\srv\dump\java_pid23624.hprof ...
Heap dump file created [28062091 bytes in 0.143 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.jay.service.HeapOOM.main(HeapOOM.java:31)
Java HotSpot(TM) 64-Bit Server VM warning: MaxNewSize (20480k) is equal to or greater than the entire heap (20480k). A new max generation size of 19968k will be used.
我們可以看到程序生成了dump文件到指定目錄,打開jvisualvm.exe工具導入heapDump文件,相應的說明如下圖所示:
一共創建了802858個實例,消耗了 98.9%的運行內存。
切換到實例數如下圖所示:
如何解決堆內存的OOM異常呢,首先需要確認內存中導致OOM的對象是否是必要的,也就是要先分清楚到底是出現了內存泄露(Memory Leak)還是內存溢出(Memory Overflow)。
Java棧內存異常
說完了Java堆內存異常,下面我們來看看Java棧內存異常,在實際開發中發生棧內存異常的情況比較少。Java棧內存異常發生的兩種情況是:
- 如果線程請求的棧深度(棧深度:指目前虛擬機棧中沒有出棧的方法幀)大於虛擬機所允許的最大深度,將拋出StackOverflowError異常。這種情況在遞歸的場景下可能會出現,如果在使用遞歸算法時沒有控制好遞歸的跳出條件,就有可能會出現這種情況,下面的例子就是一個沒有跳出條件的遞歸調用。根據前面說明,我們可以通過
-Xss160k
設置棧容量爲160K。 - 如果虛擬機的棧內存允許動態擴展,當擴展容量無法申請到足夠的內存時,將拋出OutOfMemoryError異常。
如下這個例子:
/**
* VM Args:
* 設置棧容量爲160K,默認1M
* -Xss160k
* @author xiang.wei
* @date 2020/5/27 16:09
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
System.out.println("*********棧的深度是:" + stackLength);
//遞歸調用
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Exception e) {
throw e;
}
}
}
運行結果如下:
*********棧的深度是:1287
Exception in thread "main" java.lang.StackOverflowError
at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)
可以看到,遞歸調用了1287次,棧容量不夠用了。
默認的棧容量在正常的方法調用時,棧深度可以達到1000-2000深度,所以,一般的遞歸可以承受的住,如果代碼中出現了StackOverflowError,首先需要檢查代碼,看看是不是遞歸寫的不對。不能只是調參數。
當創建很多線程時,容易出現OOM(OutOfMemoryError),這時可以通過具體情況,減少最大堆容量,或者棧容量來解決問題。
線程數(最大棧容量)+最大堆值+其他內存(忽略不計或者一般不改動)=機器最大內存*
當線程數比較多時,且無法通過業務上減少線程數,再不換機器的情況下,我們只能把最大棧容量設置小一點,或者把最大堆值設置小一點。
方法區和運行時常量池溢出
由於運行時常量池是方法區的一部分,所以這兩個區域的溢出測試可以放在一起進行。需要注意的是HotSpot從JDK7開始逐步“去永久代”的計劃,並在JDK8中完全使用元空間代替永久代,使用"永久代"還是"元空間"來實現方法區,對程序的影響是不同的。
元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存,因此,默認情況下元空間的大小僅受本地內存限制,但仍可以通過參數控制:
-XX:MetaspaceSize與-XX:MaxMetaspaceSize來控制大小。
方法區溢出也是一種常見的內存溢出異常,一個類如果要被垃圾收集器回收,要達成的條件是比較苛刻的。在經常運行時生成大量動態類的應用場景裏,就應該特別關注這些類的回收狀況,這類場景除了之前提到的程序使用了CGLib字節碼增強和動態語言外,常見的還有:大量JSP或動態產生JSP文件的應用(JSP第一次運行時需要編譯爲Java類)、基於OSGI的應用(即使是同一個類文件,被不同的加載器加載也會被視爲不同的類)。
方法區的主要職責是用於存放類型的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。 對於這部分區域的測試,基本的思路是運行時產生大量的類去填滿方法區,直到溢出爲止。
這是因爲在調用CGLib創建代理時會生成動態代理類,即Class對象到Metaspace,所以while一下就出異常了。
下面這個例子就是通過設置元空間大小,然後,通過動態代理生成大量的類,來模擬元空間內存溢出的情況。
/**
* 在JDK8下測試方法區,所以設置了Metaspace的大小爲固定的8M
-XX:MetaspaceSize=8m
-XX:MaxMetaspaceSize=8m
* @author xiang.wei
* @date 2020/5/27 17:29
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(JOOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, objects, methodProxy) -> methodProxy.invokeSuper(obj,objects));
//無限創建動態代理,生成class對象
enhancer.create();
}
}
static class JOOMObject{}
運行結果:
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:538)
at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
總結
本文首先介紹了堆內存溢出(OutOfMemoryError)發生的場景以及處理方式,OutOfMemoryError發生的場景主要就是系統創建了大量的對象,並且這些對象是有效的(即保證GC Roots到對象之間有可達路徑)。然後,介紹了棧內存異常(StackOverflowError)的發生場景以及處理方式,StackOverflowError發生的場景主要是線程調用棧深度超過了虛擬機運行的棧深度。最後,介紹了方法區的內存溢出。需要注意的是JDK1.8中完全移除了永久代,取而代之的是元空間。