JVM中十種內存溢出的解決方法

2021-01-19 聊聊微服務

導言:

對於java程式設計師來說,在虛擬機自動內存管理機制的幫助下,不需要自己實現釋放內存,不容易出現內存洩漏和內存溢出的問題,由虛擬機管理內存這一切看起來非常美好,但是一旦出現內存溢出或者內存洩漏的問題,對於不熟悉jvm虛擬機是怎麼使用內存的話,那麼排查錯誤將會是一項非常艱巨的任務。所以在了解內存溢出之前先要搞明白JVM的內存模型。

JVM(Java虛擬機)是一個抽象的計算模型。就如同一臺真實的機器,它有自己的指令集和執行引擎,可以在運行時操控內存區域。目的是為構建在其上運行的應用程式提供一個運行環境。JVM可以解讀指令代碼並與底層進行交互:包括作業系統平臺和執行指令並管理資源的硬體體系結構。

JVM內存模型

根據 JVM8 規範,JVM 運行時內存共分為虛擬機棧、堆、元空間、程序計數器、本地方法棧五個部分。還有一部分內存叫直接內存,屬於作業系統的本地內存,也是可以直接操作的。

jvm8內存模型

1. 元空間(Metaspace)

元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。

2.虛擬機棧(JVM Stacks)

每個線程有一個私有的棧,隨著線程的創建而創建。棧裡面存著的是一種叫「棧幀」的東西,每個方法會創建一個棧幀,棧幀中存放了局部變量表(基本數據類型和對象引用)、操作數棧、方法出口等信息。棧的大小可以固定也可以動態擴展。

3. 本地方法棧(Native Method Stack)

與虛擬機棧類似,區別是虛擬機棧執行java方法,本地方法站執行native方法。在虛擬機規範中對本地方法棧中方法使用的語言、使用方法與數據結構沒有強制規定,因此虛擬機可以自由實現它。

4. 程序計數器(Program Counter Register)

程序計數器可以看成是當前線程所執行的字節碼的行號指示器。在任何一個確定的時刻,一個處理器(對於多內核來說是一個內核)都只會執行一條線程中的指令。因此,為了線程切換後能恢復到正確的執行位置,每條線程都需要一個獨立的程序計數器,我們稱這類內存區域為「線程私有」內存。

5.堆內存(Heap)

堆內存是 JVM 所有線程共享的部分,在虛擬機啟動的時候就已經創建。所有的對象和數組都在堆上進行分配。這部分空間可通過 GC 進行回收。當申請不到空間時會拋出 OutOfMemoryError。堆是JVM內存佔用最大,管理最複雜的一個區域。其唯一的用途就是存放對象實例:所有的對象實例及數組都在對上進行分配。jdk1.8後,字符串常量池從永久代中剝離出來,存放在隊中。

6.直接內存(Direct Memory)

直接內存並不是虛擬機運行時數據區的一部分,也不是Java 虛擬機規範中農定義的內存區域。在JDK1.4 中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O 方式,它可以使用native 函數庫直接分配堆外內存,然後通脫一個存儲在Java堆中的DirectByteBuffer 對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回複製數據。

內存溢出的十個場景

JVM運行時首先需要類加載器(classLoader)加載所需類的字節碼文件。加載完畢交由執行引擎執行,在執行過程中需要一段空間來存儲數據(類比CPU與主存)。這段內存空間的分配和釋放過程正是我們需要關心的運行時數據區。內存溢出的情況就是從類加載器加載的時候開始出現的,內存溢出分為兩大類:OutOfMemoryError和StackOverflowError。以下舉出10個內存溢出的情況,並通過實例代碼的方式講解了是如何出現內存溢出的。

1.java堆內存溢出

當出現java.lang.OutOfMemoryError:Java heap space異常時,就是堆內存溢出了。

1.問題描述

設置的jvm內存太小,對象所需內存太大,創建對象時分配空間,就會拋出這個異常。流量/數據峰值,應用程式自身的處理存在一定的限額,比如一定數量的用戶或一定數量的數據。而當用戶數量或數據量突然激增並超過預期的閾值時,那麼就會峰值停止前正常運行的操作將停止並觸發java . lang.OutOfMemoryError:Java堆空間錯誤

2.示例代碼

編譯以下代碼,執行時jvm參數設置為-Xms20m -Xmx20m執行就會出現堆內存溢出。如果一次請求只分配一次5m的內存的話,請求量很少垃圾回收正常就不會出錯,但是一旦並發上來就會超出最大內存值,就會拋出內存溢出。

