JVM OutOfMemoryError異常

《深入理解Java虛擬機》


  在Java虛擬機中,除了程序計數器外,其他幾個運行時區域都有發生OutOfMemoryError(OOM)異常的可能。關於虛擬機啓動參數,如果使用控制檯命令執行程序,直接跟在Java命令之後書寫就行;如果使用Eclipse IDE,可在如下圖配置:右鍵工程–Properties–選擇Run/Debug Settings–新建New–Ok
  
Eclipse配置VM參數

  • JVM 堆溢出
    Java 堆用於存儲對象的實例,只要不斷的創建對象,當對象數量達到最大堆容量限制後就會產生內存溢出異常。
    設置Java堆大小爲10M,不可擴展(將堆的最小值-Xms和最大值-Xmx相等即可避免堆自動擴展),通過參數-XX:+HeapDumpOnOutOfMemoryError在出現內存溢出異常時Dump出當前內存堆轉儲快照以便事後分析。
    代碼1 Java 堆內存溢出異常測試
/**
 * VM Args: -verbose:gc -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOM {
    static class OOMObject{
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while(true){
            list.add(new OOMObject());
        }
    }
}

運行結果:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid4756.hprof ...
Heap dump file created [13038231 bytes in 0.098 secs]
  • Java棧和本地方法棧溢出
      在HotSpot虛擬機中並不區分虛擬機棧和本地方法棧,因此,對於HotSpot來說,-Xoss(設置本地方法棧大小)雖存在但無效棧容量只有-Xss參數設定
    對於Java棧和本地方法棧,Java虛擬機規範中描述了兩種異常:
      - 若線程請求的棧深度大於虛擬機所允許的最大深度,拋出StackOverflowError異常
      - 若虛擬機在擴展時無法申請足夠的內存空間,拋出OutOfMemoryError異常
     (1)使用-Xss參數減少棧內存的容量,拋出StackOverflowError,異常出現時輸出棧深度相應縮小
     (2)定義大量的本地變量,增大此方法幀中本地變量表的長度,拋出StackOverflowError,異常出現時輸出棧深度相應縮小
     
    代碼2 Java 棧溢出異常測試
/**
 * VM Args: -Xss128k
 */
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;
        }
    }
}

運行結果:

Stack length:995
Exception in thread "main" java.lang.StackOverflowError
    at com.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9)
    at com.test.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
    ... ...

  在單個線程下,無論是由於棧幀太大還是虛擬機棧容量大小,但內存無法分配的時候,虛擬機拋出的都是StackOverflowError異常。
在多線程下,通過不斷的建立線程的方式可以產生內存溢出OutOfMemoryError異常

代碼3 Java 棧 多線程內存溢出異常測試

/**
 * VM Args: -Xss2M
 */
public class JavaVMStackSOF {
    public 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 {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        oom.stackLeakByThread();
    }
}

運行上述代碼有較大的風險,可能會導致操作系統的假死
運行結果:

Exception in thread "main" java.lang.OutOfMemoryError:unable to create new native thread
  • 方法區溢出
      可以通過-XX:PermSize和-XX:MaxPermSize限制方法區大小
    方法區用於存放Class的相關信息,類名、訪問修飾符、常量池、字段描述、方法描述等,基本思路是產生大量的類去填滿方法區,指到溢出。藉助CGLib直接操作字節碼生成大量的動態類。
    代碼4 藉助CGLib使方法區內存溢出
/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class JavaMethodAreaOOM {
    static class OOMObject{
    }
    public static void main(String[] args){
        ErrHandler handler = new ErrHandler();

        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();
        }
    }
}

運行結果:

java.lang.OutOfMemoryError: PermGen space
Dumping heap to java_pid2824.hprof ...
Heap dump file created [3934632 bytes in 0.032 secs]
... ...
Exception: java.lang.OutOfMemoryError thrown from the 
UncaughtExceptionHandlerin thread "main"
  • 本機直接內存溢出
      DirectMemory容量可通過-XX:MaxDirectMemorySize指定,如果不指定默認與Java堆最大值(-Xmx)一樣,下列代碼越過了DirectByteBuffer類,直接通過反射獲取Unsafe實例進行內存分配(Unsafe類的getUnsafe()方法限制了只有引導類加載器纔會返回實例,也就是隻有rt.jar中的類才能使用Unsafe的功能)。
      雖然使用DirectByteBuffer分配內存也會拋出異常,但它拋出異常時沒有真正向操作系統申請分配內存,而是通過計算得知內存無法分配,於是手動拋出異常,真正申請分配內存的方法是unsafe.allocateMemory().
    代碼5 使用 unsafe 分配本機內存
/**
 * sun.misc.Unsafe;
 * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M 
 */
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);
        }
    }
}

運行結果:

Exception in thread "main" java.lang.OutOfMemoryError
    at sun.misc.Unsafe.allocateMemory(Native Method)
    at com.test.DirectMemoryOOM.main(DirectMemoryOOM.java:17)

由DirectMemory導致的內存溢出,如果發現OOM之後Dump文件很小,而程序中又直接或間接使用了NIO,可以考慮檢查是不是這方面的原因。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章