常見OOM問題之Java heap space 堆溢出問題詳解

本文來自于HeapDump性能社區(qū)! !有性能問題,上HeapDump性能社區(qū)!

正文

第一篇:java.lang.OutOfMemoryError:Java heap space

Java 應(yīng)用程序只允許使用有限的內(nèi)存量。此限制是在應(yīng)用程序啟動期間指定的。為了讓事情變得更復雜,Java 內(nèi)存被分成兩個不同的區(qū)域。這些區(qū)域稱為堆空間和 Permgen(用于永久代):

1.png

(https://heapdump.cn/article/3100493)

這些區(qū)域的大小是在 Java 虛擬機 (JVM) 啟動期間設(shè)置的,可以通過指定 JVM 參數(shù)-Xmx-XX:MaxPermSize進行自定義。如果您沒有明確設(shè)置大小,將使用特定于平臺的默認值。

java.lang.OutOfMemoryError:Java堆空間時,應(yīng)用程序錯誤將被觸發(fā)嘗試添加更多的數(shù)據(jù)放入堆空間區(qū)域,但沒有足夠的空間供它

請注意,可能有足夠的物理內(nèi)存可用,但是只要 JVM 達到堆大小限制,就會拋出java.lang.OutOfMemoryError: Java heap space錯誤。

1,是什么原因造成的?

java.lang.OutOfMemoryError 的最常見原因很簡單:您嘗試將 XXL 應(yīng)用程序放入 S 大小的 Java 堆空間中。也就是說 - 應(yīng)用程序只需要比正常運行可用的更多的 Java 堆空間。此 OutOfMemoryError 消息的其他原因更為復雜,并且是由編程錯誤引起的:

  • 使用量/數(shù)據(jù)量激增。該應(yīng)用程序旨在處理一定數(shù)量的用戶或一定數(shù)量的數(shù)據(jù)。當用戶數(shù)或數(shù)據(jù)量突然激增并超過預期閾值時,在峰值之前正常運行的操作停止運行并觸發(fā)java.lang.OutOfMemoryError: Java heap space錯誤。
  • 內(nèi)存泄漏。特定類型的編程錯誤會導致您的應(yīng)用程序不斷消耗更多內(nèi)存。每次使用應(yīng)用程序的泄漏功能時,都會將一些對象留在 Java 堆空間中。隨著時間的推移,泄漏的對象會消耗所有可用的 Java 堆空間并觸發(fā)已經(jīng)熟悉的java.lang.OutOfMemoryError: Java heap space錯誤。

2,舉個例子

第一個例子非常簡單——下面的 Java 代碼嘗試分配一個 2M 整數(shù)的數(shù)組。當您編譯它并使用 12MB 的 Java 堆空間 ( java -Xmx12m OOM ) 啟動時,它會失敗并顯示java.lang.OutOfMemoryError: Java heap space消息。使用 13MB Java 堆空間,程序運行得很好。

class OOM {
  static final int SIZE=2*1024*1024;
  public static void main(String[] a) {
    int[] i = new int[SIZE];
   }
}

內(nèi)存泄漏示例

第二個也是更現(xiàn)實的例子是內(nèi)存泄漏。在 Java 中,當開發(fā)人員創(chuàng)建和使用新對象(例如 new Integer(5) )時,他們不必自己分配內(nèi)存——這由 Java 虛擬機 (JVM) 負責。在應(yīng)用程序的生命周期中,JVM 會定期檢查內(nèi)存中的哪些對象仍在使用,哪些尚未使用。可以丟棄未使用的對象,回收內(nèi)存并再次使用。這個過程稱為垃圾收集。JVM 中負責收集的相應(yīng)模塊稱為垃圾收集器 (GC)

Java 的自動內(nèi)存管理依賴于GC定期查找未使用的對象并刪除它們。稍微簡化一下,我們可以說Java中的內(nèi)存泄漏是應(yīng)用程序不再使用某些對象但****垃圾收集****無法識別它的情況。結(jié)果,這些未使用的對象無限期地保留在 Java 堆空間中。這種堆積最終會觸發(fā)java.lang.OutOfMemoryError: Java heap space錯誤。

構(gòu)造一個滿足內(nèi)存泄漏定義的 Java 程序相當容易:

class KeylessEntry {

   static class Key {
      Integer id;

      Key(Integer id) {
         this.id = id;
      }

      @Override
      public int hashCode() {
         return id.hashCode();
      }
   }

   public static void main(String[] args) {
      Map m = new HashMap();
      while (true)
         for (int i = 0; i < 10000; i++)
            if (!m.containsKey(new Key(i)))
               m.put(new Key(i), "Number:" + i);
   }
}

當您執(zhí)行上面的代碼時,您可能希望它永遠運行而不會出現(xiàn)任何問題,假設(shè)樸素的緩存解決方案僅將底層 Map 擴展到 10,000 個元素,因為除此之外,所有鍵都已經(jīng)存在于 HashMap 中。然而,實際上這些元素將繼續(xù)添加,因為 Key 類在其hashCode()旁邊不包含適當?shù)?em>equals()實現(xiàn)。

