深刻理解Java虛擬機-Java內存區域與內存溢出異常

Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的"高牆",牆外面的人想進去,牆裏面的人卻想出來。css

概述

對於從事C、C++程序開發的開發人員來講,在內存管理領域,他們既是擁有最高權力的「皇帝」又是從事最基礎工做的「勞動人民」——既擁有每個對象的「全部權」,又擔負着每個對象生命開始到終結的維護責任。
對於Java程序員來講,在虛擬機自動內存管理機制的幫助下,再也不須要爲每個new操做去寫配對的delete/free代碼,不容易出現內存泄漏和內存溢出問題,由虛擬機管理內存這一切看起來都很美好。不過,也正是由於Java程序員把內存控制的權力交給了Java虛擬機,一旦出現內存泄漏和溢出方面的問題,若是不瞭解虛擬機是怎樣使用內存的,那麼排查錯誤將會成爲一項異常艱難的工做。
java

運行時數據區域

JVM包含兩個子系統和兩個組件,兩個子系統爲Class loader(類裝載)、Execution engine(執行引擎);兩個組件爲Runtime data area(運行時數據區)、Native Interface(本地接口)。程序員

Class loader(類裝載):根據給定的全限定名類名(如:java.lang.Object)來裝載class文件到Runtime data area中的method area。算法

Execution engine(執行引擎):執行classes中的指令。sql

Native Interface(本地接口):與native libraries交互,是其它編程語言交互的接口。typescript

Runtime data area(運行時數據區域):這就是咱們常說的JVM的內存。編程

Java 虛擬機在執行 Java 程序的過程當中會把它所管理的內存區域劃分爲若干個不一樣的數據區域。這些區域都有各自的用途,以及建立和銷燬的時間,有些區域隨着虛擬機進程的啓動而存在,有些區域則是依賴線程的啓動和結束而創建和銷燬。Java 虛擬機所管理的內存被劃分爲以下幾個區域:windows

程序計數器(線程私有)

程序計數器是一塊較小的內存區域,能夠看作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏,字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。「屬於線程私有的內存區域」數組

附加:安全

  1. 當前線程所執行的字節碼行號指示器

  2. 每一個線程都有一個本身的PC計數器。

  3. 線程私有的,生命週期與線程相同,隨JVM啓動而生,JVM關閉而死。

  4. 線程執行Java方法時,記錄其正在執行的虛擬機字節碼指令地址

  5. 線程執行Native方法時,計數器記錄爲(Undefined)。

  6. 惟一在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況區域。

Java虛擬機棧(線程私有)

線程私有內存空間,它的生命週期和線程相同。線程執行期間,每一個方法被執行時,都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做棧、動態連接、方法出口等信息。每一個方法從被調用到執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。「屬於線程私有的內存區域」

注意:下面的內容爲附加內容,對Java虛擬機棧進行詳細說明,感興趣的小夥伴能夠有針對性的閱讀

下面依次解釋棧幀裏的四種組成元素的具體結構和功能:

局部變量表

局部變量表局部變量表是 Java 虛擬機棧的一部分,是一組變量值的存儲空間,用於存儲方法參數局部變量。在 Class文件的方法表的 Code 屬性的 max_locals 指定了該方法所需局部變量表的最大容量

局部變量表在編譯期間分配內存空間,能夠存放編譯期的各類變量類型:

  1. 基本數據類型 :booleanbytecharshortintfloatlongdouble8種;

  2. 對象引用類型 :reference,指向對象起始地址引用指針;不等同於對象自己,根據不一樣的虛擬機實現,它多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或者其餘與此對象相關的位置

  3. 返回地址類型 :returnAddress,返回地址的類型。指向了一條字節碼指令的地址

變量槽(Variable Slot):

變量槽局部變量表最小單位,規定大小爲32位。對於64位的longdouble變量而言,虛擬機會爲其分配兩個連續Slot空間。

操做數棧

