1. 程式人生 > 其它 >【Java學習筆記(八十七)】之HotSpot虛擬機器物件解析,Java記憶體溢位異常解析

【Java學習筆記(八十七)】之HotSpot虛擬機器物件解析,Java記憶體溢位異常解析

技術標籤:Java學習筆記# JVMjvmjava

本文章由公號【開發小鴿】釋出!歡迎關注!!!


老規矩–妹妹鎮樓:

一. 虛擬機器HotSpot物件解析

(一) 物件的建立

1. 檢查類的符號引用

Java 虛擬機器遇到一個位元組碼new指令時,首先檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個類是否已經載入,解析,初始化,如果沒有,則首先需要載入類。

2. 物件分配記憶體

(1) 兩種記憶體分配方式

類載入檢查通過後,虛擬機器就要為新的物件分配記憶體,物件所需的記憶體的大小在類載入完成後就能夠確定。Java虛擬機器為物件分配Java堆中的記憶體空間,若Java堆中的記憶體是絕對規整的,則通過指標劃分已使用的記憶體和未使用的記憶體即可,這種分配方式稱為“指標碰撞”;若Java堆的記憶體不規整,虛擬機器需要維護一個空閒列表,記錄空閒記憶體塊,在分配時在列表中找出合適的記憶體塊,這種分配方式稱為“空閒列表”。

Java堆的記憶體是否規整由所採用的的垃圾收集器是否帶有空間壓縮能力決定,如Serial,ParNew等帶壓縮整理過程的垃圾收集器,Java堆使用的分配演算法是指標碰撞;如CMS這種基於清除演算法的收集器,只能使用“空閒列表”。

(2) 分配記憶體的執行緒安全問題

由於物件的建立操作十分頻繁,可能會有執行緒安全問題。這裡提供兩種解決方法:

一種是對分配記憶體空間的動作進行同步處理;

另一種是將記憶體分配的動作按照執行緒劃分在不同的空間中進行,每個執行緒在Java堆中預先分配一小塊記憶體,稱為本地執行緒分配緩衝(TLAB),執行緒首先從TLAB中取記憶體,當TLAB用完時,再用同步鎖定的方式申請新的記憶體。虛擬機器是否使用TLAB,通過設定 -XX:+/-UseTLAB

引數。


3. 物件初始化

記憶體分配完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值,若有TLAB,TLAB分配是即可完成。初始化操作保證了物件的例項欄位在Java程式碼中可以不賦初始值就直接使用。


4. 物件設定

之後,Java虛擬機器會對物件進行必要的設定,如該物件屬於哪個類,如何找到類的元資料資訊,物件的雜湊碼,物件的GC分代年齡等資訊,這些資訊存放在物件的物件頭中。

5. 建構函式

從虛擬機器的角度,此時的物件已建立,但是對於Java程式來說,還沒有執行建構函式,即Class檔案中的()方法還未執行,所有的欄位為預設零值。執行完()方法後,按照建構函式初始化了物件,才算完成物件的建立。


(二) 物件的記憶體佈局

HotSpot虛擬機器中,物件在堆記憶體中的儲存佈局可以劃分為三部分:物件頭,例項資料,對齊填充。

1. 物件頭

物件頭包含兩類資訊,如下所示:

(1) 物件執行時資料

如雜湊碼,GC分代年齡,鎖狀態標誌等,這部分資料在32位和64位的虛擬機器 中分別為32bit和64bit,官方稱為“Mark Word”。這部分資料的資料結構是動態的,可在極小的空間記憶體儲儘量多的資料。

(2) 型別指標

物件指向它的型別元資料的指標,Java虛擬機器通過這個指標確定該物件是哪個類的例項,但也並不是所有的虛擬機器實現都必須在物件資料上保留型別指標,即查詢物件的元資料資訊不一定要經過該物件本身。

2. 例項資料

物件儲存的有效資訊,即我們在程式碼中定義的各種型別的欄位內容,包括父類繼承的和子類定義的欄位。這些欄位的儲存順序會受到虛擬機器分配策略引數(-XX:FieldsAllocationStyle)和欄位定義順序的影響。順序一般是按照欄位的型別位元組的大小從大到小,相同大小的欄位一起存放,且父類中定義的變數會在子類之前。

3. 對齊填充

就是一個佔位符的作用,由於HotSpot虛擬機器的自動記憶體管理要求物件起始地址必須是8位元組的整數倍,即任何物件的大小是8位元組的倍數,而物件頭已經設計成8位元組的倍數了,如果物件例項資料沒有對齊的話, 就要通過對齊填充來對齊。


(三) 物件的訪問定位