結(jié)果,隨著時間的推移,隨著不斷使用泄漏代碼,“緩存”結(jié)果最終會消耗大量 Java 堆空間。當泄漏的內(nèi)存填滿堆區(qū)域中的所有可用內(nèi)存并且垃圾收集無法清除它時,會拋出java.lang.OutOfMemoryError:Java 堆空間

解決方案很簡單——添加類似于下面的equals()方法的實現(xiàn),你會很高興。但在你找到原因之前,你肯定會失去一些寶貴的腦細胞。

@Override
public boolean equals(Object o) {
   boolean response = false;
   if (o instanceof Key) {
      response = (((Key)o).id).equals(this.id);
   }
   return response;
}

3,解決辦法是什么?

在某些情況下,分配給 JVM 的堆數(shù)量不足以滿足在該 JVM 上運行的應(yīng)用程序的需求。在這種情況下,你應(yīng)該只分配更多的堆——請參閱本章末尾以了解如何實現(xiàn)這一點。

然而,在許多情況下,提供更多的 Java 堆空間并不能解決問題。例如,如果您的應(yīng)用程序包含內(nèi)存泄漏,添加更多堆只會推遲java.lang.OutOfMemoryError: Java heap space錯誤。此外,增加 Java 堆空間量也往往會增加GC 暫停的長度,影響應(yīng)用程序的吞吐量或延遲

如果您希望解決 Java 堆空間的潛在問題而不是掩蓋癥狀,您需要弄清楚代碼的哪一部分負責分配最多的內(nèi)存。換句話說,您需要回答以下問題:

  1. 哪些對象占據(jù)堆的大部分
  2. 在源代碼中分配這些對象的位置

此時,請確保在您的日歷中清除幾天(或 – 請參閱項目符號列表下方的自動方式)。以下是一個粗略的流程大綱,可以幫助您回答上述問題:

  • 獲得安全許可,以便從 JVM 執(zhí)行堆轉(zhuǎn)儲。“轉(zhuǎn)儲”基本上是您可以分析的堆內(nèi)容的快照。因此,這些快照可能包含機密信息,例如密碼、信用卡號等,因此出于安全原因,甚至可能無法獲取此類轉(zhuǎn)儲。
  • 在適當?shù)臅r候獲取轉(zhuǎn)儲。準備好進行一些轉(zhuǎn)儲,因為在錯誤的時間進行時,堆轉(zhuǎn)儲包含大量噪音,實際上可能毫無用處。另一方面,每次堆轉(zhuǎn)儲都會完全“凍結(jié)”JVM,因此不要使用太多堆轉(zhuǎn)儲,否則您的最終用戶會開始面臨性能問題。
  • 找到一臺可以加載轉(zhuǎn)儲的機器。當您的 JVM-to-troubleshoot 使用例如 8GB 的堆時,您需要一臺超過 8GB 的機器來分析堆內(nèi)容。啟動轉(zhuǎn)儲分析軟件(我們推薦Eclipse MAT,但也有同樣好的替代品)。
  • 檢測到最大堆消耗者的 GC 根的路徑。我們已經(jīng)覆蓋在一個單獨的后這一活動在這里。對于初學者來說尤其困難,但練習會讓您了解結(jié)構(gòu)和導航機制。
  • 接下來,您需要弄清楚在源代碼中的何處分配了具有潛在危險的大量對象。如果您非常了解應(yīng)用程序的源代碼,則可以通過幾次搜索來完成此操作。