3.解決方法

首先,如果代碼沒有什麼問題的情況下,可以適當調整-Xms和-Xmx兩個jvm參數,使用壓力測試來調整這兩個參數達到最優值。其次,儘量避免大的對象的申請,像文件上傳,大批量從資料庫中獲取,這是需要避免的,儘量分塊或者分批處理,有助於系統的正常穩定的執行。最後,儘量提高一次請求的執行速度,垃圾回收越早越好,否則,大量的並發來了的時候,再來新的請求就無法分配內存了,就容易造成系統的雪崩。

2.java堆內存洩漏

1.問題描述

Java中的內存洩漏是一些對象不再被應用程式使用但垃圾收集無法識別的情況。因此,這些未使用的對象仍然在Java堆空間中無限期地存在。不停的堆積最終會觸發java . lang.OutOfMemoryError。

2.示例代碼

當執行上面的代碼時,可能會期望它永遠運行,不會出現任何問題,假設單純的緩存解決方案只將底層映射擴展到10,000個元素,而不是所有鍵都已經在HashMap中。然而事實上元素將繼續被添加,因為key類並沒有重寫它的equals()方法。隨著時間的推移,隨著不斷使用的洩漏代碼,「緩存」的結果最終會消耗大量Java堆空間。當洩漏內存填充堆區域中的所有可用內存時,垃圾收集無法清理它,java . lang.OutOfMemoryError。

3.解決辦法

相對來說對應的解決方案比較簡單:重寫equals方法即可:

3.垃圾回收超時內存溢出

1、問題描述

當應用程式耗盡所有可用內存時,GC開銷限制超過了錯誤,而GC多次未能清除它,這時便會引發java.lang.OutOfMemoryError。當JVM花費大量的時間執行GC,而收效甚微,而一旦整個GC的過程超過限制便會觸發錯誤(默認的jvm配置GC的時間超過98%,回收堆內存低於2%)。

2.示例代碼

3.解決方法

要減少對象生命周期,儘量能快速的進行垃圾回收。

4.Metaspace內存溢出

1.問題描述

元空間的溢出,系統會拋出java.lang.OutOfMemoryError: Metaspace。出現這個異常的問題的原因是系統的代碼非常多或引用的第三方包非常多或者通過動態代碼生成類加載等方法,導致元空間的內存佔用很大。

2.示例代碼

以下是用循環動態生成class的方式來模擬元空間的內存溢出的。

3.如何解決元空間的內存溢出呢?

默認情況下,元空間的大小僅受本地內存限制。但是為了整機的性能,儘量還是要對該項進行設置,以免造成整機的服務停機。

1)優化參數配置,避免影響其他JVM進程

-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。

-XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。

除了上面兩個指定大小的選項以外,還有兩個與 GC 相關的屬性: -XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少為分配空間所導致的垃圾收集 。-XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少為釋放空間所導致的垃圾收集。

2)慎重引用第三方包

對第三方包,一定要慎重選擇,不需要的包就去掉。這樣既有助於提高編譯打包的速度,也有助於提高遠程部署的速度。

3)關注動態生成類的框架

對於使用大量動態生成類的框架,要做好壓力測試,驗證動態生成的類是否超出內存的需求會拋出異常。

5.直接內存內存溢出

1.問題描述

在使用ByteBuffer中的allocateDirect()的時候會用到,很多javaNIO(像netty)的框架中被封裝為其他的方法,出現該問題時會拋出java.lang.OutOfMemoryError: Direct buffer memory異常。如果你在直接或間接使用了ByteBuffer中的allocateDirect方法的時候,而不做clear的時候就會出現類似的問題。

2.示例代碼

3.解決辦法

如果經常有類似的操作,可以考慮設置參數:-XX:MaxDirectMemorySize,並及時clear內存。

6.棧內存溢出

1.問題描述

當一個線程執行一個Java方法時,JVM將創建一個新的棧幀並且把它push到棧頂。此時新的棧幀就變成了當前棧幀,方法執行時,使用棧幀來存儲參數、局部變量、中間指令以及其他數據。當一個方法遞歸調用自己時,新的方法所產生的數據(也可以理解為新的棧幀)將會被push到棧頂,方法每次調用自己時,會拷貝一份當前方法的數據並push到棧中。因此,遞歸的每層調用都需要創建一個新的棧幀。這樣的結果是,棧中越來越多的內存將隨著遞歸調用而被消耗,如果遞歸調用自己一百萬次,那麼將會產生一百萬個棧幀。這樣就會造成棧的內存溢出。

