JVM 實(shí)戰(zhàn) OutOfMemoryError 異常
在《Java虛擬機(jī)規(guī)范》的規(guī)定里,除了程序計(jì)數(shù)器外,虛擬機(jī)內(nèi)存的其他幾個(gè)運(yùn)行時(shí)區(qū)域都有發(fā)生OutOfMemoryError(下文稱OOM)異常的可能。(本文主要是基于 jdk1.8 展開探討)
Java 堆溢出
Java堆用于儲(chǔ)存對(duì)象實(shí)例,我們只要不斷地創(chuàng)建對(duì)象,并且保證GC Roots到對(duì)象之間有可達(dá)路徑來避免垃圾回收機(jī)制清除這些對(duì)象,那么隨著對(duì)象數(shù)量的增加,總?cè)萘坑|及最大堆的容量限制后就會(huì)產(chǎn)生內(nèi)存溢出異常。
模擬代碼
下面是簡單的模擬堆內(nèi)存溢出的代碼:
- /**
 - * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 - * @author zhengsh
 - * @date 2021-8-13
 - */
 - public class HeapOOM {
 - public static void main(String[] args) {
 - List<byte[]> list = new ArrayList<>();
 - while (true) {
 - list.add(new byte[2048]);
 - }
 - }
 - }
 
返回結(jié)果信息如下所示:
- Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
 - at cn.zhengsh.jvm.oom.HeapOOM.main(HeapOOM.java:16)
 
問題分析
我們需要定位是內(nèi)存泄漏(Memory Leak)還是,內(nèi)存溢出(Memory Overflow)
- 內(nèi)存泄漏
 - 內(nèi)存溢出
 
內(nèi)存泄漏
我們可以通過 jdk 自帶的 jvisualvm 工具來加載堆快照文件進(jìn)行分析。如果是內(nèi)存泄漏,可進(jìn)一步通過工具查看泄漏對(duì)象到GC Roots的引用鏈,找到泄漏對(duì)象是通過怎樣的引用路徑、與哪些GC Roots相關(guān)聯(lián),才導(dǎo)致垃圾收集器無法回收它們,根據(jù)泄漏對(duì)象的類型信息 以及它到GC Roots引用鏈的信息,一般可以比較準(zhǔn)確地定位到這些對(duì)象創(chuàng)建的位置,進(jìn)而找出產(chǎn)生內(nèi)存泄漏的代碼的具體位置。
內(nèi)存溢出
如果不是內(nèi)存泄漏,換句話說就是內(nèi)存中的對(duì)象確實(shí)都是必須存活的,那就應(yīng)當(dāng)檢查Java虛擬機(jī)的堆參數(shù)(-Xmx與-Xms)設(shè)置,與機(jī)器的內(nèi)存對(duì)比,看看是否還有向上調(diào)整的空間。再從代碼上檢查是否存在某些對(duì)象生命周期過長、持有狀態(tài)時(shí)間過長、存儲(chǔ)結(jié)構(gòu)設(shè)計(jì)不合理等情況,盡量減少程序運(yùn) 行期的內(nèi)存消耗。
虛擬機(jī)棧和本地方法棧溢出
HotSpot虛擬機(jī)中并不區(qū)分虛擬機(jī)棧和本地方法棧,因此對(duì)于HotSpot來說,-Xoss參數(shù)(設(shè)置 本地方法棧大小)雖然存在,但實(shí)際上是沒有任何效果的,棧容量只能由-Xss參數(shù)來設(shè)定。關(guān)于虛擬機(jī)棧和本地方法棧,在《Java虛擬機(jī)規(guī)范》中描述了兩種異常:
- 如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError異常。
 - 如果虛擬機(jī)的棧內(nèi)存允許動(dòng)態(tài)擴(kuò)展,當(dāng)擴(kuò)展棧容量無法申請(qǐng)到足夠的內(nèi)存時(shí),將拋出 OutOfMemoryError異常。
 
《Java虛擬機(jī)規(guī)范》明確允許Java虛擬機(jī)實(shí)現(xiàn)自行選擇是否支持棧的動(dòng)態(tài)擴(kuò)展,而HotSpot虛擬機(jī) 的選擇是不支持?jǐn)U展,所以除非在創(chuàng)建線程申請(qǐng)內(nèi)存時(shí)就因無法獲得足夠內(nèi)存而出現(xiàn) OutOfMemoryError異常,否則在線程運(yùn)行時(shí)是不會(huì)因?yàn)閿U(kuò)展而導(dǎo)致內(nèi)存溢出的,只會(huì)因?yàn)闂H萘繜o法 容納新的棧幀而導(dǎo)致StackOverflowError異常。
虛擬機(jī)棧內(nèi)存溢出
StackOverflowError
示例代碼:
- /**
 - * VM Args:-Xss128k
 - *
 - * @author zhengsh
 - * @date 2021-08-13
 - */
 - 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:992
 - at cn.zhengsh.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
 - at cn.zhengsh.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:14)
 - //.... 省略更多
 
