1. 程式人生 > >Java 內存溢出分析

Java 內存溢出分析

這一 tsp str 字符串 ensure 運行時常量 window flow lower

一、前言

Java 的 JVM 的內存一般可分為 3 個區:堆(heap)、棧(stack)和方法區(method)。

技術分享圖片

1.1 堆區

1)存儲的全部是對象,每個對象都包含一個與之對應的 Class 的信息,Class 的目的是得到操作指令;

2)JVM 只有一個堆區(heap)被所有線程共享,堆中不存放基本類型和對象引用,只存放對象本身。

1.2 棧區

1)每個線程包含一個棧區,棧中只保存基礎數據類型的對象和自定義對象的引用(不是對象),對象都存放在堆區中;

2)每個棧中的數據(原始類型和對象引用)都是私有的,其他棧不能訪問;

3)棧分為3個部分:基本類型變量區、執行環境上下文、操作指令區(存放操作指令)。

1.3 方法區

1)又叫靜態區或永久代,跟堆一樣,被所有的線程共享。方法區包含所有的 Class 和 static 變量;

2)方法區中包含的都是在整個程序中永遠唯一的元素,如 Class,static 變量;

3)運行時常量池都分配在 Java 虛擬機的方法區之中,但是從 JDK 1.7 的 HotSpot 虛擬機開始中,已經把原本放在永久代的字符串常量池移出了。

二、內存溢出分析

以下代碼驗證,建議使用 JDK 1.6,因為從 JDK 1.7 開始,HotSpot 虛擬機改進了很多,特別是方法區(永久代)。

2.1 Java 堆溢出

Java堆是用於存儲對象實例的,因此,只要我們不斷創建對象,並且保證對象不被垃圾回收機制清除,那麽當堆中對象的大小超過了最大堆的容量限制,就會出現堆內存溢出。

堆溢出常見的異常是:java.lang.OutOfMemoryError: Java heap space

下面這段代碼,將Java堆的大小設置為 20MB,並且不可擴展(即將堆的最小值 -Xms 參數和最大值 -Xmx 參數設置為相等的 20MB,來避免堆自動擴展);通過參數 -XX:+HeapDumpOnOutOfMemoryError 可以讓虛擬機在出現內存溢出異常時生成當前內存堆的快照,以便事後進行分析;-verbose:gc 的作用是在虛擬機發生內存回收時在輸出設備顯示信息。

package com.java.error;

import java.util.ArrayList;
import
java.util.List; /** * VM Args:-verbose:gc -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * Error:java.lang.OutOfMemoryError: Java heap space * @author moonxy * */ public class HeapOOM { public static void main(String[] args) { List<HeapOOM> list = new ArrayList<HeapOOM>(); while(true) { list.add(new HeapOOM()); } } }

運行後顯示如下:

[GC (Allocation Failure)  5380K->3745K(19968K), 0.0042158 secs]
[GC (Allocation Failure)  9287K->9710K(19968K), 0.0058399 secs]
[Full GC (Ergonomics)  9710K->7589K(19968K), 0.1200134 secs]
[Full GC (Ergonomics)  16387K->12869K(19968K), 0.1112792 secs]
[Full GC (Ergonomics)  16428K->16382K(19968K), 0.1711686 secs]
[Full GC (Allocation Failure)  16382K->16370K(19968K), 0.1371103 secs]
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid1204.hprof ...
Heap dump file created [28024534 bytes in 0.077 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:261)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	at java.util.ArrayList.add(ArrayList.java:458)
	at com.java.error.OutOfMemoryJavaHeapSpaceTest.main(OutOfMemoryJavaHeapSpaceTest.java:19)

控制臺打印了OutOfMemoryError,並且提示是Java heap發生的內存溢出,同時生成了dump文件。對於Java堆溢出,一般通過內存映像分析工具(如Eclipse Memory Analyzer)對dump文件進行堆快照分析,確認內存中的對象是不是必要的:

如果對象不是必要的,那就屬於內存泄漏,需要分析為什麽對象沒有被回收;

如果對象確實有必要繼續存在,那就屬於內存溢出,需要通過將堆的大小調高(-Xms-Xmx)來避免內存溢出。

此處比較一下內存泄漏和內存溢出:

內存泄漏 memory leak:是指程序在申請內存後,無法釋放已經申請的內存空間,一次內存泄露似乎不會有大的影響,但內存泄漏堆積後的後果就是內存溢出;

內存溢出 out of memory:是指程序申請內存後,沒有足夠的內存供申請者使用或將 long 類型的數據存儲到 int 類型的存儲空間時,就會出現 OOM 錯誤。

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

由於HotSpot虛擬機不區分虛擬機棧和本地方法棧,因此,對於 HotSpot 來說,-Xoss參數(設置本地方法棧大小)是無效的,棧容量只由 -Xss 設置。

在Java虛擬機規範中,這個區域有兩種異常情況:

如果線程運行時的棧幀的總大小超過虛擬機限制的大小,會拋出 StackOverflowError,這一點通常發生在遞歸運行時,棧溢出容易出現:java.lang.StackOverflowError

如果虛擬機棧設置為可以動態擴展,並且在擴展時無法申請到足夠內存,則會拋出 OutOfMemoryError,此時容易出現:java.lang.OutOfMemoryError: unable to create native thread

StackOverflowError

/**
 * VM Args:-Xss128k
 * Error:java.lang.StackOverflowError
 * @author moonxy
 */
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;
        }
    }
}

運行後顯示如下:

994
Exception in thread "main" java.lang.StackOverflowError
	at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:15)
	at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16)
	at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16)
	at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16)
	at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16)
     ……

OutOfMemoryError

不建議執行如下驗證,容易發生系統假死,因為在 Windows 平臺的虛擬機中,Java 線程是映射到操作系統的內核線程中的。

/**
 * VM Args:-verbose:gc -Xss2m
 * Error:java.lang.OutOfMemoryError: unable to create native thread
 * @author moonxy
 *
 */
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 oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}

運行後顯示如下:

java.lang.OutOfMemoryError: unable to create new native thread

線程需要消耗棧容量,而操作系統分配給每個進程的內存是有限制的。考慮如下場景:系統總內存 6G,JVM分配了 2G 內存,其中堆內存分配了 1G,方法區(永久代)分配 512M,忽略其他較小的內存分配,則剩余分配給虛擬機棧和本地方法棧的內存就只有512M,很有可能沒有足夠的可用內存創建很多的線程。就有可能出現 java.lang.OutOfMemoryError: unable to create new native thread。如果是這種情況,考慮減小堆內存大小或適當減少每個線程的棧分配大小。

線程會占用內存,如果每個線程都占用更多內存,整體上將消耗更多的內存。每個線程默認占用內存大小取決於 JVM 實現。可以利用 -Xss 參數限制線程內存大小,降低總內存消耗。例如,JVM 默認每個線程占用 1M 內存,應用有 500個 線程,那麽將消耗 500M 內存空間。如果實際上 256K 內存足夠線程正常運行,配置 -Xss256k,那麽 500 個線程將只需要消耗 125M 內存。(註意,如果 -Xss 設置的過低,又將會產生 java.lang.StackOverflowError 錯誤)

2.3 方法區溢出

方法區存儲的是虛擬機加載的類信息、常量、靜態變量、JIT 編譯器編譯後的代碼等數據,在 JDK1.7之前,HotSpot 都是使用 "永久代" 來管理這些數據,也就是說,方法區的數據,其實也是屬於堆內存的,會占用堆內存。

因此:方法區的數據大小 < -XX:MaxPermSize < -Xmx

我們只要限制一下永久代的大小(-XX:MaxPermSize),很容易就會發生方法區溢出,此時方法區容易出現:java.lang.OutOfMemoryError: PermGen space。可以通過設置 -XX:PermSize 和 -XX:MaxPermSize 來限制方法區大小。

/**
 * VM Args:-verbose:gc -XX:PermSize=10M -XX:MaxPermSize=10M
 * Error:java.lang.OutOfMemoryError: PermGen space
 * @author moonxy
 *
 */
public class OutOfMemoryPermGenSpaceTest {
    
    public static void main(String[] args) {
        
        List<String> oomPgsList = new ArrayList<String>();
        int i = 0;
        while(true) {
            oomPgsList.add(String.valueOf(i++).intern());
        }
    }
}

運行後結果如下:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

如果我們在 JDK 1.7 的環境下執行上面同樣的代碼,會發現 while 循環會一直執行下去。原因是在 JDK 1.7 中,HotSpot 已經不再將常量放入永久代中進行管理,而是放到內存上限是本機內存的 Native Memory 中,這樣做的好處就是減少了內存溢出的幾率(沒有了-XX:MaxPermSize 的限制),同時常量不再占用堆的內存。這種 "去永久代" 的做法,從 JDK 1.7 已經開始進行。終於在 JDK 1.8 中,被徹底的執行了,永久代被徹底去掉了,同時 HotSpot 新增了一塊叫做 Mataspace 的區域,並提供了 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 參數,來設置運行 Java 虛擬機使用的 Metaspace 的初始容量和最大容量。

2.4 本機直接內存溢出

直接內存(Direct Memory)並不是虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。但是這部分內存也被頻繁的使用,而且也可能導致 OutOfMemoryError 出現,即:java.lang.OutOfMemory

本機直接內存的容量可以通過 -XX:MaxDirectMemorySize 指定,如果不指定,默認與 Java 堆最大值(-Xmx指定)一樣。

本機直接內存溢出的一個明顯特征是,dump 文件很小,因為主要對象都在 direct memory了,並且異常信息也不會說明是在哪個區域發生內存溢出,就像這樣:java.lang.OutOfMemoryError

/**
 * 
 * VM Args:-verbose:gc -XX:MaxDirectMemorySize
 * Error:java.lang.OutOfMemory
 * @author moonxy
 *
 */
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

綜上,可以設置如下的 JVM 參數:

-Xms256m -Xmx1024m -XX:PermSize=256m -XX:MaxPermSize=512m

技術分享圖片

Java 內存溢出分析