JVM內(nèi)存管理:OutOfMemoryError異常的問題
《Java 虛擬機(jī)規(guī)范》規(guī)定,除了程序計(jì)數(shù)器,其他幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)都有可能 OOM;OOM 與 VM 本身實(shí)現(xiàn)細(xì)節(jié)密切相關(guān),而非 Java 語言約定的公共行為;
1. Java Heap 溢出
OutOfMemoryError:Java heap space
,Java Heap 用于存儲(chǔ)對(duì)象實(shí)例,只要有足夠的對(duì)象沒有被 GC 清除,總會(huì)觸及 Heap 上限;(-Xmx 參數(shù)決定);
VM Arguments 設(shè)置
# -Xms 表示堆的最小值 # -Xmx 表示堆的最大值 # -XX:+HeapDumpOnOutOfMemoryError 表示打開開關(guān):VM 出現(xiàn) OOM 時(shí) Dump 當(dāng)前內(nèi)存堆轉(zhuǎn)儲(chǔ)快照 -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
示例代碼
循環(huán) new OOMOjbect 對(duì)象,直到用完 -Xmx 設(shè)置的內(nèi)存,從而觸發(fā)堆不夠用的異常;
public class HeapOOM { static class OOMObject { } public static void main(String[] args) { List<OOMObject> list = new ArrayList<>(); while (true) { list.add(new OOMObject()); } } }
運(yùn)行結(jié)果
觸發(fā) OutOfMemoryError: Java heap space
,并 dump 下快照文件 java_pid34676.hprof
;
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid34676.hprof ...
Heap dump file created [27927927 bytes in 0.092 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:267)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
at java.util.ArrayList.add(ArrayList.java:464)
at edu.aurelius.jvm.oom.HeapOOM.main(HeapOOM.java:17)Process finished with exit code 1
Dump 分析
可以通過 JDK 自帶的 Virtual VM
對(duì)程序內(nèi)存進(jìn)行查看和分析,從而發(fā)現(xiàn)一些明顯占用內(nèi)存較多的對(duì)象,判斷是否就是導(dǎo)致 OOM 的對(duì)象;
分析 OOM 問題時(shí)可以從兩方面考慮:內(nèi)存泄漏(Memory Leak,導(dǎo)致 OOM 的對(duì)象本該被 GC 回收,因一些程序編寫錯(cuò)誤導(dǎo)致無法被回收掉)、內(nèi)存溢出(Memory Overflow,導(dǎo)致 OOM 的對(duì)象確實(shí)無法被 GC 回收);
Eclipse Memory Analyzer
的 Leak Suspect 功能可以更方便的幫助開發(fā)者找到內(nèi)存泄漏的可疑對(duì)象;如下圖所示就是發(fā)現(xiàn)了我們故意循環(huán) new 出的 HeapOOM$OOMObject
對(duì)象(點(diǎn)擊 detail 可見 GC Roots 引用鏈);
根據(jù) GC Roots 引用鏈,找到破壞引用路徑的引用從而消除內(nèi)存泄漏問題;
若分析得出導(dǎo)致 OOM 的對(duì)象是有必要存在的,則需要從資源層面(如 JVM 堆參數(shù) -Xmx、-Xms,物理可用內(nèi)存)和代碼層面(存儲(chǔ)結(jié)構(gòu)、對(duì)象生命周期、引用關(guān)系時(shí)間長(zhǎng)短等)進(jìn)行擴(kuò)展或優(yōu)化了;
2. Java VM Stack 與 Native Method Stack 溢出
StackOverflowError
,HotSpot VM 并不區(qū)分虛擬機(jī)棧和本地方法棧,對(duì) HotSpot 來說,-Xoss(設(shè)置本地方法棧大小的參數(shù))實(shí)際是無效的,棧容量只能通過 -Xss 參數(shù)來設(shè)置;
《Java 虛擬機(jī)規(guī)范》描述了兩種棧溢出異常:
StackOverflowError
,線程請(qǐng)求的棧深度大于 VM 所運(yùn)行的最大深度(Xss 允許的內(nèi)存大小用盡);OutOfMemoryError
,當(dāng) VM 允許動(dòng)態(tài)擴(kuò)展棧內(nèi)存,而擴(kuò)展棧時(shí)無法申請(qǐng)到足夠的內(nèi)存(進(jìn)程最大內(nèi)存
減去 Heap 最大容量、方法區(qū)最大容量、直接內(nèi)存大小、JVM 本身消耗內(nèi)存、程序計(jì)數(shù)器大小,剩下的就是給到所有棧的);
演示 1: 限制棧容量
VM Arguments 設(shè)置
# 設(shè)置 VM Stack 大??; -Xss160k
不同版本的 JVM 和不同操作系統(tǒng)的棧容量最大大小限制可能不一樣,若設(shè)置的大小小于限制,會(huì)得到如下提示信息:
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.The stack size specified is too small, Specify at least 160k
Process finished with exit code 1
示例代碼
運(yùn)行一段無限遞歸的代碼,用于觸發(fā)棧容量不夠用的異常;
public class JVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) { JVMStackSOF oom = new JVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); throw e; } } }
運(yùn)行結(jié)果
stack length:774
Exception in thread "main" java.lang.StackOverflowError
at edu.aurelius.jvm.oom.JVMStackSOF.stackLeak(JVMStackSOF.java:11)
at edu.aurelius.jvm.oom.JVMStackSOF.stackLeak(JVMStackSOF.java:12)
...
at edu.aurelius.jvm.oom.JVMStackSOF.stackLeak(JVMStackSOF.java:12)
at edu.aurelius.jvm.oom.JVMStackSOF.main(JVMStackSOF.java:18)Process finished with exit code 1
體現(xiàn)了棧容量太小觸發(fā) StackOverflowError;
演示 2: 定義大量本地變量
示例代碼
在代碼中添加 100 個(gè)本地變量,然后進(jìn)行無限遞歸,觸發(fā)棧容量不夠用的異常;
public class JVMStackSOF { private int stackLength = 1; public void stackLeak() { 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++; stackLeak(); 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) { JVMStackSOF oom = new JVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); throw e; } } }
運(yùn)行結(jié)果
stack length:41
Exception in thread "main" java.lang.StackOverflowError
at edu.aurelius.jvm.oom.JVMStackSOF.stackLeak(JVMStackSOF.java:12)
at edu.aurelius.jvm.oom.JVMStackSOF.stackLeak(JVMStackSOF.java:13)
...
at edu.aurelius.jvm.oom.JVMStackSOF.stackLeak(JVMStackSOF.java:13)
at edu.aurelius.jvm.oom.JVMStackSOF.main(JVMStackSOF.java:20)Process finished with exit code 1
體現(xiàn)了棧幀過大觸發(fā) StackOverflowError;與 示例 1 對(duì)比可見,當(dāng)棧容量不變,棧幀變大時(shí),棧的深度(stack length)明顯變小了;
如果是可以動(dòng)態(tài)擴(kuò)展棧大小的虛擬機(jī)(比如 Classic VM),可能會(huì)導(dǎo)致 OutOfMemoryError 而非 StackOverflowError 的結(jié)果;
演示 3: 線程過多
VM Arguments 設(shè)置
# 將 VM Stack 大小設(shè)置得大一些; -Xss2M
示例代碼
不斷建立不會(huì)終止的線程(可能會(huì)導(dǎo)致操作系統(tǒng)死機(jī),謹(jǐn)慎操作),把 JVM Stack 和 Native Method Stack 可支配的內(nèi)存耗盡,從而觸發(fā)無法創(chuàng)建本地線程的異常;
public class JVMStackSOF { private void dontStop() { while (true) { } } public void stackLeakByThread() { while (true) { new Thread(() -> dontStop()).start(); } } public static void main(String[] args) { JVMStackSOF oom = new JVMStackSOF(); oom.stackLeakByThread(); } }
運(yùn)行結(jié)果
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread
如果建立過多內(nèi)存會(huì)導(dǎo)致內(nèi)存溢出,而又無法減少線程數(shù)量或增加可用內(nèi)存,可以從限制棧容量、減少堆最大大小的方向考慮優(yōu)化 unable to create native thread
;
3. Method Area 與 Runtime Constant Pool 溢出
使用 永久代 或 元空間 來實(shí)現(xiàn)方法區(qū),對(duì)方法區(qū)內(nèi)存溢出表現(xiàn)是有影響的;
演示 1: 限制永久代容量
VM Arguments 設(shè)置
# 設(shè)置 Java 堆永久代大小為 6 M; -XX:PermSize=6M -XX:MaxPermSize=6M
示例代碼
通過持續(xù)往常量池添加 i 的字符串常量,用光 6MB 的永久代,從而觸發(fā)永久代空間不足的異常;
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()); } } }
運(yùn)行結(jié)果
JDK 6 下的運(yùn)行結(jié)果如下:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at edu.aurelius.jvm.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 15)
而在 JDK 7 或更高的版本中(無論是添加 -XX:MaxPermSize 限制,還是添加 JDK 8 才有的 -XX:MaxMetaspaceSize 限制)循環(huán)將會(huì)持續(xù)進(jìn)行下去,直至 Heap 溢出(永久代的字符串常量池被移至 Java Heap);可能是常量分配導(dǎo)致 Heap 溢出,也可能是 Set 擴(kuò)容導(dǎo)致 Heap 溢出;
示例 2: String.intern() 影響
String.intern()
調(diào)用的作用是將 首次遇到 的字符串放入字符串常量池;在 JDK 6 中,是將字符串復(fù)制一份到永久代的字符串常量池存儲(chǔ)起來,并返回該字符串實(shí)例的引用;在 JDK 7 后,是將字符串實(shí)例的引用記錄到堆中字符串常量池中;
示例代碼
通過 StringBuilder::new 實(shí)例化兩個(gè)字符串,并通過 String.intern()
調(diào)用獲取字符串與原字段進(jìn)行比較;
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);
運(yùn)行結(jié)果
- JDK 6: false(永久代常量對(duì)象引用 vs. 堆對(duì)象引用);false(永久代常量對(duì)象引用 vs. 堆對(duì)象引用);
- JDK 7: true(堆對(duì)象引用 vs. 堆對(duì)象引用);false(堆對(duì)象引用 vs. 堆對(duì)象引用);
示例 3: 模擬方法區(qū)溢出
VM Arguments 設(shè)置
# 設(shè)置 Java 堆永久代大小為 10 M - JDK 7 -XX:PermSize=10M -XX:MaxPermSize=10M # 設(shè)置元空間大小為 10 M - JDK 8 -XX:MetaspaceSize=10485760 -XX:MaxMetaspaceSize=10485760 # -XX:MinMetaspaceFreeRatio:GC 之后控制最小元空間剩余容量的百分比,減少 GC 頻率; # -XX:Max-MetaspaceFreeRatio:GC 之后控制最大云空間剩余容量的百分比;
-XX:MetaspaceSize
即元空間初始容量,當(dāng)方法區(qū)使用達(dá)到該值會(huì)觸發(fā) GC 進(jìn)行類型卸載,同時(shí)會(huì)調(diào)整該值;若釋放大量空間,則降低該值,若釋放少量空間,則在不超過 -XX:MaxMetaspaceSize
(默認(rèn) -1,表示不限制)的情況下提高該值;
示例代碼
借助 CGLib 直接操作字節(jié)碼運(yùn)行時(shí)生成大量動(dòng)態(tài)代碼,從而觸發(fā)方法區(qū)內(nèi)存溢出異常;
public class JavaMethodAreaOOM { public static void main(String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1)); enhancer.create(); } } static class OOMObject { } }
運(yùn)行結(jié)果
JDK 7 運(yùn)行提示永久代溢出
Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632) at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
...
JDK 8 運(yùn)行提示元空間溢出
Caused by: java.lang.OutOfMemoryError: Metaspace
...
4. Direct Memory 溢出
VM Arguments 設(shè)置
# 設(shè)置 Java 堆最大 20 M,直接內(nèi)存最大 10 M; -Xmx20M -XX:MaxDirectMemorySize=10M
若不指定 -XX:MaxDirectMemorySize
,直接內(nèi)存容量將與 Java Heap 容量一致;
示例代碼
代碼通過反射獲取 Unsafe 實(shí)例直接進(jìn)行內(nèi)存分配(越過了 DirectByteBuffer 類,DirectByteBuffer 分配內(nèi)存時(shí),通過計(jì)算得知內(nèi)存不足就會(huì)拋出溢出異常,不會(huì)真正向操作系統(tǒng)申請(qǐng)內(nèi)存),從而觸發(fā)直接內(nèi)存不夠的異常;
public class DirectMemoryOOM { private static final int _5MB = 1024 * 1024 * 5; 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(_5MB); } } }
運(yùn)行結(jié)果
[謹(jǐn)慎操作] -XX:MaxDirectMemorySize=10M
的限制只對(duì) ByteBuffer.allocateDirect(size)
或 new DirectByteBuffer(capacity)
的分配方式有效,對(duì) unsafe.allocateMemory(size) 并無限制,如上代碼會(huì)持續(xù)分配內(nèi)存直至讓操作系統(tǒng)死機(jī);
若使用 ByteBuffer.allocateDirect(size)
方法申請(qǐng)直接內(nèi)存,可得到如下結(jié)果:
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:695)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at edu.aurelius.jvm.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20)
5. GC 頻繁
OutOfMemoryError:GC overhead limit exceeded
當(dāng)超過 98% 的時(shí)間在 GC 時(shí),也會(huì)拋出 OOM 異常;
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java中的getClass()以及getName()方法使用
這篇文章主要介紹了Java中的getClass()以及getName()方法使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12詳解Java構(gòu)建樹結(jié)構(gòu)的公共方法
本文主要介紹了詳解Java構(gòu)建樹結(jié)構(gòu)的公共方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04Java中Stringbuild,Date和Calendar類的用法詳解
這篇文章主要為大家詳細(xì)介紹了Java中Stringbuild、Date和Calendar類的用法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-04-04Java二進(jìn)制操作(動(dòng)力節(jié)點(diǎn)Java學(xué)院整理)
這篇文章給大家介紹了java二進(jìn)制操作技巧,包括移位、位運(yùn)算操作符等相關(guān)知識(shí)點(diǎn),非常不錯(cuò),感興趣的朋友參考下吧2017-03-03Java連接PostgreSql數(shù)據(jù)庫及基本使用方式
這篇文章主要介紹了Java連接PostgreSql數(shù)據(jù)庫及基本使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03解讀動(dòng)態(tài)數(shù)據(jù)源dynamic-datasource-spring-boot-starter使用問題
這篇文章主要介紹了解讀動(dòng)態(tài)數(shù)據(jù)源dynamic-datasource-spring-boot-starter使用問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03Mybatis中的@Param及動(dòng)態(tài)SQL詳解
這篇文章主要介紹了Mybatis中的@Param及動(dòng)態(tài)SQL詳解,@Param是MyBatis所提供的作為Dao層的注解,作用是用于傳遞參數(shù),從而可以與SQL中的的字段名相對(duì)應(yīng),需要的朋友可以參考下2023-10-10