Java虛擬機器—物件的記憶體佈局和訪問定位

前言:

Java是一門面向「物件」的語言,學Java就要學會面向「物件」程式設計。物件的建立在語言層面很簡單,new一個即可,但是在JVM實現層面呢,是怎樣實現的呢?讓我們來研究下Java虛擬機器中物件的建立以及記憶體佈局和訪問定位。所以,本文的內容如下:

物件的建立

物件的記憶體佈局

物件的訪問定位

1.物件的建立

在Java中,物件是很重要的概念,宏觀上來看,物件的建立在Java語言層面上通常用new關鍵詞即可完成,建立完即可直接使用。而在微觀上,物件的建立又包括哪些過程?

在Java虛擬機器層面,物件的建立主要分為2個過程:

物件記憶體的分配

物件初始化

記憶體分配完成後在JVM層面上,物件已經建立成功了,但是僅僅分配了記憶體的物件並不能被我們使用,還需要一步初始化的過程。

1.1物件記憶體的分配

JVM透過指令來建立新物件,物件分為普通物件和陣列物件,

普通物件和陣列物件的建立指令是不同的。

建立類例項的指令:new

建立陣列的指令:newarray,anewarray,multianewarray

舉例:在java語法層面,int[] array = new int[]和List list = new ArrayList()都是透過new關鍵字的,但是在JVM裡卻是透過不同的指令,前者是建立一個數組物件,用的是newarray指令,而後者用的是new指令

以new一個String物件為例,看下其位元組碼指令:

public static String testString(){ String s = new String(“物件測試:testString”); return s;}對應的位元組碼指令如下:————————————————Code: stack=3, locals=1, args_size=0 0: new #11 // class java/lang/String 3: dup //dup指令用於複製運算元棧頂的值,再將其入棧(結果是棧頂有2個相同值) 4: ldc #12 // String 物件測試:testString(ldc指令用於將其壓入棧頂) 6: invokespecial #13 // Method java/lang/String。“”:(Ljava/lang/String;)V 9: astore_0 10: aload_0 11: areturn

再看一下new一個ArrayList時的情形:

public static List testList(){ List list = new ArrayList(); return list;}對應的位元組碼指令如下:————————————————Code: stack=2, locals=1, args_size=0 0: new #14 // class java/util/ArrayList 3: dup 4: invokespecial #15 // Method java/util/ArrayList。“”:()V 7: astore_0 8: aload_0 9: areturn

我們以new指令為例,當虛擬機器遇到new指令時,首先回去檢查此條指令在常量池中能否定位到一個類的符號引用,並且檢查此符號引用所代表的類是否被載入、解析和初始化過。如果沒有則必須先執行相應的類載入過程。在類載入檢查通過後,虛擬機器將為新生

物件在Java堆中分配記憶體。分配記憶體的實現方式有兩種:

指標碰撞(Bump the Pointer)

空閒列表(Free List)

具體使用哪種方式取決於Java堆中記憶體的排列是否規整。假設Java堆中記憶體是絕對規整的,即有大片連續的記憶體,而不是散落不均的記憶體空間,已使用的記憶體和未使用的記憶體空間相互挨著各佔一邊。那

僅僅將指標向未使用空間那邊挪動一段和物件大小相等的距離即可,這種方式就叫做——指標碰撞。

如果堆記憶體是不規整的,虛擬機器就無法透過指標碰撞的方式來劃分記憶體了,此時虛擬機器需要維護一個列表用以記錄整個記憶體空間上哪些可用,哪些不可用。此時,

在分配記憶體時,JVM需要找到一塊足夠大的記憶體空間分配給新new出來的物件,並且更新列表上的記錄。這種方式就叫做——空閒列表

Java堆中記憶體的排列是否規整取決於堆中垃圾收集器,如果JVM中的垃圾收集器帶有空間壓縮整理功能,則記憶體規整;否則記憶體不規整。

在使用Serial、ParNew等等帶有Compact過程(壓縮整理)的垃圾收集器時,系統採用的分配演算法是指標碰撞;而使用CMS這種基於Mark-Sweep演算法的收集器時,通常採用空閒列表。

記憶體分配完成後,虛擬機器還要將分配到的記憶體空間都初始化為零值(物件頭除外),這一步驟保證了物件的例項欄位在Java程式碼中可以不賦初始值即可使用,如:byte、short、long轉化為物件後初始值為0,Boolean初始值為false

Java虛擬機器—物件的記憶體佈局和訪問定位

接下來,虛擬機器要對物件進行必要的設定,如:此物件是哪個類的例項、如何才能找到類的元資料資訊、物件雜湊碼、物件的GC分帶年齡等資訊。這些資訊存在物件頭(Object Header)中。物件頭的內容將在下面↓

物件的記憶體佈局中介紹。

