1. 程式人生 > 實用技巧 >哪些場景會產生OOM?怎麼解決?

哪些場景會產生OOM?怎麼解決?

這個面試題是一個朋友在面試的時候碰到的,什麼時候會丟擲OutOfMemery異常呢?初看好像挺簡單的,其實深究起來考察的是對整個JVM的瞭解,而且這個問題從網上可以翻到一些亂七八糟的答案,其實在總結下來基本上4個場景可以概括下來。

堆記憶體溢位

堆記憶體溢位太常見,大部分人都應該能想得到這一點,堆記憶體用來儲存物件例項,我們只要不停的建立物件,並且保證GC Roots和物件之間有可達路徑避免垃圾回收,那麼在物件數量超過最大堆的大小限制後很快就能出現這個異常。

寫一段程式碼測試一下,設定堆記憶體大小2M。

public class HeapOOM {
    public static void main(String[] args) {
        List<HeapOOM> list = new ArrayList<>();
        while (true) {
            list.add(new HeapOOM());
        }
    }
}

執行程式碼,很快能看見OOM異常出現,這裡的提示是Java heap space堆記憶體溢位。

一般的排查方式可以通過設定-XX: +HeapDumpOnOutOfMemoryError在發生異常時dump出當前的記憶體轉儲快照來分析,分析可以使用Eclipse Memory Analyzer(MAT)來分析,獨立檔案可以在官網下載。

另外如果使用的是IDEA的話,可以使用商業版JProfiler或者開源版本的JVM-Profiler,此外IDEA2018版本之後內建了分析工具,包括Flame Graph(火焰圖)和Call Tree(呼叫樹)功能。

方法區(執行時常量池)和元空間溢位

方法區和堆一樣,是執行緒共享的區域,包含Class檔案資訊、執行時常量池、常量池,執行時常量池和常量池的主要區別是具備動態性,也就是不一定非要是在Class檔案中的常量池中的內容才能進入執行時常量池,執行期間也可以可以將新的常量放入池中,比如String的intern()方法。

我們寫一段程式碼驗證一下String.intern(),同時我們設定-XX:MetaspaceSize=50m -XX:MaxMetaspaceSize=50m 元空間大小。由於我使用的是1.8版本的JDK,而1.8版本之前方法區存在於永久代(PermGen),1.8之後取消了永久代的概念,轉為元空間(Metaspace),如果是之前版本可以設定PermSize MaxPermSize永久代的大小。

 private static String str = "test";
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        while (true){
            String str2 = str + str;
            str = str2;
            list.add(str.intern());
        }
}

執行程式碼,會發現程式碼報錯。

再次修改配置,去除元空間限制,修改堆記憶體大小-Xms20m -Xmx20m,可以看見堆記憶體報錯。

這是為什麼呢?intern()本身是一個native方法,它的作用是:如果字串常量池中已經包含一個等 於此String物件的字串,則返回代表池中這個字串的String物件;否則,將此String物件包含的字串新增到常量池中,並且返回String物件的引用。

而在1.7版本之後,字串常量池已經轉移到堆區,所以會報出堆記憶體溢位的錯誤,如果1.7之前版本的話會看見PermGen space的報錯。

直接記憶體溢位

直接記憶體並不是虛擬機器執行時資料區域的一部分,並且不受堆記憶體的限制,但是受到機器記憶體大小的限制。常見的比如在NIO中可以使用native函式直接分配堆外記憶體就容易導致OOM的問題。

直接記憶體大小可以通過-XX:MaxDirectMemorySize指定,如果不指定,則預設與Java 堆最大值-Xmx一樣。

由直接記憶體導致的記憶體溢位,一個明顯的特徵是在Dump檔案中不會看見明顯的異常,如果發現OOM之後Dump檔案很小,而程式中又直接或間接使用了NIO,那就可以考慮檢查一下是不是這方面的原因。

棧記憶體溢位

棧是執行緒私有,它的生命週期和執行緒相同。每個方法在執行的同時都會建立一個棧幀用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊,方法呼叫的過程就是棧幀入棧和出棧的過程。

在java虛擬機器規範中,對虛擬機器棧定義了兩種異常:

  1. 如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常
  2. 如果虛擬機器棧可以動態擴充套件,並且擴充套件時無法申請到足夠的記憶體,丟擲OutOfMemoryError異常

先寫一段程式碼測試一下,設定-Xss160k,-Xss代表每個執行緒的棧記憶體大小

public class StackOOM {
    private int length = 1;


    public void stackTest() {
        System.out.println("stack lenght=" + length);
        length++;
        stackTest();
    }


    public static void main(String[] args) {
        StackOOM test = new StackOOM();
        test.stackTest();
    }
}

測試發現,單執行緒下無論怎麼設定引數都是StackOverflow異常。

嘗試把程式碼修改為多執行緒,調整-Xss2m,因為為每個執行緒分配的記憶體越大,棧空間可容納的執行緒數量越少,越容易產生記憶體溢位。反之,如果記憶體不夠的情況,可以調小該引數來達到支撐更多執行緒的目的。

public class StackOOM {
    private void dontStop() {
        while (true) {
        }
    }


    public void stackLeakByThread() {
        while (true) {
            new Thread(() -> dontStop()).start();
        }
    }


    public static void main(String[] args) throws Throwable {
        StackOOM stackOOM = new StackOOM();
        stackOOM.stackLeakByThread();
    }