Java記憶體溢位(OOM)分析

2022-09-16 12:01:42
當JVM記憶體不足時,會丟擲java.lang.OutOfMemoryError.
 
主要的OOM型別右:
  • Java heap space:堆空間不足
  • GC overhead limit exceeded : GC開銷超出限制
  • Permgen space:永久代記憶體不足
  • Metaspace:元空間記憶體不足
  • Unable to create new native thread:無法建立新的本地執行緒
  • Out of swap space? : 交換空間記憶體不足
  • Kill process or sacrifice child

 

Java heap space:堆空間不足

  • 通用解決方案:通過-Xmx設定更大的堆記憶體【該方式可能只是延遲報錯的發生,如果不能從根本上找到原因,報錯還是可能會發生】
  • 進一步原因分析及解決方案:
    • 流量/資料量峰值 : 可以考慮新增機器資源,或者做限流
    • 記憶體漏失 : 需要找到持有的物件,修改程式碼
    • 建立了一個超大物件(通常是一個大陣列) : 可以進行業務切分
  • 程式碼範例
    • 記憶體漏失【-Xmx10m】
package oom;

import java.util.HashMap;
import java.util.Map;

/**
* 記憶體洩露
*/
public class JavaHeapSpace2 {

    public static void main(String[] args) {
        Map<Key,String> map = new HashMap<>();
        while (true) {
            Key key = new Key();
            if(!map.containsKey(key)) {
                map.put(key, "Java Overhead");
                System.out.println(key);
            }
        }

    }
}

class Key { }
View Code
oom.Key@2ef70cb4
......
oom.Key@457298d0
oom.Key@484b94f2
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.HashMap.resize(HashMap.java:704)
    at java.util.HashMap.putVal(HashMap.java:663)
    at java.util.HashMap.put(HashMap.java:612)
    at oom.JavaHeapSpace2.main(JavaHeapSpace2.java:16)
View Result
    • 建立了一個超大物件
package oom;

import java.lang.management.ManagementFactory;
import java.util.List;

public class JavaHeapSpace {

    private static final int SIZE = 12 * 1024 * 2014;

    public static void main(String[] args) {
        List<String> inputArguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
        System.out.println("JVM Arguments : " + inputArguments);
        int[] arr = new int[SIZE];
    }
}
View Code
JVM Arguments : [-Xmx12m, -Dfile.encoding=GBK]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at oom.JavaHeapSpace.main(JavaHeapSpace.java:13)

Process finished with exit code 1
View Result[-Xmx12m]
JVM Arguments : [-Xmx35m,-Dfile.encoding=GBK]

Process finished with exit code 0
View Result[-Xmx35m]

 

GC overhead limit exceeded : GC開銷超出限制

預設情況下,當應用程式花費超過98%的時間用來做GC並且回收了不到2%的堆記憶體時,會丟擲java.lang.OutOfMemoryError:GC overhead limit exceeded錯誤。
此類問題的原因與解決方案跟 Java heap space 非常類似,可以參考上文
  • 程式碼演示【使用預設的VM設定】
package oom;

import java.util.HashMap;
import java.util.Map;

public class JavaHeapSpace2 {

    public static void main(String[] args) {
        Map<Key,String> map = new HashMap<>();
        while (true) {
            Key key = new Key();
            if(!map.containsKey(key)) {
                map.put(key, "Java Overhead");
                System.out.println(key);
            }
        }
    }
}

class Key { }
View Code
oom.Key@61f7f66c
oom.Key@1da844d
......
oom.Key@792b37e7
oom.Key@3d8151c0Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at java.nio.CharBuffer.wrap(CharBuffer.java:373)
    at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:265)
    at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
View Result

 

Permgen space:永久代記憶體不足
  • 背景:永久代主要儲存類的資訊,比如:類載入參照、執行時常數池、欄位、方法等。因此,Permgen的大小取決於被載入類的數量及類的大小。
  • 原因:
    • 載入了太多的類
    • 載入了超大類
  • 注意:JDK8已經完全移除永久代空間,取而代之的是元空間(Metaspace)

 

Metaspace:元空間記憶體不足
  • 背景:Metaspace儲存類的後設資料資訊
  • 原因:
    • 載入了太多的類
    • 載入了超大類
  • 解決方案
    • 調整-XX:MaxMetaspaceSize引數
    • 刪除-XX:MaxMetaspaceSize引數,解除限制【預設是沒有限制的。預設情況下,對於64位元伺服器端JVM,MetaspaceSize預設大小是21M(初始限制值),一旦達到這個限制值,FullGC將被觸發進行類解除安裝,並且這個限制值將會被重置,新的限制值依賴於Metaspace的剩餘容量。如果沒有足夠空間被釋放,這個限制值將會上升。】
  • 程式碼演示【JDK1.8】
package oom;

import javassist.CannotCompileException;
import javassist.ClassPool;