2.示例代碼

3.解決辦法

如果程序中確實有遞歸調用,出現棧溢出時,可以調高-Xss大小,就可以解決棧內存溢出的問題了。遞歸調用防止形成死循環,否則就會出現棧內存溢出。

7.創建本地線程內存溢出

1.問題描述

線程基本只佔用heap以外的內存區域,也就是這個錯誤說明除了heap以外的區域,無法為線程分配一塊內存區域了,這個要麼是內存本身就不夠,要麼heap的空間設置得太大了,導致了剩餘的內存已經不多了,而由於線程本身要佔用內存,所以就不夠用了。

3.解決方法

首先檢查作業系統是否有線程數的限制,使用shell也無法創建線程,如果是這個問題就需要調整系統的最大可支持的文件數。日常開發中儘量保證線程最大數的可控制的,不要隨意使用線程池。不能無限制的增長下去。

8.超出交換區內存溢出

1.問題描述

在Java應用程式啟動過程中,可以通過-Xmx和其他類似的啟動參數限制指定的所需的內存。而當JVM所請求的總內存大於可用物理內存的情況下,作業系統開始將內容從內存轉換為硬碟。一般來說JVM會拋出Out of swap space錯誤,代表應用程式向JVM native heap請求分配內存失敗並且native heap也即將耗盡時,錯誤消息中包含分配失敗的大小(以字節為單位)和請求失敗的原因。

2.解決辦法

增加系統交換區的大小,我個人認為,如果使用了交換區,性能會大大降低,不建議採用這種方式,生產環境儘量避免最大內存超過系統的物理內存。其次,去掉系統交換區,只使用系統的內存,保證應用的性能。

9.數組超限內存溢出

1.問題描述

有的時候會碰到這種內存溢出的描述Requested array size exceeds VM limit,一般來說java對應用程式所能分配數組最大大小是有限制的,只不過不同的平臺限制有所不同,但通常在1到21億個元素之間。當Requested array size exceeds VM limit錯誤出現時,意味著應用程式試圖分配大於Java虛擬機可以支持的數組。JVM在為數組分配內存之前,會執行特定平臺的檢查:分配的數據結構是否在此平臺是可尋址的。

2.示例代碼

以下就是代碼就是數組超出了最大限制。

3.解決方法

因此數組長度要在平臺允許的長度範圍之內。不過這個錯誤一般少見的,主要是由於Java數組的索引是int類型。 Java中的最大正整數為2 ^ 31 - 1 = 2,147,483,647。 並且平臺特定的限制可以非常接近這個數字,例如:我的環境上(64位macOS,運行Jdk1.8)可以初始化數組的長度高達2,147,483,645(Integer.MAX_VALUE-2)。若是在將數組的長度再增加1達到nteger.MAX_VALUE-1會出現的OutOfMemoryError。

系統殺死進程內存溢出

1.問題概述

在描述該問題之前,先熟悉一點作業系統的知識:作業系統是建立在進程的概念之上,這些進程在內核中作業,其中有一個非常特殊的進程,稱為「內存殺手(Out of memory killer)」。當內核檢測到系統內存不足時,OOM killer被激活,檢查當前誰佔用內存最多然後將該進程殺掉。一般Out of memory:Kill process or sacrifice child錯會在當可用虛擬虛擬內存(包括交換空間)消耗到讓整個作業系統面臨風險時,會被觸發。在這種情況下,OOM Killer會選擇「流氓進程」並殺死它。

2.示例代碼

3.解決方法

雖然增加交換空間的方式可以緩解Java heap space異常,還是建議最好的方案就是升級系統內存,讓java應用有足夠的內存可用,就不會出現這種問題。

總結

通過以上的10種出現內存溢出情況,大家在實際碰到問題時也就會知道怎麼解決了,在實際編碼中也要記得:1.第三方jar包要慎重引入,堅決去掉沒有用的jar包,提高編譯的速度和系統的佔用內存。

2.對於大的對象或者大量的內存申請,要進行優化,大的對象要分片處理,提高處理性能,減少對象生命周期。

3.儘量固定線程的數量,保證線程佔用內存可控,同時需要大量線程時,要優化好作業系統的最大可打開的連接數。

