引言
在系列文章的第一篇中,我們就瞭解到,除了程序計數器外,虛擬機內存的其他幾個運行時區域都有發生OutOfMemoryError(下文稱OOM)異常的可能。本篇博客將通過代碼示例來再現異常情景,最後也會總結若干最基本的與自動內存管理子系統相關的HotSpot虛擬機參數。
異常場景
首先,我們可以在開發工具中設置一些JVM運行參數,如最大堆、最小堆等,這對示例代碼的結果會有直接影響。以IDEA開發工具來說,我們在Edit Configurations對話框中的VM Options一項中進行設置,如下:
1. Java堆溢出
Java堆用於儲存對象實例,我們只要不斷地創建對象,並且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼隨着對象數量的增加,總容量觸及最大堆的容量限制後就會產生內存溢出異常。
示例代碼:
/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @Description : java堆溢出
* @Author : huzhiting
* @Date: 2020-06-04 16:59
*/
public class HeapOOM {
private static class OOMObject{}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true){
list.add(new OOMObject());
}
}
}
運行結果:
2. 虛擬機棧和本地方法棧溢出
由於HotSpot虛擬機中並不區分虛擬機棧和本地方法棧,因此對於HotSpot來說,-Xoss參數(設置本地方法棧大小)雖然存在,但實際上是沒有任何效果的,棧容量只能由-Xss參數來設定。
回顧一下可能發生的兩種異常情況:
(1)如果線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverflowError異常。
(2)如果虛擬機的棧內存允許動態擴展,當擴展棧容量無法申請到足夠的內存時,將拋出OutOfMemoryError異常。
而HotSpot虛擬機的選擇是不支持擴展,所以除非在創建線程申請內存時就因無法獲得足夠內存而出現OutOfMemoryError異常,否則在線程運行時是不會因爲擴展而導致內存溢出的。
示例代碼:
/**
* VM Args:-Xss128K
* @Description : java棧溢出
* @Author : huzhiting
* @Date: 2020-06-04 17:48
*/
public class JavaVMStackSOF {
private int stackLength=1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
運行結果:
3. 方法區和運行時常量池溢出
String::intern()是一個本地方法,它的作用是如果字符串常量池中已經包含一個等於此String對象的字符串,則返回代表池中這個字符串的String對象的引用;否則,會將此String對象包含的字符串添加到常量池中,並且返回此String對象的引用。
在JDK 6或更早之前的HotSpot虛擬機中,常量池都是分配在永久代中,我們可以通過-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可間接限制其中常量池的容量。
示例代碼:
/**
* VM args: -XX:PermSize=6M -XX:MaxPermSize=6M
* @Description : 運行時常量池溢出
* @Author : huzhiting
* @Date: 2020-06-04 18:16
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
short i =0;
while (true){
set.add(String.valueOf(i++).intern());
}
}
}
運行結果:
從運行結果中可以看到,運行時常量池溢出時,在OutOfMemoryError異常後面跟隨的提示信息是“PermGen space”,說明運行時常量池的確是屬於方法區(即JDK 6的HotSpot虛擬機中的永久代)的一部分。
而使用JDK 7或更高版本的JDK來運行這段程序並不會得到相同的結果,無論是在JDK 7中繼續使用-XX:MaxPermSize參數或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize參數把方法區容量同樣限制在6MB,也都不會重現JDK 6中的溢出異常。
出現這種變化,是因爲自JDK 7起,原本存放在永久代的字符串常量池被移至Java堆之中。這時候使用-Xmx參數限制最大堆到6MB就能夠看到以下運行結果:
在JDK 8以後,永久代便完全退出了歷史舞臺,元空間作爲其替代者登場。所以在默認設置下,虛擬機很難產生方法區溢出的異常現象,這裏就不再用代碼進行測試了。
爲了避免一些破壞性操作,HotSpot提供了一些參數作爲元空間的防禦措施,主要包括:
- -XX:MaxMetaspaceSize:設置元空間最大值,默認是-1,即不限制,或者說只受限於本地內存大小。
- -XX:MetaspaceSize:指定元空間的初始空間大小,以字節爲單位,達到該值就會觸發垃圾收集進行類型卸載,同時收集器會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過-XX:MaxMetaspaceSize(如果設置了的話)的情況下,適當提高該值。
- -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之後控制最小的元空間剩餘容量的百分比,可減少因爲元空間不足導致的垃圾收集的頻率。類似的還有-XX:Max-MetaspaceFreeRatio,用於控制最大的元空間剩餘容量的百分比。
4. 本機直接內存溢出
直接內存(Direct Memory)的容量大小可通過-XX:MaxDirectMemorySize參數來指定,如果不去指定,則默認與Java堆最大值(由-Xmx指定)一致。
溢出運行結果如下:
總結
在有了一定工作經驗的基礎上,相信本篇博客的核心OutOfMemoryError也是不少人都遇到過的。
線上如果出現此類問題,一定是劃定爲線上事故了。所以,作爲一名有經驗的開發者,在遇到該類問題如何快速定位問題,如何性能調優等等,都是必須要掌握的。
本文只是通過一些示例代碼列舉異常現象,後續還會針對JVM參數及問題排查過程進行單獨總結,持續學習中…