Java程序員必備:常見OOM異常分析

前言

放假這幾天,溫習了深刻理解Java虛擬機的第二章, 整理了JVM發生OOM異常的幾種狀況,並分析緣由以及解決方案,但願對你們有幫助。html

Java 堆溢出

Java堆用於存儲對象實例,只要不斷地建立對象,而且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼在對象數量到達最大堆的容量限制後就會產生內存溢出異常。java

Java 堆溢出緣由

  • 沒法在 Java 堆中分配對象
  • 應用程序保存了沒法被GC回收的對象。
  • 應用程序過分使用 finalizer。

Java 堆溢出排查解決思路

  1. 查找關鍵報錯信息,如
java.lang.OutOfMemoryError: Java heap space
複製代碼
  1. 使用內存映像分析工具(如Eclipsc Memory Analyzer或者Jprofiler)對Dump出來的堆儲存快照進行分析,分析清楚是內存泄漏仍是內存溢出。
  2. 若是是內存泄漏,可進一步經過工具查看泄漏對象到GC Roots的引用鏈,修復應用程序中的內存泄漏。
  3. 若是不存在泄漏,先檢查代碼是否有死循環,遞歸等,再考慮用 -Xmx 增長堆大小。

demo代碼

package oom;

import java.util.ArrayList;
import java.util.List;

/**
 * JVM配置參數
 * -Xms20m    JVM初始分配的內存20m
 * -Xmx20m   JVM最大可用內存爲20m
 * -XX:+HeapDumpOnOutOfMemoryError 當JVM發生OOM時,自動生成DUMP文件
 * -XX:HeapDumpPath=/Users/weihuaxiao/Desktop/dump/  生成DUMP文件的路徑
 */
public class HeapOOM {
    static class OOMObject {
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        //在堆中無限建立對象
        while (true) {
            list.add(new OOMObject());
        }
    }
}

複製代碼

運行結果

按照前面的排查解決方案,咱們來一波分析。spring

1.查找報錯關鍵信息bash

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
複製代碼

2. 使用內存映像分析工具Jprofiler分析產生的堆儲存快照jsp

由圖可得,OOMObject這個類建立了810326個實例,是屬於內存溢出,這時候先定位到對應代碼,發現死循環致使的,修復便可。函數

棧溢出

關於虛擬機棧和本地方法棧,在Java虛擬機規範中描述了兩種異常:工具

  • 若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError 異常;
  • 若是虛擬機棧能夠動態擴展,當擴展時沒法申請到足夠的內存時會拋出 OutOfMemoryError 異常。

棧溢出緣由

  • 在單個線程下,棧幀太大,或者虛擬機棧容量過小,當內存沒法分配的時候,虛擬機拋出StackOverflowError 異常。
  • 不斷地創建線程的方式會致使內存溢出。

棧溢出排查解決思路

  1. 查找關鍵報錯信息,肯定是StackOverflowError仍是OutOfMemoryError
  2. 若是是StackOverflowError,檢查代碼是否遞歸調用方法等
  3. 若是是OutOfMemoryError,檢查是否有死循環建立線程等,經過-Xss下降的每一個線程棧大小的容量

demo代碼

package oom;

/**
 * -Xss2M
 */
public class JavaVMStackOOM {
    private void dontStop(){
        while(true){

        }
    }
    public void stackLeakByThread(){
        while(true){
            Thread thread = new Thread(new Runnable(){
                public void run() {
                    dontStop();
                }
            });
            thread.start();}
    }
    public static void main(String[] args) {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}
複製代碼

運行結果

1.查找報錯關鍵信息post

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
複製代碼

2.肯定是建立線程致使的棧溢出OOM性能

Thread thread = new Thread(new Runnable(){
                public void run() {
                    dontStop();
                }
            });
複製代碼

3.排查代碼,肯定是否顯示使用死循環建立線程,或者隱式調用第三方接口建立線程(以前公司,調用騰訊雲第三方接口,上傳圖片,遇到這個問題)學習

方法區溢出

方法區,(又叫永久代,JDK8後,元空間替換了永久代),用於存放Class的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。運行時產生大量的類,會填滿方法區,形成溢出。

方法區溢出緣由