Java程式會通過棧上的reference資料來操作堆上的具體物件,物件的訪問方式是由虛擬機器實現而定的,常用的有兩種:控制代碼和直接指標。

1. 控制代碼

Java堆中劃分出控制代碼池,reference中儲存的是物件的控制代碼地址,而控制代碼中包含了Java堆中物件例項資料和方法區中型別資料各自的地址。使用控制代碼的好處是當物件被移動時,只會改變控制代碼中的例項資料指標,而不會改變reference中的資料。

2. 直接指標

不用在Java堆中分配控制代碼池,在Java物件的記憶體中不僅儲存物件例項資料,還儲存物件型別資料的指標,reference直接儲存物件地址即可。相比於控制代碼,減少了一次指標定位的時間開銷,速度更快,鑑於物件訪問操作十分頻繁,HotSpot使用直接指標更多一些。


二. Java記憶體溢位異常

(一) Java堆溢位

Java堆用於儲存物件例項,如果物件數量過多,且總容量到達了最大堆的容量限制,就會產生記憶體溢位異常。通過設定虛擬機器的引數:

-Xms 表示堆的最小值;
-Xmx表示堆的最大值;

堆的最大值和最小值設定成一樣的話表示堆不可擴充套件;

-XX:+HeapDumpOnOutOfMemoryError 表示在記憶體溢位異常出現時Dump出當前的記憶體堆轉儲快照

當我們獲得了堆記憶體快照後,就可以使用記憶體映像分析工具分析,首先確認是出現了記憶體洩露還是記憶體溢位。記憶體洩露,是由於垃圾回收器無法回收這些記憶體導致的;記憶體溢位,是由於申請記憶體大於現有的記憶體導致的。

對於記憶體洩露,檢視洩露物件到GC Roots的引用鏈,找到洩露物件是通過怎樣的引用路徑,與哪些GC Roots相關聯,可以較精確地定位到這些物件建立的程式碼位置。

對於記憶體溢位,檢查堆引數與機器的記憶體對比,向上調整,再從程式碼上調整,看看有哪些物件生命週期過長,儘量減少程式執行期間的記憶體消耗。

(二) 虛擬機器棧和本地方法棧溢位

HotSpot不區分虛擬機器棧和本地方法棧,棧容量只能由-Xss引數來設定,且不支援動態擴充套件,因此除非在建立執行緒申請記憶體時就無法獲得足夠記憶體丟擲OutOfMemeoryError異常,否則線上程執行時不會因為擴充套件而導致記憶體溢位,只會因為棧容量無法容納新的棧幀導致StackOverflowError。

特殊情況,通過多執行緒的方式,在HotSpot上也可以出現記憶體溢位異常,但是這種異常與棧空間無直接關係,主要取決於作業系統的記憶體使用情況。若每個執行緒所分配的記憶體過大,所有執行緒的記憶體超過了作業系統分配給每個程序的記憶體限制,則會產生記憶體溢位異常。這時候,需要通過減少最大堆和減少棧容量的方法來換取更多的執行緒,這種通過減少記憶體的手段解決記憶體溢位比較少見。


(三) 方法區和執行時和常量池溢位

JDK6以前,常量池在永久代中,通過-XX:permSize-XX:MaxPermSize來限制永久代的大小,即限制常量池的容量。JDK7以後,使用元空間,常量池被移到Java堆中,不會再出現記憶體溢位異常。

String::intern()是一個本地方法,如果字串常量池中已經包含一個等於此String物件的字串,則返回池中這個String物件的引用,否則,會將此String物件包含的字串新增到常量池中。JDK6以前,當首次使用String::intern()方法時,新建的String物件在Java堆中,而呼叫intern()方法後,該物件的引用也會儲存到永久代的常量池中,不在Java堆中,這兩個是完全不同的兩個值。JDK7以後,常量池在Java堆中,該String物件呼叫intern()方法得到的引用與該物件是一致的,都在Java堆中。

對於元空間,HotSpot還是提供了一些防禦措施:
-XX:MaxMetaspaceSize設定元空間最大值,預設是-1表示不限制

-XX:MetaspaceSize指定元空間初始大小,以位元組為單位,達到該值就會觸發垃圾收集進行型別解除安裝,同時調整空間大小。如果釋放了大量空間,則降低該值;如果釋放了少量空間,則適當提高該值。


(四) 直接記憶體溢位

直接記憶體導致的記憶體溢位,特徵是Heap Dump檔案不會有明顯的異常,且檔案大小比較小。如程式直接過著間接地使用了DirectMemory(NIO),就可以檢查直接記憶體了。