壓縮指針:64位系統下,Java虛擬機是如何“偷”回4字節內存的?
Java對象:在內存中的真面目
在Java中,通過new關鍵字創建一個Java類的實例對象時,該對象會通過碰撞指針方式存儲在內存的堆中,并被分配一個內存地址。在Java虛擬機中,一個Java對象由對象頭(Object Header)、實例數據(Instance Data)和對齊填充(Padding)三部分構成。

對象頭
對象頭由兩個字(計算機術語,表示計算機處理數據的最小單位)組成。如果對象是一個Java數組,對象頭中還必須包含一部分用于記錄數組長度的數據,因為雖然Java虛擬機可以通過Java對象的元數據信息確定Java對象的大小,但無法從數組的元數據中確定數組的大小。
對象頭的兩個字分別是Mark Word和Klass Pointer。
1)Mark Word:即標記字段,用于存儲對象自身的運行時數據,如哈希碼(HashCode)、垃圾回收分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等。
2)Klass Pointer:即類型指針,是對象指向它的類元數據的指針,Java虛擬機通過這個指針來確定這個對象是哪個Java類的實例。
實例數據
實例數據部分存儲對象的屬性字段信息。如果對象沒有屬性字段,那么這部分就不會有數據。字段類型的不同會占用不同的字節,例如,boolean類型占1個字節,int類型占4個字節等。
對齊填充
對齊填充是為了滿足Java虛擬機堆中對象起始地址需要對齊至8的倍數的要求。如果一個對象未使用到8N個字節,則需要進行填充,以補齊對象頭和實例數據占用內存后的剩余空間。
字段內存對齊的目的之一是確保字段只出現在同一處理器的緩存行中。如果字段未對齊,可能會出現跨緩存行的字段,即該字段的讀取可能需要替換兩個緩存行,而該字段的存儲也可能同時污染兩個緩存行,這對程序執行效率都是不利的。實際上,對齊填充的最終目標是為了實現計算機的高效尋址。

壓縮指針
在64位Java虛擬機中,對象頭部的Mark Word和Klass Pointer,分別占據64位,因此每個Java對象的內存額外開銷就是16字節。以Integer類為例,它僅有一個int類型的私有字段,占用4字節,因此,每個Integer對象的內存開銷至少增加400%。這也是Java引入基本數據類型(Primitive Data Type)的原因之一。
在64位系統中,普通的對象引用通常需要64位(8字節)的空間。然而,對于許多應用程序,這種大尺寸的引用是不必要的,因為它們的堆內存使用量遠小于64位地址空間的上限(即18億GB)。因此,64位Java虛擬機引入了壓縮指針(Compressed Pointer)技術,將Java對象指針壓縮為32位。這樣,對象頭部中的Klass Pointer也被壓縮為32位,從而將對象頭部的大小從16字節減小到12字節。

工作原理
Java虛擬機假設所有對象的大小都是8字節的倍數(不足將對齊填充),因此對象的實際地址可以表示為基地址加上一個偏移量,而這個偏移量是8的倍數。因此,Java虛擬機只需要存儲這個偏移量,而不是完整的64位地址。由于偏移量是8的倍數,所以它的最后三位總是0,Java虛擬機可以將這個偏移量右移三位,從而將其壓縮到32位的空間。
下面是8和它的倍數對應的二進制關系。
8 = 1000
16 = 10000
24 = 11000
32 = 100000
40 = 101000
48 = 110000
56 = 111000
64 = 1000000
72 = 1001000
可以看到,在二進制下,8和它的倍數的后三位都是0。因為后三位都是0,所以可以在拿到地址后舍棄后三位,讀取的時候加上后三位。這個過程可以用以下的偽代碼表示。
int offset = getObjectOffset(); // 獲取對象的32位偏移量
offset = offset << 3; // 將偏移量左移三位
long address = HEAP_BASE + offset; // HEAP_BASE是堆基地址,加上偏移量就可以得到對象的實際內存地址。
需要注意的是,壓縮指針技術只適用于堆內存大小小于32GB的情況。如果堆內存大小超過32GB,那么32位的偏移量將不足以表示所有可能的對象位置,因此必須使用完整的64位引用。
未完待續
很高興與你相遇!如果你喜歡本文內容,記得關注哦
本文來自博客園,作者:poemyang,轉載請注明原文鏈接:http://www.rzrgm.cn/poemyang/p/19170159
浙公網安備 33010602011771號