JVM探秘:記憶體溢出

来源:https://www.cnblogs.com/cellei/archive/2019/12/30/12122013.html
-Advertisement-
Play Games

本系列筆記主要基於《深入理解Java虛擬機:JVM高級特性與最佳實踐 第2版》,是這本書的讀書筆記。 在 Java 虛擬機記憶體區域中,除了程式計數器外,其他幾個記憶體區域都可能會發生OutOfMemoryError,這次通過一些代碼來驗證虛擬機各個記憶體區域存儲的內容。 在實際工作中遇到記憶體溢出異常時, ...


本系列筆記主要基於《深入理解Java虛擬機:JVM高級特性與最佳實踐 第2版》,是這本書的讀書筆記。

在 Java 虛擬機記憶體區域中,除了程式計數器外,其他幾個記憶體區域都可能會發生OutOfMemoryError,這次通過一些代碼來驗證虛擬機各個記憶體區域存儲的內容。

在實際工作中遇到記憶體溢出異常時,需要做到能根據異常信息快速判斷是哪個記憶體區域的溢出,知道什麼樣的代碼會導致這些區域記憶體溢出,並且知道出現記憶體溢出後如何處理。

Java堆溢出

Java 堆用於存儲對象實例,只要不斷的擴展對象,並且保證 GC Roots 到對象有可達路徑來避免垃圾回收,那麼對象數量到達堆的最大容量後就會發生記憶體溢出異常。

模擬堆記憶體溢出

以下代碼會把堆大小限制在20M且不可擴展(將最小參數-Xms和最大參數-Xmx設為相同就會避免自動擴展),通過參數-XX:+HeapDumpOnOutOfMemoryError可以讓虛擬機在發生記憶體溢出時Dump出記憶體快照用來分析。

參數 說明
-XX:+HeapDumpOnOutOfMemoryError 記憶體溢出時自動導出記憶體快照
-XX:HeapDumpPath=E:/dumps/ 導出記憶體快照時保存的路徑
/**
 * Java堆記憶體溢出異常
 * VM args: -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
 * -Xms和-Xmx設為相同值避免堆記憶體自動擴展,
 * -XX:+HeapDumpOnOutOfMemoryError可以讓虛擬機在發生OOM時Dump出記憶體快照
 * Run With JDK 1.8
 * */
public class HeapOOM {