4.對於遞歸調用,也要控制好遞歸的層級,不要太高,超過棧的深度。

5.分配給棧的內存並不是越大越好,因為棧內存越大,線程多,留給堆的空間就不多了,容易拋出OOM。JVM的默認參數一般情況沒有問題(包括遞歸)。

———— / END / ————

相關焦點

  • 「JVM教程與調優」了解JVM 堆內存溢出以及非堆內存溢出
    記得選中我們的Arguments,在JVM 參數中,將我們的值設置進去。最後點擊Run運行起來。然後我們在瀏覽器中請求:localhost:8080/heap我們再觀察控制臺列印:通過列印結果,我們可以看到堆內存溢出了。
  • 概述:JVM內存模型、線程隔離數據區、線程共享數據區
    在程序運行的這一過程中,jvm會將其管理的內存空間劃分為不同的區域,這些區域各有各的用途,我們將其分為五類:方法區堆虛擬機棧本地方法棧程序計數器其中方法區和堆是線程共享的,隨jvm啟動和停止而創建和銷毀;而虛擬機棧、本地方法棧和程序計數器則是線程私有的,隨線程的創建和結束而創建和銷毀。
  • JVM內存區域之線程私有區域
    對於java程式設計師來說,在虛擬機自動內存管理機制的幫助下,不再需要為沒一個new操作去配對的free/delete(C、C++語言對對象的刪除和內存釋放操作),不容易出現內存洩漏和內存溢出問題,看起來由虛擬機管理內存一切看起來很美好。
  • Excel答疑:怎麼解決內存溢出的問題?
    相信有心的小夥伴可以發現,Excel在使用過程中有很多限制,或者說有的效果實現起來太過複雜,那我們就只能轉換角度,用另外的方法去實現,而方法,那就非VBA莫數了。小編呢也打算去研究一下,可是當小編興致衝衝的打開excel,想研究一下VBE界面時,卻發現Excel非常不友善的提示 'VBE6EXT.OLB'不能被加載內存溢出。
  • java內存溢出問題(工作中常用、面試中常問的一個知識點)
    內存溢出是指應用系統中存在無法回收的內存或使用的內存過多,最終使得程序運行要用到的內存大於虛擬機能提供的最大內存。這篇文章整理自《深入理解java虛擬機》。因為內存溢出問題不僅是工作中的一個重要方面,而且面試中也是經常問。
  • Facebook 開源 oomd,一種處理內存溢出的新方法
    oomd 是用戶空間內存溢出殺手(OOM Killer),它在最近關於塊 I/O 延遲控制器的文章中有被提及到。當內存不足時,內存溢出殺手會殺掉一些進程,它的主要任務是保護內核,因此應用程式可能會受到影響。相比傳統的 Linux 內存溢出殺手,oomd 會全面監視系統,評估系統是否處於不可恢復的工作負荷下。在系統的 OOM Killer 作用前,oomd 會在用戶空間採取糾正措施。
  • 系統內存佔用較高?
    因此OOM溢出是必然的。為什麼調用了線程的interrupt方法並沒有終止線程?或者說是因為jvm需要一點時間去響應這個方法?其實並非如此,感興趣的同學可以把循環次數加的更大一些,在循環開始幾次就進行interrupt,你會發現結果還是這樣。
  • java創建對象的過程詳解(從內存角度分析)
    小結:創建一個對象包含下面兩個過程:1、類構造器完成類初始化(分配內存、賦予默認值)2、類實例化(賦予給定值)二、類初始化下面我們直接給出一個例子看一下java是如何初始化的。我們知道一個類中,往往包含靜態變量、靜態代碼塊、變量、普通方法、構造方法等信息。那麼他們是如何初始化的呢?
  • 這一定是全網寫JVM最好的文章之一—JVM運行時數據區
    為什麼說Java是一次編譯到處運行不管是windows,mac,還是linux,unix,oracle官網上都提供了下載對應的jdk版本,我們只需要編寫java代碼,因為jvm不同作業系統上下載的不同版本的,所以不需要我們管各種作業系統之間的區別,jvm只識別字節碼,所以jvm其實跟語言是解耦的,也沒有直接關聯。
  • 面試中常被問的11組JVM關係,收藏了
    如果是下面這種情況,就是典型的方法區中元素指向堆中的對象。堆指向方法區方法區中會包含類的信息,對象保存在堆中,創建一個對象的前提是有對應的類信息,這個類信息就在方法區中。JVM 內部數據結構的一些引用,比如 sun.jvm.hotspot.memory.Universe 類。用於同步的監控對象,比如調用了對象的 wait() 方法。
  • 如何讓手機內存變大?內存不足解決方法【詳解】
    在購買手機的時候,有些人就選擇了一些內存比較大的機型,避免日後自己因手機內存小而帶來不好的體驗。但是使用使用中人們就會發現,再多的手機內存也不夠用,隨著軟體不斷升級、手機中使用的垃圾增多等等,人們也開始發現手機中的內存越來越小了。手機內存一小對正常使用肯定是會造成麻煩的,那麼怎樣讓手機內存變大呢?
  • 深入理解JVM:學習Java的內存模型
    主內存與工作內存Java內存模型的主要目的是定義程序中各種變量的訪問規則,即關注在虛擬機中把變量值存儲到內存和從內存中取出變量值這樣的底層細節。Java內存模型規定了所有變量都存儲在主內存(Main Memory)中(此處的內存為Java虛擬機內存的一部分)。每條線程還有自己的工作內存(Working Memory),線程的工作內存中保存了被該線程使用的變量內存副本,線程對變量的所有操作都必須在工作內存中進行,也不能直接讀寫主內存中數據。不同的線程之間變量值的傳遞均需要通過主內存來完成。
  • 劍與遠徵英雄卡太多怎麼辦 英雄卡溢出解決方法
    劍與遠徵英雄卡溢出是很多玩家面臨的問題,本次就為大家帶來了英雄卡溢出解決方法,告訴大家英雄卡溢出怎麼辦,想了解的朋友可以參考,快一起看看吧。
  • 談談著名的緩衝區溢出
    緩衝區就是作業系統為函數執行專門劃分出的一段內存,包括棧(自動變量)、堆(動態內存)和靜態數據區(全局或靜態)。其中緩衝區溢出發生在棧裡,棧存放了函數的參數、返回地址、EBP(EBP是當前函數的存取指針,即存儲或者讀取數時的指針基地址,可以看成一個標準的函數起始代碼)和局部變量。
  • 筆記本電腦內存不足怎麼辦 解決方法【圖文教程】
    經常在用電腦的時候,有時候會出現內存不足的情況。說內存不足一般是指虛擬內存不足,有時是指C盤空間不足,有時中了木馬也會提示內存不足。那麼, 筆記本 電腦內存不足怎麼辦 ?下面來詳細看一下。   電腦內存不足怎麼辦  設置虛擬內存方法:右擊我的電腦/屬性/高級/性能中的設置/高級/虛擬內存中的更改/選自定義大小,在初始大小和最大值,同時設為你的物理內存的兩倍。
  • JVM垃圾回收機制如何判斷是否死亡?詳解引用計數法和可達性分析
    原因很簡單:我們不想止於「增刪改查工程師」這樣的初級水平,一旦程序發生了內存溢出、內存洩漏等問題時,我們可以用已掌握的知識更好的調節和優化我們的代碼。在學這章節之前,默認大家已經了解並掌握了Java內存運行時的五個區域的功能:方法區、Java堆、虛擬機棧、本地方法棧、程序計數器。
  • 程式設計師,這些內存溢出 MAT 排查工具,你一定要知道
    本文總結了排查內存溢出問題的MAT工具,先來看看本文目錄:Java 堆內存分析工具。訪問http://localhost:8080/mat不過一會就報異常了。點擊圖中的最大區域outgoing references 對象的引出incoming references 對象的引入另外Path to GC Roots這是快速分析的一個常用功能,顯示和 GC Roots 之間的路徑。
  • JVM基本結構
    各部分介紹如下:類加載子系統:負責從文件系統或者網絡中加載Class信息,加載的類信息存放在一個叫方法區的內存空間中方法區:除了包含加載的類信息之外,還包含運行時常量池信息,包括字符串字面量以及數字常量
  • 為什麼手機內存老是不足 如何解決手機內存不足的狀況【圖解】
    很多親都喜歡大內存的手機,但眾所周知大內存的手機價格往往都比較高。所以有些親就買了相對較低內存的手機,買好之後就後悔了,根本不夠用嘛。尤其對於女同胞們,光光相冊就是滿滿的好幾個G的內存佔用量。再加上些雜七雜八的應用,不知不覺「手機內存不足」的提示就不停地跳出來。那麼手機內存老是不足該怎麼辦呢?小編在這裡給大家整理了一些方法,希望有用。