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