操做數棧Operand Stack)也常稱爲操做棧,是一個後入先出棧。在 Class 文件的 Code 屬性的 max_stacks 指定了執行過程當中最大的棧深度。Java虛擬機的解釋執行引擎被稱爲基於棧的執行引擎 ,其中所指的就是指-操做數棧

  1. 局部變量表同樣,操做數棧也是一個以32字長爲單位的數組。

  2. 虛擬機在操做數棧中可存儲的數據類型intlongfloatdoublereferencereturnType等類型 (對於byteshort以及char類型的值在壓入到操做數棧以前,也會被轉換爲int)。

  3. 局部變量表不一樣的是,它不是經過索引來訪問,而是經過標準的棧操做 — 壓棧出棧來訪問。好比,若是某個指令把一個值壓入到操做數棧中,稍後另外一個指令就能夠彈出這個值來使用。

虛擬機把操做數棧做爲它的工做區——大多數指令都要從這裏彈出數據,執行運算,而後把結果壓回操做數棧

beginiload_0 // push the int in local variable 0 onto the stackiload_1 // push the int in local variable 1 onto the stackiadd // pop two ints, add them, push resultistore_2 // pop int, store into local variable 2end

在這個字節碼序列裏,前兩個指令 iload_0 和 iload_1 將存儲在局部變量表中索引爲01的整數壓入操做數棧中,其後iadd指令從操做數棧中彈出那兩個整數相加,再將結果壓入操做數棧。第四條指令istore_2則從操做數棧中彈出結果,並把它存儲到局部變量表索引爲2的位置。

下圖詳細表述了這個過程當中局部變量表操做數棧的狀態變化(圖中沒有使用的局部變量表操做數棧區域以空白表示)。

動態連接

每一個棧幀都包含一個指向運行時常量池中所屬的方法引用,持有這個引用是爲了支持方法調用過程當中的動態連接

Class文件的常量池中存在有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用:

  1. 靜態解析:一部分會在類加載階段或第一次使用的時候轉化爲直接引用(如finalstatic域等),稱爲靜態解析

  2. 動態解析:另外一部分將在每一次的運行期間轉化爲直接引用,稱爲動態連接

方法返回地址

當一個方法開始執行之後,只有兩種方法能夠退出當前方法:

  1. 正常返回:當執行遇到返回指令,會將返回值傳遞給上層的方法調用者,這種退出的方式稱爲正常完成出口(Normal Method Invocation Completion),通常來講,調用者的PC計數器能夠做爲返回地址。

  2. 異常返回:當執行遇到異常,而且當前方法體內沒有獲得處理,就會致使方法退出,此時是沒有返回值的,稱爲異常完成出口(Abrupt Method Invocation Completion),返回地址要經過異常處理器表來肯定。

當一個方法返回時,可能依次進行如下3個操做:

  1. 恢復上層方法局部變量表操做數棧

  2. 返回值壓入調用者棧幀操做數棧

  3. PC計數器的值指向下一條方法指令位置。

小結

注意:在Java虛擬機規範中,對這個區域規定了兩種異常。

其一:若是當前線程請求的棧深度大於虛擬機棧所容許的深度,將會拋出 StackOverflowError 異常(在虛擬機棧不容許動態擴展的狀況下);

其二:若是擴展時沒法申請到足夠的內存空間,就會拋出 OutOfMemoryError 異常。

本地方法棧(線程私有)

本地方法棧Java虛擬機棧發揮的做用很是類似,主要區別是Java虛擬機棧執行的是Java方法服務,而本地方法棧執行Native方法服務(一般用C編寫)。

有些虛擬機發行版本(譬如Sun HotSpot虛擬機)直接將本地方法棧Java虛擬機棧合二爲一。與虛擬機棧同樣,本地方法棧也會拋出StackOverflowErrorOutOfMemoryError異常。

Java堆(全局共享)

對大多數應用而言,Java 堆是虛擬機所管理的內存中最大的一塊,是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一做用就是存放對象實例,幾乎全部的對象實例都是在這裏分配的(不絕對,在虛擬機的優化策略下,也會存在棧上分配、標量替換的狀況,後面的章節會詳細介紹)。

Java 堆是 GC 回收的主要區域,所以不少時候也被稱爲 GC 堆。

從內存回收的角度看,因爲如今收集器基本都採用分代收集算法,因此在Java堆被劃分紅兩個不一樣的區域:新生代 (Young Generation) 、老年代 (Old Generation) 。新生代 (Young) 又被劃分爲三個區域:一個Eden區和兩個Survivor區 - From Survivor區和To Survivor區。不過不管如何劃分,都與存放內容無關,不管哪一個區域,存儲的都仍然時對象實例,記你一步劃分的目的是爲了使JVM可以更好的管理堆內存中的對象,包括內存的分配以及回收。

簡要概括:新的對象分配是首先放在年輕代 (Young Generation) 的Eden區,Survivor區做爲Eden區和Old區的緩衝,在Survivor區的對象經歷若干次收集仍然存活的,就會被轉移到老年代Old中。

從內存回收的角度看,線程共享的 Java 堆可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。「屬於線程共享的內存區域」

方法區(全局共享)

方法區和Java堆同樣,爲多個線程共享,它用於存儲類信息常量靜態常量即時編譯後的代碼等數據。Non-Heap(非堆)「屬於線程共享的內存區域」

運行時常量池

運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池(Constant Pool Table),用於存放編譯期生成的各類字面常量和符號引用,這部份內容會在類加載後進入方法區的運行時常量池。

下面信息爲附加信息

  • HotSpot虛擬機中,將方法區稱爲「永久代」,本質上二者並不等價,僅僅是由於HotSpot虛擬機把GC分代收集擴展至方法區。

  • JDK 7的HotSpot中,已經將本來存放於永久代中的字符串常量池移出。

  • 根據虛擬機規範的規定,當方法區沒法知足內存分配需求時,將會拋出OutOfMemoryError異常。當常量池沒法再申請到內存時也會拋出OutOfMemoryError異常。

  • JDK 8的HotSpot中,已經將永久代廢除,用元數據實現了方法區。元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。理論上取決於32位/64位系統可虛擬的內存大小。可見也不是無限制的,須要配置參數。

直接內存

直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。Java 中的 NIO 可使用 Native 函數直接分配堆外內存,一般直接內存的速度會優於Java堆內存,而後經過一個存儲在 Java 堆中的 DiectByteBuffer 對象做爲這塊內存的引用進行操做。這樣能在一些場景顯著提升性能,對於讀寫頻繁、性能要求高的場景,能夠考慮使用直接內存,由於避免了在 Java 堆和 Native 堆中來回複製數據。直接內存不受 Java 堆大小的限制。

HotSpot虛擬機對象探祕

對象的建立

說到對象的建立,首先讓咱們看看 Java 中提供的幾種對象建立方式:

Header 解釋
使用new關鍵字 調用了構造函數
使用Class的newInstance方法 調用了構造函數
使用Constructor類的newInstance方法 調用了構造函數
使用clone方法 沒有調用構造函數
使用反序列化 沒有調用構造函數

下面是對象建立的主要流程:

虛擬機遇到一條new指令時,先檢查常量池是否已經加載相應的類,若是沒有,必須先執行相應的類加載。類加載經過後,接下來分配內存。若Java堆中內存是絕對規整的,使用「指針碰撞「方式分配內存;若是不是規整的,就從空閒列表中分配,叫作」空閒列表「方式。劃份內存時還須要考慮一個問題-併發,也有兩種方式: CAS同步處理,或者本地線程分配緩衝(Thread Local Allocation Buffer, TLAB)。而後內存空間初始化操做,接着是作一些必要的對象設置(元信息、哈希碼…),最後執行<init>方法。

下面內容是對象建立的詳細過程

對象的建立一般是經過new關鍵字建立一個對象的,當虛擬機接收到一個new指令時,它會作以下的操做。

1.判斷對象對應的類是否加載、連接、初始化

虛擬機接收到一條new指令時,首先會去檢查這個指定的參數是否能在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已被類加載器加載、連接和初始化過。若是沒有則先執行相應的類加載過程。

2.爲對象分配內存

類加載完成後,接着會在Java堆中劃分一塊內存分配給對象。內存分配根據Java堆是否規整,有兩種方式:

  • 指針碰撞:若是Java堆的內存是規整,即全部用過的內存放在一邊,而空閒的的放在另外一邊。分配內存時將位於中間的指針指示器向空閒的內存移動一段與對象大小相等的距離,這樣便完成分配內存工做。

  • 空閒列表:若是Java堆的內存不是規整的,則須要由虛擬機維護一個列表來記錄那些內存是可用的,這樣在分配的時候能夠從列表中查詢到足夠大的內存分配給對象,並在分配後更新列表記錄。

選擇哪一種分配方式是由 Java 堆是否規整來決定的,而 Java 堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。

3.處理併發安全問題

對象的建立在虛擬機中是一個很是頻繁的行爲,哪怕只是修改一個指針所指向的位置,在併發狀況下也是不安全的,可能出現正在給對象 A 分配內存,指針還沒來得及修改,對象 B 又同時使用了原來的指針來分配內存的狀況。解決這個問題有兩種方案:

  • 對分配內存空間的動做進行同步處理(採用 CAS + 失敗重試來保障更新操做的原子性);

  • 把內存分配的動做按照線程劃分在不一樣的空間之中進行,即每一個線程在 Java 堆中預先分配一小塊內存,稱爲本地線程分配緩衝(Thread Local Allocation Buffer, TLAB)。哪一個線程要分配內存,就在哪一個線程的 TLAB 上分配。只有 TLAB 用完並分配新的 TLAB 時,才須要同步鎖。經過-XX:+/-UserTLAB參數來設定虛擬機是否使用TLAB。

4.初始化分配到的內存空間

內存分配完後,虛擬機要將分配到的內存空間初始化爲零值(不包括對象頭)。若是使用了 TLAB,這一步會提早到 TLAB 分配時進行。這一步保證了對象的實例字段在 Java 代碼中能夠不賦初始值就直接使用。

5.設置對象的對象頭

接下來設置對象頭(Object Header)信息,包括對象的所屬類、對象的HashCode和對象的GC分代年齡等數據存儲在對象的對象頭中。

6.執行init方法進行初始化

執行init方法,初始化對象的成員變量、調用類的構造方法,這樣一個對象就被建立了出來。

對象的內存佈局

HotSpot虛擬機中,對象在內存中存儲的佈局能夠分爲三塊區域:對象頭Header)、實例數據Instance Data)和對齊填充Padding)。

對象頭

HotSpot虛擬機中,對象頭有兩部分信息組成:運行時數據 和 類型指針,若是是數組對象,還有一個保存數組長度的空間。

Mark Word(運行時數據):用於存儲對象自身運行時的數據,如哈希碼(hashCode)、GC分帶年齡線程持有的鎖偏向線程ID 等信息。在32位系統佔4字節,在64位系統中佔8字節;

HotSpot虛擬機對象頭Mark Word在其餘狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下對象的存儲內容以下表所示:

存儲內容 標誌位 狀態
對象哈希碼、對象分代年齡 01 未鎖定
指向鎖記錄的指針 00 輕量級鎖定
指向重量級鎖的指針 10 膨脹(重量級鎖定)
空,不須要記錄信息 11 GC標記
偏向線程ID、偏向時間戳、對象分代年齡 01 可偏向
  • Class Pointer(類型指針):用來指向對象對應的Class對象(其對應的元數據對象)的內存地址。在32位系統佔4字節,在64位系統中佔8字節;

  • Length:若是是數組對象,還有一個保存數組長度的空間,佔4個字節;

實例數據

實例數據 是對象真正存儲的有效信息,不管是從父類繼承下來的仍是該類自身的,都須要記錄下來,而這部分的存儲順序受虛擬機的分配策略定義的順序的影響。

默認分配策略:

long/double -> int/float -> short/char -> byte/boolean -> reference

若是設置了-XX:FieldsAllocationStyle=0(默認是1),那麼引用類型數據就會優先分配存儲空間:

reference -> long/double -> int/float -> short/char -> byte/boolean

結論:

分配策略老是按照字節大小由大到小的順序排列,相同字節大小的放在一塊兒。

對齊填充

無特殊含義,不是必須存在的,僅做爲佔位符。

HotSpot虛擬機要求每一個對象的起始地址必須是8字節的整數倍,也就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(32位爲1倍,64位爲2倍),所以,當對象實例數據部分沒有對齊的時候,就須要經過對齊填充來補全。

對象的訪問定位

Java程序須要經過 JVM 棧上的引用訪問堆中的具體對象。對象的訪問方式取決於 JVM 虛擬機的實現。目前主流的訪問方式有 句柄 和 直接指針 兩種方式。

指針: 指向對象,表明一個對象在內存中的起始地址。

句柄: 能夠理解爲指向指針的指針,維護着對象的指針。句柄不直接指向對象,而是指向對象的指針(句柄不發生變化,指向固定內存地址),再由對象的指針指向對象的真實內存地址。

句柄訪問

Java堆中劃分出一塊內存來做爲句柄池,引用中存儲對象的句柄地址,而句柄中包含了對象實例數據對象類型數據各自的具體地址信息,具體構造以下圖所示:

優點:引用中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只會改變句柄中實例數據指針,而引用自己不須要修改。

直接指針

若是使用直接指針訪問,引用 中存儲的直接就是對象地址,那麼Java堆對象內部的佈局中就必須考慮如何放置訪問類型數據的相關信息。

優點:速度更,節省了一次指針定位的時間開銷。因爲對象的訪問在Java中很是頻繁,所以這類開銷聚沙成塔後也是很是可觀的執行成本。HotSpot 中採用的就是這種方式。

實戰:OutOfMemoryError異常

內存異常是咱們工做當中常常會遇到問題,但若是僅僅會經過加大內存參數來解決問題顯然是不夠的,應該經過必定的手段定位問題,究竟是由於參數問題,仍是程序問題(無限建立,內存泄露)。定位問題後才能採起合適的解決方案,而不是一內存溢出就查找相關參數加大。

概念

內存泄露:代碼中的某個對象本應該被虛擬機回收,但由於擁有GCRoot引用而沒有被回收。

內存溢出:虛擬機因爲堆中擁有太多不可回收對象沒有回收,致使沒法繼續建立新對象。

在分析問題以前先給你們講一講排查內存溢出問題的方法,內存溢出時JVM虛擬機會退出,那麼咱們怎麼知道JVM運行時的各類信息呢,Dump機制會幫助咱們,能夠經過加上VM參數-XX:+HeapDumpOnOutOfMemoryError讓虛擬機在出現內存溢出異常時生成dump文件,而後經過外部工具(VisualVM)來具體分析異常的緣由。

除了程序計數器外,Java虛擬機的其餘運行時區域都有可能發生OutOfMemoryError的異常,下面分別給出驗證:

Java堆溢出

Java堆用來存儲對象,所以只要不斷建立對象,並保證 GC Roots 到對象之間有可達路徑來避免垃圾回收機制清楚這些對象,那麼當對象數量達到最大堆容量時就會產生 OOM。