import java.lang.management.ManagementFactory;
import java.util.List;

public class Metaspace{

    public static void main(String[] args) throws CannotCompileException {
        List<String> inputArguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
        System.out.println("JVM Arguments : " + inputArguments);

        for (int i = 0; i < 100000000; i++) {
            ClassPool.getDefault().makeClass("User" + i).toClass();
        }
    }
}
View Code
JVM Arguments : [-XX:MaxMetaspaceSize=35m, -Dfile.encoding=GBK]
Exception in thread "main" javassist.CannotCompileException: by java.lang.OutOfMemoryError: Metaspace
    at javassist.ClassPool.toClass(ClassPool.java:1099)
    at javassist.ClassPool.toClass(ClassPool.java:1042)
    at javassist.ClassPool.toClass(ClassPool.java:1000)
    at javassist.CtClass.toClass(CtClass.java:1224)
    at oom.Permgen.main(Permgen.java:16)
Caused by: java.lang.OutOfMemoryError: Metaspace
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
    at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at javassist.ClassPool.toClass2(ClassPool.java:1112)
    at javassist.ClassPool.toClass(ClassPool.java:1093)
    ... 4 more
View Result

 

Unable to create new native thread:無法建立新的本地執行緒
  •  背景:每個執行緒都需要一定的記憶體空間,當JVM向底層作業系統請求建立一個新的native執行緒時,如果沒有足夠的資源分配就會報這個錯誤
  • 原因分析及解決方案:
    • 執行緒數超過了OS最大執行緒數ulimit限制 : 調高 OS 層面的執行緒最大數 - 執行 ulimia-a 檢視最大執行緒數限制,使用 ulimit-u xxx 調整最大執行緒數限制
    • 執行緒數超過了本地執行緒最大數:限制執行緒池大小  ; 
    • native記憶體不足 : 
      • 使用 -Xss 引數減少執行緒棧的大小
      • 升級設定,為機器提供更多的記憶體
  • 程式碼範例
package oom;

public class UnableCreateThread {

    public static void main(String[] args) {
        while (true) {
            new Thread(() -> {
                try {
                    Thread.sleep(100000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
View Code
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x000000006a674447, pid=18648, tid=0x0000000000067b68
#
# JRE version: Java(TM) SE Runtime Environment (8.0_171-b11) (build 1.8.0_171-b11)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.171-b11 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# V  [jvm.dll+0x214447][thread 424944 also had an error]
An unrecoverable stack overflow has occurred.
[thread 424964 also had an error]
An unrecoverable stack overflow has occurred.
[thread 424984 also had an error]
[thread 424992 also had an error]
An unrecoverable stack overflow has occurred.
[thread 424988 also had an error]
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
    at java.lang.Thread.start0(Native Method)
    at java.lang.Thread.start(Thread.java:717)
    at oom.UnableCreateThread.main(UnableCreateThread.java:14)
View Result

 

Out of swap space? : 交換空間記憶體不足
  • 背景:虛擬記憶體(Virtual Memory)由實體記憶體(Physical Memory)和交換空間(Swap Space)兩部分組成。在JVM請求的總記憶體大於可用實體記憶體的情況下,作業系統會將記憶體中的資料交換到磁碟上去。當交換空間也將耗盡時就會報 Outof swap space? 錯誤。
  • 原因分析及解決方案:【往往是由作業系統級別的問題引起的】
    • 作業系統設定的交換空間不足 :
      •  加交換空間【對交換空間執行垃圾回收演演算法會使GC暫停的時間增加幾個數量級,因此使用增加交換空間的方法】
      • 升級機器以包含更多記憶體
    • 系統上的另一個程序消耗所有記憶體資源:如果應用部署在JVM需要同其他程序激烈競爭獲取資源的物理機上,建議將服務隔離到單獨的虛擬機器器中
    • 本地記憶體漏失導致應用程式失敗: 優化應用程式以減少其記憶體佔用
 
Kill process or sacrifice child
  • 背景:作業系統是建立在程序的概念之上,這些程序在核心中作業,其中有一個非常特殊的程序,名叫「記憶體殺手(Out of memory killer)」。當核心檢測到系統記憶體不足時,OOM killer被啟用,然後選擇一個程序殺掉。
  • 原因分析:程式佔用大量系統記憶體導致其他程序沒有可用記憶體
  • 解決方案:
    • 調整OOM Killer設定
    • 升級機器以包含更多記憶體
 
你可以通過修改各種啟動引數來「快速修復」這些記憶體溢位錯誤,但你需要正確區分你是否只是推遲或者隱藏了java.lang.OutOfMemoryError的症狀。如果你的應用程式確實存在記憶體漏失或者本來就載入了一些不合理的類,那麼所有這些設定都只是推遲問題出現的時間而已,實際也不會改善任何東西。
 
 
參考文獻: