天天看點

【JVM進階之路】四:直面記憶體溢出和記憶體洩漏1、記憶體溢出2、記憶體洩漏

在Java中,和記憶體相關的問題主要有兩種,記憶體溢出和記憶體洩漏。

  • 記憶體溢出(Out Of Memory) :就是申請記憶體時,JVM沒有足夠的記憶體空間。通俗說法就是去蹲坑發現坑位滿了。
  • 記憶體洩露 (Memory Leak):就是申請了記憶體,但是沒有釋放,導緻記憶體空間浪費。通俗說法就是有人占着茅坑不拉屎。

1、記憶體溢出

在JVM的幾個記憶體區域中,除了程式計數器外,其他幾個運作時區域都有發生記憶體溢出(OOM)異常的可能。

【JVM進階之路】四:直面記憶體溢出和記憶體洩漏1、記憶體溢出2、記憶體洩漏

1.1、Java堆溢出

Java堆用于儲存對象執行個體,我們隻要不斷地建立對象,并且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼随着對象數量的增加,總容量觸及最大堆的容量限制後就會産生記憶體溢出異常。

我們來看一個代碼的例子:

/**
 * VM參數: -Xms20m -Xmx20m -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());
        }
    }
}      

接下來,我們來設定一下程式啟動時的JVM參數。限制記憶體大小為20M,不允許擴充,并通過參數-XX:+HeapDumpOnOutOf-MemoryError 讓虛拟機Dump出記憶體堆轉儲快照。

在Idea中設定JVM啟動參數如下圖:

【JVM進階之路】四:直面記憶體溢出和記憶體洩漏1、記憶體溢出2、記憶體洩漏

運作一下:

【JVM進階之路】四:直面記憶體溢出和記憶體洩漏1、記憶體溢出2、記憶體洩漏

Java堆記憶體的OutOfMemoryError異常是實際應用中最常見的記憶體溢出異常情況。出現Java堆記憶體溢出時,異常堆棧資訊“java.lang.OutOfMemoryError”會跟随進一步提示“Java heap space”。 Java堆檔案快照檔案dump到了java_pid18728.hprof檔案。

要解決這個記憶體區域的異常,正常的處理方法是首先通過記憶體映像分析工具(如JProfiler、Eclipse Memory Analyzer等)對Dump出來的堆轉儲快照進行分析。

看到記憶體占用資訊如下:

【JVM進階之路】四:直面記憶體溢出和記憶體洩漏1、記憶體溢出2、記憶體洩漏

然後可以檢視代碼問題如下:

【JVM進階之路】四:直面記憶體溢出和記憶體洩漏1、記憶體溢出2、記憶體洩漏
常見堆JVM相關參數:

-XX:PrintFlagsInitial

: 檢視所有參數的預設初始值

-XX:PrintFlagsFinal

:檢視所有的參數的最終值(可能會存在修改,不再是初始值)

-Xms

: 初始堆空間記憶體(預設為實體記憶體的1/64)

-Xmx

: 最大堆空間記憶體(預設為實體記憶體的1/4)

-Xmn

: 設定新生代大小(初始值及最大值)

-XX:NewRatio

: 配置新生代與老年代在堆結構的占比

-XX:SurvivorRatio

:設定新生代中Eden和S0/S1空間的比例

-XX:MaxTenuringThreshold

:設定新生代垃圾的最大年齡(預設15)

-XX:+PrintGCDetails

:輸出詳細的GC處理日志

列印

GC

簡要資訊:①

-XX:+PrintGC

-verbose:gc

-XX:HandlePromotionFailure

:是否設定空間配置設定擔保

1.2、虛拟機棧和本地方法棧溢出

HotSpot虛拟機中将虛拟機棧和本地方法棧合二為一,是以對于HotSpot來說,-Xoss參數(設定本地方法棧大小)雖然存在,但實際上是沒有任何效果的,棧容量隻能由-Xss參數來設定。關于虛拟機棧和本地方法棧,有兩種異常:

  • 如果線程請求的棧深度大于虛拟機所允許的最大深度,将抛出

    StackOverflowError

    異常。
  • 如果虛拟機的棧記憶體允許動态擴充,當擴充棧容量無法申請到足夠的記憶體時,将抛出

    OutOfMemoryError

1.2.1、StackOverflowError

HotSpot虛拟機不支援棧的動态擴充,在HotSpot虛拟機中,以下兩種情況都會導緻StackOverflowError。

  • 棧容量過小

    如下,使用Xss參數減少棧記憶體容量

/**
 * vm參數:-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;
        }
    }

}      

運作結果:

【JVM進階之路】四:直面記憶體溢出和記憶體洩漏1、記憶體溢出2、記憶體洩漏
  • 棧幀太大

    如下,通過一長串變量,來占用局部變量表空間。

    【JVM進階之路】四:直面記憶體溢出和記憶體洩漏1、記憶體溢出2、記憶體洩漏
【JVM進階之路】四:直面記憶體溢出和記憶體洩漏1、記憶體溢出2、記憶體洩漏

無論是由于棧幀太大還是虛拟機棧容量太小,當新的棧幀記憶體無法配置設定的時候, HotSpot虛拟機抛出的都是StackOverflowError異常。

1.2.2、OutOfMemoryError

雖然不支援動态擴充棧,但是通過不斷建立線程的方式,也可以在HotSpot上産生記憶體溢出異常。

需要注意,這樣産生的記憶體溢出異常和棧空間是否足夠并不存在任何直接的關系,主要取決于作業系統本身的記憶體使用狀态。因為作業系統給每個程序的記憶體時有限的,線程數一多,自然會超過程序的容量。

建立線程導緻記憶體溢出異常 :

/**
 * vm參數:-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) throws Throwable {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}      

以上是一段比較有風險的代碼,可能會導緻系統假死,運作結果如下:

【JVM進階之路】四:直面記憶體溢出和記憶體洩漏1、記憶體溢出2、記憶體洩漏

1.3、方法區和運作時常量池溢出

這裡再提一下方法區和運作時常量池的變遷,JDK1.7以後字元串常量池移動到了堆中,JDK1.8在直接記憶體中劃出一塊區域元空間來實作方區域。

String:intern()是一個本地方法,它的作用是如果字元串常量池中已經包含一個等于此String對象的 字元串,則傳回代表池中這個字元串的String對象的引用;否則,會将此String對象包含的字元串添加到常量池中,并且傳回此String對象的引用。在JDK 6或更早之前的HotSpot虛拟機中,常量池都是配置設定在永久代中,永久代本身記憶體不限制可能會出現錯誤:

java.lang.OutOfMemoryError: PermGen space      

1.4、本機直接記憶體溢出

直接記憶體(Direct Memory)的容量大小可通過-XX:MaxDirectMemorySize參數來指定,如果不去指定,則預設與Java堆最大值(由-Xmx指定)一緻。

直接通過反射擷取

Unsafe

執行個體,通過反射向作業系統申請配置設定記憶體:

/**
 * vm參數:-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);
        }
    }
}      
【JVM進階之路】四:直面記憶體溢出和記憶體洩漏1、記憶體溢出2、記憶體洩漏

由直接記憶體導緻的記憶體溢出,一個明顯的特征是在Heap Dump檔案中不會看見有什麼明顯的異常情況。

2、記憶體洩漏

記憶體回收,簡單說就是應該被垃圾回收的對象沒有被垃圾回收。

【JVM進階之路】四:直面記憶體溢出和記憶體洩漏1、記憶體溢出2、記憶體洩漏

在上圖中:對象 X 引用對象 Y,X 的生命周期比 Y 的生命周期長,Y生命周期結束的時候,垃圾回收器不會回收對象Y。

我們來看幾個記憶體洩漏的例子:

  • 靜态集合類引起記憶體洩漏

    靜态集合的生命周期和 JVM 一緻,是以靜态集合引用的對象不能被釋放。

public class OOM {
 static List list = new ArrayList();

 public void oomTests(){
   Object obj = new Object();

   list.add(obj);
  }
}      
  • 單例模式:

    和上面的例子原理類似,單例對象在初始化後會以靜态變量的方式在 JVM 的整個生命周期中存在。如果單例對象持有外部的引用,那麼這個外部對象将不能被 GC 回收,導緻記憶體洩漏。

  • 資料連接配接、IO、Socket等連接配接

    建立的連接配接不再使用時,需要調用 close 方法關閉連接配接,隻有連接配接被關閉後,GC 才會回收對應的對象(Connection,Statement,ResultSet,Session)。忘記關閉這些資源會導緻持續占有記憶體,無法被 GC 回收。

try {
            Connection conn = null;
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection("url", "", "");
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("....");
          } catch (Exception e) { 
           
          }finally {
            //不關閉連接配接
          }
        }      
  • 變量不合理的作用域

    一個變量的定義作用域大于其使用範圍,很可能存在記憶體洩漏;或不再使用對象沒有及時将對象設定為 null,很可能導緻記憶體洩漏的發生。

public class Simple {
    Object object;
    public void method1(){
        object = new Object();
        //...其他代碼
        //由于作用域原因,method1執行完成之後,object 對象所配置設定的記憶體不會馬上釋放
        object = null;
    }
}      
  • 引用了外部類的非靜态内部類

    非靜态内部類(或匿名類)的初始化總是需要依賴外部類的執行個體。預設情況下,每個非靜态内部類都包含對其包含類的隐式引用,若在程式中使用這個内部類對象,那麼即使在包含類對象超出範圍之後,也不會被回收(内部類對象隐式地持有外部類對象的引用,使其成不能被回收)。

  • Hash 值發生改變

    對象Hash值改變,使用HashMap、HashSet等容器中時候,由于對象修改之後的Hah值和存儲進容器時的Hash值不同,會導緻無法從容器中單獨删除目前對象,造成記憶體洩露。

  • ThreadLocal 造成的記憶體洩漏

    ThreadLocal 可以實作變量的線程隔離,但若使用不當,就可能會引入記憶體洩漏問題。

繼續閱讀