    static class OOMObject{
    }

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

運行結果:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid1344.hprof ...
Heap dump file created [29068691 bytes in 0.108 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 test.oom.HeapOOM.main(HeapOOM.java:21)

可以從異常信息中看到,OOM異常發生在“main”線程,發生的記憶體區域是“Java heap space”。

通過IntelliJ IDEA運行的話,可以點擊Edit Configurations配置VM參數,生成的堆Dump快照文件為hprof尾碼,存放在Working directory配置對應的目錄下,如下圖:

image

堆記憶體溢出分析

要分析 Java 堆的記憶體溢出,首先通過快照分析工具(如Java VisualVM)對 Dump 出來的的快照進行分析,確認記憶體中的對象是否是必要的。如果是不必要的而沒有垃圾回收掉,則發生的是記憶體泄漏(Memory Leak);如果都是必要的,則是記憶體溢出(Memory Overflow)。

如果是記憶體泄漏,通過工具進一步查看對象實例到 GC Roots 的引用鏈,找到泄露對象是通過什麼路徑與 GC Roots 相關聯導致垃圾收集器無法回收它們。根據泄露對象的類型信息和到 GC Roots 的引用鏈,就可以定位到泄露代碼的位置。

如果是記憶體溢出,也就是說這些對象還都必須存活,那麼就檢查堆記憶體的大小參數(-Xms與-Xmx)與物理記憶體比較還是否可以調大,再從代碼上檢查是否存在某些對象生命周期過長、持有狀態時間過長的情況,嘗試減少程式運行期的記憶體消耗。

打開 JDK 自帶的分析工具 Java VisualVM(bin目錄下的jvisualvm.exe),點擊文件->裝入選擇堆快照java_pid1344.hprof文件,打開後顯示的是概述信息,這裡會顯示快照的一些基本信息、環境屬性以及線程信息。

然後點擊,打開後如下圖:

image

從上圖可以看到,數量最多的且占用記憶體最大的對象是OOMObject類型的實例,OOMObject類型共有實例810,326個,大小總共12,965,216個位元組(byte),而這些對象都是在while迴圈中new出來加入到List中的,都是應該存活的對象,也就是說發生的OOM是記憶體溢出而不是記憶體泄漏。

然後在OOMObject的記錄上右鍵點擊在實例試圖中顯示,則會打開實例視圖,見下圖:

image

可以看到其中一個OOMObject對象的引用鏈,它被一個Object[]數組中的元素引用,我們都知道ArrayList是基於數組實現的,而這個Object[]數組對象就是一個 GC Root,它的記憶體地址是578296

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

在記憶體區域那篇文章講到過,HotSpot虛擬機把本地方法棧和虛擬機棧合二為一了,棧容量由-Xss參數設置。關於虛擬機棧和本地方法棧,虛擬機規範規定了兩種異常:

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

這裡把異常分為了兩種,看似嚴謹實際上有相互重疊的地方,當棧空間無法繼續分配時,到底是記憶體太小,還是已使用的棧空間太大,本質上只是對同一個問題的不同描述而已。

有兩種方法會拋出StackOverflowError異常,一種是通過-Xss參數減小棧記憶體容量;一種是定義大量局部變數,從而增大此方法幀中的局部變數表的長度。以下代碼是第一種:

/**
 * Java棧記憶體溢出異常
 * 通過減小棧記憶體容量拋出StackOverflowError
 * VM args: -Xss128K
 * Run With JDK 1.8
 * */
public class StackOOM {

    private int stackLength = 1;
    
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) throws Throwable {
        StackOOM oom = new StackOOM();
        try {
            oom.stackLeak();
        }catch(Throwable e){
            System.out.println("stack length: " + oom.stackLength);
            throw e;
        }
    }
}

運行結果:

stack length: 998
Exception in thread "main" java.lang.StackOverflowError
    at com.cellei.outofmemory.StackOOM.stackLeak(StackOOM.java:15)
    at com.cellei.outofmemory.StackOOM.stackLeak(StackOOM.java:16)
    at com.cellei.outofmemory.StackOOM.stackLeak(StackOOM.java:16)
    ...
    at com.cellei.outofmemory.StackOOM.main(StackOOM.java:22)

實驗結果表明,不論是減小棧容量大小還是增加棧幀大小,當記憶體無法分配時虛擬機拋出的都是StackOverflowError異常。

如果不限於單線程,不斷的建立線程的情況下倒是會拋出OutOfMemoryError異常,但跟棧空間是否足夠大沒有直接關係,而且棧是線程私有的記憶體區域。這種情況下,每個線程的棧分配的記憶體越大,就越容易產生記憶體溢出異常。

虛擬機提供了參數來控制堆記憶體和方法區的最大容量,物理記憶體減去堆記憶體最大值,再減去方法區的最大值,程式計數器消耗記憶體很小忽略不計,剩下的就被虛擬機棧和本地方法棧瓜分了。所以每個線程分配到的棧容量越大,則可以建立的線程數量越少,建立線程時就越容易把剩下的記憶體耗盡。如果建立過多導致了記憶體溢出,在不能減少線程數的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程。

方法區和運行時常量池溢出

JDK1.6及之前,運行時常量池是方法區的一部分,且方法區還使用永久代實現,那時候可以在限制永久代大小的情況下,迴圈調用String.intern()方法造成運行時常量池溢出而導致方法區溢出。使用參數-XX:PermSize-XX:MaxPermSize來限制永久代也就是方法區的大小。String.intern()方法是一個Native方法,作用是:如果字元串常量池中已經包含一個等於此String對象的字元串,則返回代表常量池中這個字元串的String對象;否則,將此String對象包含的字元串添加到常量池中。

JDK1.7的時候常量池挪到了堆記憶體中,到了JDK1.8就乾脆取消了永久代,取而代之的是元空間(MetaSpace),且元空間是位於本地記憶體而不是虛擬機記憶體。

以下代碼,在JDK1.6及之前的版本中會產生記憶體溢出:

/**
 * 要求運行在 JDK1.6 或以前
 * 導致常量池溢出從而產生永久代溢出
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 * Run With JDK 1.6
 */
public class ConstantPoolOverflowTest
{
    public static void main(String[] args)
    {
        List<String> list = new ArrayList<String>();
        int i = 0;
        while (true)
        {
            list.add(String.valueOf(i++).intern());
        }
    }
}

運行結果:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    at java.lang.String.intern(Native Method)
    ...

可見運行結果提示了PermGen space,表明是那個版本的永久代也就是方法區溢出。

既然JDK1.7及之後常量池挪到了 Java 堆中,在那之後的版本如何產生方法區溢出呢?既然方法區用於存放類的相關信息,基本思路就是在運行時產生大量的類去填充方法區,直到溢出。可以使用 JDK 的動態代理,也可以使用第三方庫比如 CGLib 實現。

以下代碼使用CGLib庫,在運行時不斷的產生類導致方法區溢出。由於JDK1.8的方法區改為了使用元空間實現,所以可以使用參數-XX:MetaspaceSize-XX:MaxMetaspaceSize限制方法區大小。

/**
 * 限制元空間大小後
 * 使用CGLib運行時產生類,導致元空間也就是方法區溢出
 * VM Args:-XX:MetaspaceSize=8M -XX:MaxMetaspaceSize=28M
 * Run With JDK 1.8
 */
public class MethodAreaOOM {

