1. 程式人生 > >Java內存溢出異常(上)

Java內存溢出異常(上)

受限 lower 請求 ... 虛擬機棧 err 機制 out more

上一篇文章我們講了JVM運行時數據區域與內存溢出異常,其中對於內存溢出異常這部分將的不夠詳細,這篇文章將著重講解Java內存溢出異常的相關知識。如果有沒看過上一篇文章的小夥伴們,請點擊Java內存區域與內存溢出異常。

Java的內存溢出異常主要分為兩類:分別是內存溢出和棧溢出。在以下幾種情況,會拋出內存異常:Java堆溢出、虛擬機棧和本地方法棧溢出、方法區和運行時常量池溢出、以及本機直接內存溢出,下面講一一介紹這幾類異常。

Java堆溢出

在Java內存區域與內存溢出異常中講過,Java堆主要是用來存儲對象實例的。這部分的內存區域的大小可以通過-Xms參數和-Xmx參數進行設置,通常將-Xms和-Xmx的值設置為相同的值,以減少內存擴展或者收縮時的開銷。

Java堆的空間是有限的,受到物理內存與虛擬機內存的雙重限制(通常虛擬機內存的會設置成小於物理內存)。因此,如果對象實例的數量不斷增加,而垃圾回收機制沒有進行及時清理的時候,對象實例所占用的空間就會達到Java堆的空間最大值。此時,就會因為Java堆內存不足,導致無法為新的實例分配空間,從而拋出OutOfMemoryError異常。

通過設置-Xms20m -Xmx20m運行以下代碼可以模擬這一情況:

/**
 * VM Args: -Xms20m -Xmx20m
 *
 * @author bdq
 */
public class HeapOOM {
    static class OOMObject {

    }

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

  

運行結果:

java.lang.OutOfMemoryError: Java heap space

  

這即是常見的OOM異常,針對這類異常,往往在打印異常信息的同時會進一步提示異常原因,如上圖所示的”Java heap space”。當然,只靠這點信息不足以判斷到底是內存容量設置小了,還是出現了內存泄漏(關於內存泄漏的知識將會在後面的文章中進行講述)。因此我們還要輔以其他手段來進一步確定問題的根源,比如加上-XX:+HeapDumpOnOutOfMemoryError參數使得虛擬機在出現內存溢出異常時Dump出當前的內存堆轉儲快照,然後用相關的工具進行分析。這類知識,本篇文章暫不作過多的講解,將會在後面的文章一一介紹。

虛擬機棧和本地方法棧溢出

為什麽要把虛擬機棧和本地方法棧的溢出放在一起討論呢,因為在HotSpot虛擬機中並不區分虛擬機棧和本地方法棧。對於HotSpot來說,雖然說-Xoss參數是用來設置本地方法棧大小,但實際上是無效的,棧的容量只由-Xss參數設定。

在Java虛擬機規範中對虛擬機棧和本地方法棧描述了兩種異常:

  1. 如果線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverflowError異常。
  2. 如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。

這種分類其實並不是很明確,因為內存太小或者已使用的棧空間太大都會導致棧空間無法繼續分配。

StackOverflowError的出現條件很簡單,下面這段簡單的代碼就會出現棧溢出:

public class JavaVMStackSOF {
    private int stackLength = 1;

    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
        JavaVMStackSOF javaVMStackSOF = new JavaVMStackSOF();
        try {
            javaVMStackSOF.stackLeak();
        } catch (Throwable e) {
            System.out.println("Stack length:" + javaVMStackSOF.stackLength);
            throw e;
        }
    }
}

  

運行結果如下:

Stack length:18663
Exception in thread "main" java.lang.StackOverflowError
	at cn.bdqfork.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
	at cn.bdqfork.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
	at cn.bdqfork.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
	at cn.bdqfork.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
    ......

  

對上面的運行結果,不同的計算機Stack length的大小是不確定的,從輸出的異常信息來看,是因為stackLeak方法遞歸調用層數過多導致的。在大多數情況下,棧深度在虛擬機默認參數下是夠用的。

OutOfMemoryError異常比較難以出現,一般發生在多線程環境下。當創建一個線程時,虛擬機會分配一個私有的棧空間給相應的線程,這個空間的大小可以用-Xss參數來設置。通過不斷的創建新的進程,可以產生內存溢出異常。

原因是這樣的,當進程運行時,操作系統分配給進程的內存是有限的,Java堆和方法區這兩部分占了大部分,忽略到程序計數器所占用的很小的一塊內存,不計算虛擬機本身占用的內存,剩下的就由虛擬機棧和本地方法棧所占用。因此,創建的線程數量到達一定程度時,虛擬機棧和本地方法棧所占用的空間就會使得進程的內存空間不夠用,從而拋出內存溢出異常。

這部分的測試代碼如下:

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) {
        JavaVMStackOOM javaVMStackOOM = new JavaVMStackOOM();
        javaVMStackOOM.stackLeakByThread();
    }
}

  

這段代碼的運行有一定的風險,因為Java的線程並不是完全的用戶級線程,有映射到操作系統的部分,所以可能會產生系統假死的現象,請謹慎運行。

運行結果如下:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

  

由此可以看出,我們在進行多線程開發時,對於線程的數量要有一定的把握,線程池的復用是很有必要的。

受限於篇幅原因,剩余的知識點,將會在下一篇進行講解。

Java內存溢出異常(上)