/** * java堆內存溢出測試 * VM Args: -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()); } }}

運行結果:

java.lang.OutOfMemoryError: Java heap spaceDumping heap to java_pid7164.hprof … Heap dump file created [27880921 bytes in 0.193 secs]Exception in thread 「main」 java.lang.OutOfMemoryError: Java heap spaceat java.util.Arrays.copyOf(Arrays.java:2245)at java.util.Arrays.copyOf(Arrays.java:2219)at java.util.ArrayList.grow(ArrayList.java:242)at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)at java.util.ArrayList.add(ArrayList.java:440)at com.jvm.oom.HeapOOM.main(HeapOOM.java:17)

堆內存 OOM 是常常會出現的問題,異常信息會進一步提示 Java heap space

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

在 HotSpot 虛擬機中不區分虛擬機棧和本地方法棧,棧容量只由 -Xss 參數設定。關於虛擬機棧和本地方法棧,在 Java 虛擬機規範中描述了兩種異常:

  • 若是線程請求的棧深度大於虛擬機所容許的最大深度,將拋出 StackOverflowError 異常。

  • 若是虛擬機在擴展棧時沒法申請到足夠的內存空間,則拋出 OutOfMemoryError 異常。

/** * 虛擬機棧和本地方法棧內存溢出測試,拋出stackoverflow exception * VM ARGS: -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; }
}
}

運行結果:

stack length = 11420 Exception in thread 「main」 java.lang.StackOverflowError at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12) at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13) at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)

以上代碼在單線程環境下,不管是因爲棧幀太大仍是虛擬機棧容量過小,當內存沒法分配時,拋出的都是 StackOverflowError 異常。

若是測試環境是多線程環境,經過不斷創建線程的方式能夠產生內存溢出異常,代碼以下所示。可是這樣產生的 OOM 與棧空間是否足夠大不存在任何聯繫,在這種狀況下,爲每一個線程的棧分配的內存足夠大,反而越容易產生OOM 異常。這點不難理解,每一個線程分配到的棧容量越大,能夠創建的線程數就變少,創建多線程時就越容易把剩下的內存耗盡。這點在開發多線程的應用時要特別注意。若是創建過多線程致使內存溢出,在不能減小線程數或更換64位虛擬機的狀況下,只能經過減小最大堆和減小棧容量來換取更多的線程。

/** * JVM 虛擬機棧內存溢出測試, 注意在windows平臺運行時可能會致使操做系統假死 * VM Args: -Xss2M -XX:+HeapDumpOnOutOfMemoryError */
public class JVMStackOOM {
private void dontStop() {while (true) {} }
public void stackLeakByThread() {while (true) { Thread thread = new Thread(new Runnable() {
@Overridepublic void run() { dontStop(); } }); thread.start(); } }
public static void main(String[] args) { JVMStackOOM oom = new JVMStackOOM(); oom.stackLeakByThread(); }}

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

方法區用於存放Class的相關信息,對這個區域的測試,基本思路是運行時產生大量的類去填滿方法區,直到溢出。使用CGLib實現。

方法區溢出也是一種常見的內存溢出異常,在常常生成大量Class的應用中,須要特別注意類的回收狀況,這類場景除了使用了CGLib字節碼加強和動態語言外,常見的還有JSP文件的應用(JSP第一次運行時要編譯爲Java類)、基於OSGI的應用等。

/** * 測試JVM方法區內存溢出 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M */public class MethodAreaOOM {
public static void main(String[] args) {while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {return proxy.invokeSuper(obj, args); } }); enhancer.create(); } }
static class OOMObject{}}

本機直接內存溢出

DirectMemory 容量可經過 -XX:MaxDirectMemorySize 指定,如不指定,則默認與Java堆最大值同樣。測試代碼使用了 Unsafe 實例進行內存分配。

由 DirectMemory 致使的內存溢出,一個明顯的特徵是在Heap Dump 文件中不會看見明顯的異常,若是發現 OOM 以後 Dump 文件很小,而程序直接或間接使用了NIO,那就能夠考慮檢查一下是否是這方面的緣由。

/** * 測試本地直接內存溢出 * VM Args: -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); } }}

本章小結

經過本章的學習,咱們明白了虛擬機中的內存是如何劃分的,哪部分區域、什麼樣的代碼和操做可能致使內存溢出異常。雖然Java有垃圾收集機制,但內存溢出異常離咱們仍然並不遙遠,本章只是講解了各個區域出現內存溢出異常的緣由。

原文連接:

https://thinkwon.blog.csdn.net/article/details/103827387

本文分享自微信公衆號 - 源代碼社區(ydmsq666)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索