    static class OOMObject{
    }

    public static void main(String[] args){
        while(true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object o, Method method, Object[] objects, 
                MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(o, objects);
                }
            });
            enhancer.create();
        }
    }
}

運行結果:

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
    at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345)
    at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
    at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114)
    at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
    at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
    at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
    at com.cellei.oom.MethodAreaOOM.main(MethodAreaOOM.java:29)

可見異常信息提示Metaspace,就是說元空間(方法區)記憶體溢出了。方法區溢出也是一種比較常見的溢出,一個類要被垃圾收集器回收,判定條件是比較苛刻的。在經常動態產生大量 Class 的應用中,要特別註意類的回收情況。

本機記憶體直接溢出

DirectMemory容量可以通過參數-XX:MaxDirectMemorySize指定,如果不指定,則預設與 Java 堆最大值(-Xmx)一樣。通過反射獲取Unsafe實例進行記憶體分配,allocateMemory()方法會真正申請分配記憶體。

/**
 * 不斷的申請記憶體,導致本機記憶體溢出
 * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
 * Run With JDK 1.8
 * */
public class DirectMemoryOOM {

    private static final int _1M = 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(_1M);
        }
    }
}

運行結果:

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

由DirectMemory導致的記憶體溢出,有一個特點就是Heap Dump文件中不會看到明顯異常,如果Dump文件非常小,又直接間接使用了NIO,則有可能是這方面的原因。

本文代碼的 Github Repo 地址:https://github.com/cellei/JVM-Practice


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • vue是雙向數據綁定的框架,數據驅動是他的靈魂,他的實現原理眾所周知是Object.defineProperty方法實現的get、set重寫,但是這樣說太牽強外門了。本文將巨集觀介紹他的實現 + "使用vue" + "分析Object.defineProperty" + "簡單的源碼解析" + "一切 ...
  • EFK,ELK都是目前最為流行的分散式日誌框架,主要實現了日誌的收集,存儲,分析等,它可以與docker容器進行結合,來收集docker的控制台日誌,就是stdout日誌. elasticsearch.master_data_client說明 預設情況下,每個節點都有成為主節點的資格,也會存儲數據, ...
  • 架構師,老兵哥剛參加工作那些年業界還沒有這個職位,那時候跟技術相關的崗位就是開發工程師、測試工程師和系統工程師,後來隨著軟體規模不斷增長而產生的,尤其是在互聯網浪潮下用戶數和訪問量都是海量化的。在各種機緣巧合下,老兵哥結合個人喜好選擇了走架構師路徑,從懵懵懂懂邊做邊學,到現在總算摸出了些門道,回顧這... ...
  • Redis安裝(單機及各類集群,阿裡雲) 前言 上周,我朋友突然悄悄咪咪地指著手機上的一篇博客說,這是你的博客吧。我看了一眼,是之前發佈的《Rabbit安裝(單機及集群,阿裡雲》。我朋友很哈皮地告訴我,我的博客被某個Java平臺進行了微信推送。看到許多人閱讀,並認同了我的博客,心理還是很開心的。 好 ...
  • HTTP 協議全稱是超文本傳輸協議(Hypertext Transfer Protocol),這裡面需要理解三個地方:超文本、傳輸、協議,下麵就從 HTTP 協議的歷史講起。 20 世紀 60 年代,美國國防部高等研究計劃署(ARPA)建立了 ARPA 網,它有四個分佈在世界各地的節點,被認為是互聯 ...
  • 通過Java日期時間API系列10 Jdk8中java.time包中的新的日期時間API類的DateTimeFormatter, 可以看出java8的DateTimeFormatter完美解決了SimpleDateFormat線程安全問題。下麵是關於DateTimeFormatter的使用實例,包括 ...
  • 系統自身的error處理一般是 errors.New()或fmt.Errorf()等,對一些需要複雜顯示的,不太友好,我們可以擴展下error。 error在標準庫中被定義為一個介面類型,該介面只有一個Error()方法 那麼,自定義error只要擁有Error()方法,就實現了error介面,這裡 ...
  • 場景 Dubbo環境搭建-管理控制台dubbo-admin實現服務監控: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/103624846 Dubbo搭建HelloWorld-搭建服務提供者與服務消費者並完成遠程調用(附代碼下載) ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...