OutOfMemoryError
- package cn.zhengsh.jvm.oom;
 - /**
 - * @author zhengsh
 - * @date 2021-08-13
 - */
 - public class JavaVMStackSOF2 {
 - private static int stackLength = 0;
 - public static void test() {
 - long unused1, unused2, unused3, unused4, unused5, unused6, unused7, unused8, unused9, unused10, unused11,
 - unused12, unused13, unused14, unused15, unused16, unused17, unused18, unused19, unused20, unused21,
 - unused22, unused23, unused24, unused25, unused26, unused27, unused28, unused29, unused30, unused31,
 - unused32, unused33, unused34, unused35, unused36, unused37, unused38, unused39, unused40, unused41,
 - unused42, unused43, unused44, unused45, unused46, unused47, unused48, unused49, unused50, unused51,
 - unused52, unused53, unused54, unused55, unused56, unused57, unused58, unused59, unused60, unused61,
 - unused62, unused63, unused64, unused65, unused66, unused67, unused68, unused69, unused70, unused71,
 - unused72, unused73, unused74, unused75, unused76, unused77, unused78, unused79, unused80, unused81,
 - unused82, unused83, unused84, unused85, unused86, unused87, unused88, unused89, unused90, unused91,
 - unused92, unused93, unused94, unused95, unused96, unused97, unused98, unused99, unused100;
 - stackLength++;
 - test();
 - unused1 = unused2 = unused3 = unused4 = unused5 = unused6 = unused7 = unused8 = unused9 = unused10 = unused11 =
 - unused12 = unused13 = unused14 = unused15 = unused16 = unused17 = unused18 = unused19 = unused20 =
 - unused21 = unused22 = unused23 = unused24 = unused25 = unused26 = unused27 = unused28 = unused29 =
 - unused30 = unused31 = unused32 = unused33 = unused34 = unused35 = unused36 = unused37 = unused38 =
 - unused39 = unused40 = unused41 = unused42 = unused43 =
 - unused44 = unused45 = unused46 = unused47 = unused48 = unused49 = unused50 = unused51 =
 - unused52 = unused53 = unused54 = unused55 = unused56 = unused57 = unused58 = unused59 =
 - unused60 = unused61 = unused62 = unused63 = unused64 = unused65 = unused66 =
 - unused67 = unused68 = unused69 = unused70 = unused71 = unused72 = unused73 =
 - unused74 = unused75 = unused76 = unused77 = unused78 = unused79 = unused80 =
 - unused81 = unused82 = unused83 = unused84 = unused85 = unused86 =
 - unused87 = unused88 = unused89 = unused90 = unused91 = unused92 =
 - unused93 = unused94 = unused95 =
 - unused96 = unused97 = unused98 = unused99 = unused100 = 0;
 - }
 - public static void main(String[] args) {
 - try {
 - test();
 - } catch (Error e) {
 - System.out.println("stack length:" + stackLength);
 - throw e;
 - }
 - }
 - }
 
輸出結(jié)果:
- stack length:6986
 - Exception in thread "main" java.lang.StackOverflowError
 - at cn.zhengsh.jvm.oom.JavaVMStackSOF2.test(JavaVMStackSOF2.java:22)
 - at cn.zhengsh.jvm.oom.JavaVMStackSOF2.test(JavaVMStackSOF2.java:22)
 
總結(jié)
無論是由于棧幀太大還是虛擬機(jī)棧容量太小,當(dāng)新的棧幀內(nèi)存無法分配的時(shí)候, HotSpot虛擬機(jī)拋出的都是StackOverflowError異常??墒侨绻谠试S動(dòng)態(tài)擴(kuò)展棧容量大小的虛擬機(jī)上,相同代碼則會(huì)導(dǎo)致不一樣的情況。
創(chuàng)建線程導(dǎo)致內(nèi)存溢出
注意:下面的這個(gè)實(shí)驗(yàn)可能導(dǎo)致操作系統(tǒng)卡死,建議大家在虛擬機(jī)中執(zhí)行
- /**
 - * VM Args:-Xss512k
 - *
 - * @author zhengsh
 - * @date 2021-08-13
 - */
 - 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();
 - }
 - }
 
方法區(qū)和運(yùn)行時(shí)常量池溢出
由于運(yùn)行時(shí)常量池是方法區(qū)的一部分,所以這兩個(gè)區(qū)域的溢出測(cè)試可以放到一起進(jìn)行。HotSpot從JDK 7 開始逐步“去永久代”的計(jì)劃,并在JDK 8中完全使用元空間來代替永久代。
方法區(qū)內(nèi)存溢出
方法區(qū)的主要職責(zé)是用于存放類型的相關(guān)信息,如類 名、訪問修飾符、常量池、字段描述、方法描述等。對(duì)于這部分區(qū)域的測(cè)試,基本的思路是運(yùn)行時(shí)產(chǎn) 生大量的類去填滿方法區(qū),直到溢出為止。雖然直接使用Java SE API也可以動(dòng)態(tài)產(chǎn)生類(如反射時(shí)的 GeneratedConstructorAccessor和動(dòng)態(tài)代理等),但在本次實(shí)驗(yàn)中借助了CGLib直接操作字節(jié)碼運(yùn)行時(shí)生成了大量的動(dòng)態(tài)類。
- /**
 - * VM Args:-XX:MetaspaceSize=21m -XX:MaxMetaspaceSize=21m
 - *
 - * @author zhengsh
 - * @date 2021-08-13
 - */
 - 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() {
 - @Override
 - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
 - return proxy.invokeSuper(obj, args);
 - }
 - });
 - enhancer.create();
 - }
 - }
 - static class OOMObject {
 - }
 - }
 
輸出代碼
- Caused by: java.lang.OutOfMemoryError: Metaspace
 - Caused by: java.lang.OutOfMemoryError: Metaspace
 
常量池案例
String::intern()是一個(gè)本地方法,它的作用是如果字符串常量池中已經(jīng)包含一個(gè)等于此String對(duì)象的 字符串,則返回代表池中這個(gè)字符串的String對(duì)象的引用;否則,會(huì)將此String對(duì)象包含的字符串添加 到常量池中,并且返回此String對(duì)象的引用。在JDK 6或更早之前的HotSpot虛擬機(jī)中,常量池都是分配在永久代中,我們可以通過-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可間接限制其中常量池的容量。
- /**
 - * @author zhengsh
 - * @date 2021-08-13
 - */
 - public class RuntimeConstantPoolOOM2 {
 - public static void main(String[] args) {
 - String str1 = new StringBuilder("計(jì)算機(jī)").append("軟件").toString();
 - System.out.println(str1.intern() == str1);
 - String str2 = new StringBuilder("ja").append("va").toString();
 - System.out.println(str2.intern() == str2);
 - }
 - }
 
這段代碼在JDK 6中運(yùn)行,會(huì)得到兩個(gè)false,而在JDK 7中運(yùn)行,會(huì)得到一個(gè)true和一個(gè)false。產(chǎn)生差異的原因是,在JDK 6中,intern()方法會(huì)把首次遇到的字符串實(shí)例復(fù)制到永久代的字符串常量池中存儲(chǔ),返回的也是永久代里面這個(gè)字符串實(shí)例的引用,而由StringBuilder創(chuàng)建的字符串對(duì)象實(shí)例在 Java堆上,所以必然不可能是同一個(gè)引用,結(jié)果將返回 false。而JDK 7(以及部分其他虛擬機(jī),例如JRockit)的intern()方法實(shí)現(xiàn)就不需要再拷貝字符串的實(shí)例到永久代了,既然字符串常量池已經(jīng)移到Java堆中,那只需要在常量池里記錄一下首次出現(xiàn)的實(shí)例引用即可,因此intern()返回的引用和由StringBuilder創(chuàng)建的那個(gè)字符串實(shí)例就是同一個(gè)。而對(duì)str2比較返 回false,這是因?yàn)?ldquo;java”這個(gè)字符串在執(zhí)行String-Builder.toString()之前就已經(jīng)出現(xiàn)過了,字符串常量 池中已經(jīng)有它的引用,不符合intern()方法要求“首次遇到”的原則,“計(jì)算機(jī)軟件”這個(gè)字符串則是首次出現(xiàn)的,因此結(jié)果返回true。
本機(jī)直接內(nèi)存溢出
直接內(nèi)存(Direct Memory)的容量大小可通過 -XX:MaxDirectMemorySize參數(shù)來指定,默認(rèn)與Java堆最大值(由-Xmx指定)一致,代碼越過了DirectByteBuffer類直接通過反射獲取Unsafe實(shí)例進(jìn)行內(nèi)存分配(Unsafe類的getUnsafe()方法指定只有引導(dǎo)類加載器才會(huì)返回實(shí)例,體現(xiàn)了設(shè)計(jì)者希望只有虛擬機(jī)標(biāo)準(zhǔn)類庫里面的類才能使用Unsafe的功能,在JDK 10時(shí)才將Unsafe 的部分功能通過VarHandle開放給外部使用),因?yàn)殡m然 DirectByteBuffer分配內(nèi)存也會(huì)拋出內(nèi)存溢出異常,但它拋出異常時(shí)并沒有真正向操作系統(tǒng)申請(qǐng)分配內(nèi)存,而是通過計(jì)算得知內(nèi)存無法分配就會(huì)在代碼里手動(dòng)拋出溢出異常,真正申請(qǐng)分配內(nèi)存的方法是 Unsafe::allocateMemory()。
- /**
 - * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
 - *
 - * @author zhengsh
 - * @date 2021-08-13
 - */
 - 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);
 - }
 - }
 - }
 
輸出內(nèi)容:
- Exception in thread "main" java.lang.OutOfMemoryError
 - Exception in thread "main" java.lang.OutOfMemoryError
 - at java.base/jdk.internal.misc.Unsafe.allocateMemory(Unsafe.java:616)
 - at jdk.unsupported/sun.misc.Unsafe.allocateMemory(Unsafe.java:462)
 - at cn.zhengsh.jvm.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:21)
 
參考資料
《深入理解 JVM 虛擬機(jī)-第三版》 周志明
https://docs.oracle.com/javase/specs/jls/se8/html/index.html
















 
 
 









 
 
 
 