  • 使用CGLib生成了大量的代理類,致使方法區被撐爆
  • 在Java7以前,頻繁的錯誤使用String.intern方法
  • 大量jsp和動態產生jsp
  • 應用長時間運行,沒有重啓

方法區溢出排查解決思路

  • 檢查是否永久代空間設置得太小
  • 檢查代碼是否頻繁錯誤得使用String.intern方法
  • 檢查是否跟jsp有關。
  • 檢查是否使用CGLib生成了大量的代理類
  • 重啓大法,重啓JVM

demo代碼

package oom;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 *  jdk8以上的話,
 *  虛擬機參數:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M 
 */
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() {
                public Object intercept(Object obj, Method method,
                                        Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }
    static class OOMObject {
    }
}
複製代碼

運行結果

1.查找報錯關鍵信息

Caused by: java.lang.OutOfMemoryError: Metaspace
複製代碼

2.檢查JVM元空間設置參數是否太小

-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M 
複製代碼

3. 檢查對應代碼,是否使用CGLib生成了大量的代理類

while (true) {
  ...
   enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object obj, Method method,
                                        Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
  }
複製代碼

本機直接內存溢出

直接內存並非虛擬機運行時數據區的一部分,也不是Java 虛擬機規範中定義的內存區域。可是,這部份內存也被頻繁地使用,並且也可能致使OOM。

在JDK1.4 中新加入了NIO(New Input/Output)類,它可使用 native 函數庫直接分配堆外內存,而後經過一個存儲在Java堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在 Java 堆和 Native 堆中來回複製數據。

直接內存溢出緣由

  • 本機直接內存的分配雖然不會受到Java 堆大小的限制,可是受到本機總內存大小限制。
  • 直接內存由 -XX:MaxDirectMemorySize 指定,若是不指定,則默認與Java堆最大值(-Xmx指定)同樣。
  • NIO程序中,使用ByteBuffer.allocteDirect(capability)分配的是直接內存,可能致使直接內存溢出。

直接內存溢出

  • 檢查代碼是否恰當
  • 檢查JVM參數-Xmx,-XX:MaxDirectMemorySize 是否合理。

demo代碼

package oom;

import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;

/**
 * -Xmx256m -XX:MaxDirectMemorySize=100M
 */
public class DirectByteBufferTest {
    public static void main(String[] args) throws InterruptedException{
        //分配128MB直接內存
        ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);

        TimeUnit.SECONDS.sleep(10);
        System.out.println("ok");
    }
}
複製代碼

運行結果

ByteBuffer分配128MB直接內存,而JVM參數-XX:MaxDirectMemorySize=100M指定最大是100M,所以發生直接內存溢出。

ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);
複製代碼

GC overhead limit exceeded

  • 這個是JDK6新加的錯誤類型,通常都是堆過小致使的。
  • Sun 官方對此的定義:超過98%的時間用來作GC而且回收了不到2%的堆內存時會拋出此異常。

解決方案

  • 檢查項目中是否有大量的死循環或有使用大內存的代碼,優化代碼。
  • 檢查JVM參數-Xmx -Xms是否合理
  • dump內存,檢查是否存在內存泄露,若是沒有,加大內存。

demo代碼

package oom;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * JVm參數 -Xmx8m -Xms8m
 */
public class GCoverheadTest {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            executor.execute(() -> {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    //do nothing
                }
            });
        }
    }
}

複製代碼

運行結果

實例代碼使用了newFixedThreadPool線程池,它使用了無界隊列,無限循環執行任務,會致使內存飆升。由於設置了堆比較小,因此出現此類型OOM。

總結

本文介紹瞭如下幾種常見OOM異常

java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: unable to create new native thread
java.lang.OutOfMemoryError: Metaspace
java.lang.OutOfMemoryError: Direct buffer memory
java.lang.OutOfMemoryError: GC overhead limit exceeded
複製代碼

但願你們遇到OOM異常時,對症下藥,順利解決問題。同時,若是有哪裏寫得不對,歡迎指出,感激涕零。

參考與感謝

我的公衆號

  • 若是你是個愛學習的好孩子,能夠關注我公衆號,一塊兒學習討論。
  • 若是你以爲本文有哪些不正確的地方,能夠評論,也能夠關注我公衆號,私聊我,你們一塊兒學習進步哈。
相關文章
相關標籤/搜索