根據虛擬機器當前執行狀態的不同、是否有偏向鎖等,物件頭的內容組成會有所不同。在設定好物件頭以後,從JVM的角度看,物件的建立就正式完成了。

1.2物件初始化

物件建立完成後,還需要初始化,才能正式被我們使用,一般來說JVM中new指令之後會緊跟著有invokespecial指令,該指令用於指令物件所屬類的方法。init方法完成後(物件初始化完成),物件才正式可用,至此物件建立的完整過程就結束了。

2.物件的記憶體佈局

不同的虛擬機器可能有不同的實現,這裡我們以HotSpot虛擬機器為例講解物件的記憶體佈局。在HotSpot虛擬機器中,物件在記憶體區域中分為3塊內容:

物件頭(Header)

例項資料(Instance Data)

對齊填充(Padding)

2.1物件頭

HotSpot中物件頭分為兩部分資訊:

物件自身的執行時資料

物件的型別指標

物件頭的第一部分用於儲存自身的執行時資料包括:

雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳

等。執行時資料部分的長度在32位和64位虛擬機器中分別為32位和64位,官方稱之為“

Mark Word

”,由於執行時資料這部分較多,可能會不夠存放,所以此Mark Word部分被設計為非固定結構,即其結構可以隨著物件狀態而改變以便在較小空間內儘量複用自己的空間。

物件頭的第二部分是物件的型別指標。

型別指標,即物件指向它的類元資料的指標,透過這個指標,虛擬機器可以確定物件是哪個類的例項。

但是不同虛擬機器實現不同,在有的虛擬機器實現上,並不需要透過這個指標查詢元資料資訊,所以也就沒有型別指標。另外,如果物件是個陣列,在物件頭部還需要一塊用於記錄陣列長度的資料

例如,在32位的HotSpot虛擬機器中,物件非鎖定狀態下,Mark Word的32位空間是這樣分配的:25bit用於儲存物件的雜湊碼HashCode、4bit儲存物件分代年齡、2bit儲存物件鎖標誌位、1bit固定為0。而在其他狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下,又是另一種佈局,如下圖所示:

Java虛擬機器—物件的記憶體佈局和訪問定位

2.2例項資料

例項資料部分是物件真正儲存有效資訊的區域

,物件可能會包含各種型別的欄位無論是從父類繼承的還是在子類中定義的,這些都需要在此記錄。這部分的儲存順序會受到虛擬機器分配策略引數和欄位在Java原始碼中定義順序的共同影響。

2.3對齊填充

這部分並不是必然存在的,也沒有特別的含義,僅僅起到佔位符的作用。由於HotSpot VM的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,即物件大小必須是8位元組的整數倍,如果不夠則需要透過對其填充來補全

3.物件的訪問定位

物件建立後就是為了被使用的,為了使用物件,我們需要知道物件在記憶體中的地址,即Java堆中的地址。我們透過運算元棧中的reference(引用型別)資料來找到物件的位置。

但是Java虛擬機器規範只規定reference是一個指向物件的引用,並沒有明確規定這個ref引用是直接指向物件還是指向物件的指標,所以物件訪問方式還要取決於虛擬機器的實現。目前,主流的實現方式有2種:

控制代碼訪問

直接指標訪問

1.控制代碼訪問

使用控制代碼訪問物件

,則

reference儲存的引用指向Java堆中的控制代碼池,控制代碼池中存放了指向物件例項資料和型別資料的指標。在這種情況下,物件的例項資料和型別資料是分開存放的,例項資料和控制代碼池同樣存放於Java堆中的例項池中(和控制代碼池同處於Java堆中,只不過區域不同);型別資料則存放於方法區中。

Java虛擬機器—物件的記憶體佈局和訪問定位

2.直接指標訪問

直接指標訪問,則reference儲存的引用就是直接指向Java堆中的物件例項資料,此種情況下,物件的型別資料還是儲存於方法區(在例項資料中,儲存了指向物件型別資料的指標)

Java虛擬機器—物件的記憶體佈局和訪問定位

這兩種訪問方式各有優劣,使用控制代碼訪問的優勢在於reference中儲存的是控制代碼地址,在物件被移動(垃圾收集時經常會發生)時只需改變控制代碼池中例項資料指標即可,reference本身無需修改。劣勢在於,相對應直接指標訪問,多了一道訪問流程,故速度較慢。

直接指標訪問的優勢在於reference中儲存的引用直接就指向Java堆中的物件,所以訪問效率高,由於Java中對物件的訪問是非常頻繁的,所以累計起來節省了不少時間(相對與控制代碼訪問)。劣勢在於,如果物件一旦被修改,則reference也需要隨之修改。

學完本篇文章,我們對Java堆、方法區的概念和物件的「建立」過程有了更深入的理解。在接下來的文章裡,我們將要探討下物件的「死亡」過程,以及GC垃圾收集過程。喜歡的盆友,請點個讚唄