或者,我們建議Plumbr,這是唯一具有自動根本原因檢測功能的 Java 監(jiān)控解決方案。在其他性能問題中,它捕獲所有java.lang.OutOfMemoryError并自動為您提供有關(guān)最需要內(nèi)存的數(shù)據(jù)結(jié)構(gòu)的信息。

Plumbr 負責在幕后收集必要的數(shù)據(jù)——這包括有關(guān)堆使用情況的相關(guān)數(shù)據(jù)(只有對象布局圖,沒有實際數(shù)據(jù)),以及一些您甚至在堆轉(zhuǎn)儲中都找不到的數(shù)據(jù)。它還會為您進行必要的數(shù)據(jù)處理——在 JVM 遇到java.lang.OutOfMemoryError 時立即進行。這是來自 Plumbr 的java.lang.OutOfMemoryError事件警報示例:

2.png

(https://heapdump.cn/article/3100493)

無需任何額外的工具或分析,您就可以看到:

  • 哪些對象消耗的內(nèi)存最多(271 個com.example.map.impl.PartitionContainer實例消耗了 248MB 總堆中的 173MB)
  • 這些對象的分配位置(大部分分配在MetricManagerImpl類中,第 304 行)
  • 當前引用這些對象的是什么(直到 GC 根的完整引用鏈)

有了這些信息,您就可以放大潛在的根本原因,并確保將數(shù)據(jù)結(jié)構(gòu)修剪到適合您的內(nèi)存池的級別。

但是,當您從內(nèi)存分析或閱讀 Plumbr 報告得出的結(jié)論是內(nèi)存使用是合法的并且源代碼中沒有任何更改時,您需要讓您的 JVM 有更多的 Java 堆空間才能正常運行。在這種情況下,更改您的 JVM 啟動配置并添加(或增加值,如果存在)以下內(nèi)容:

-Xmx1024m

上述配置將為應(yīng)用程序提供 1024MB 的 Java 堆空間。您可以對 GB 使用 g 或 G,對 MB 使用 m 或 M,對 KB 使用 k 或 K。例如,以下所有內(nèi)容都相當于說最大的 Java 堆空間為 1GB:

java -Xmx1073741824 com.mycompany.MyClass
    java -Xmx1048576k com.mycompany.MyClass
    java -Xmx1024m com.mycompany.MyClass
    java -Xmx1g com.mycompany.MyClass

Java OOM系列專題:

第一篇:Java OOM 原理篇 : 什么是 Java OOM

第二篇:Java OOM 基礎(chǔ)篇:常見的OutOfMemoryError 場景一:Java heap space 堆溢出問題詳解

第三篇:Java OOM 基礎(chǔ)篇:常見的OutOfMemoryError 場景二 : GC overhead limit exceeded 問題詳解

第四篇:Java OOM 基礎(chǔ)篇:常見的OutOfMemoryError 場景三: PermGen space 永久空間問題詳解

第五篇:Java OOM 基礎(chǔ)篇:常見的OutOfMemoryError 場景四: Permgen size 元空間問題詳解

第六篇:Java OOM 實戰(zhàn)篇:應(yīng)用故障之Java heap space 堆溢出實戰(zhàn)

第七篇:Java OOM 高級篇:體驗了一把線上CPU100%及應(yīng)用OOM的排查和解決過程

第八篇:Java OOM 高級篇:線上Docker 上Springboot程序OOM問題的排查分享

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容