在做JVM記憶體調優的時候,我們免不了需要去計算物件的大小。計算物件大小又要考慮是普通物件還是陣列物件,因為普通物件與陣列物件的物件頭存在些許差異。而且自JDK6以後,為了節省記憶體、提高執行效率,又引入了新的技術:指標壓縮。更加劇了計算物件大小的難度。
這篇文章就來深入分析如何計算物件大小,確保計算的結果與實際情況不差一個位元組。
物件結構
物件結構想必深入學習過JVM的人都知道,它分為三大部分:物件頭、例項資料、對齊填充。其中物件頭又分為三個部分:Mark Word、型別指標、陣列長度。其實物件頭還有第四部分,這是目前你看到的書、視訊都沒有提到的,物件頭也有對齊填充部分,這個部分也不是一定會有,只有陣列物件在未開啟指標壓縮的情況下才會出現。是不是一臉懵,那就繼續往後看。
指標壓縮
看到這四個字是不是一堆的問號:這是什麼?這怎麼實現的?為什麼說它節省了記憶體?……我們們就來搞清楚這些個問題。
我們們先達成觀念上的一致:所有的物件都是以8位元組對齊的。現在我有3個物件:test1(16位元組)、test2(32位元組)、test3(24位元組),為了便於說明,假如這三個物件中間沒有其他物件,那他們的記憶體地址是:
test1 = 0x0000 0 000(0位元組 ~ 16位元組)
test2 = 0x0001 0 000(16位元組 ~ 48位元組)
test3 = 0x0011 0 000(48位元組 ~ 72位元組)
大家發現規律了嗎,所有物件指標的後三位都是0,這就是指標壓縮的實現原理。開啟指標壓縮後,JVM會將物件指標的後三位給截去,如果test2 = 0x10000,在開啟指標後就變成了test2=0x10,在使用的時候將後三位的0補回去,即test2=0x10 000。
因為開啟了指標壓縮後,物件指標就變成了4位元組(32位),加上補3位,共35位。即開啟指標壓縮後一個物件指標能表示的最大堆空間是2的35次方,即32G。
那讀者可以想一想?如果我想擴充一個oop能表示的堆空間大小該怎麼做呢?
下面我們們來看四種情況(普通物件-關閉指標壓縮、普通物件-開啟指標壓縮、陣列物件-關閉指標壓縮、陣列物件-開啟指標壓縮)下的物件大小是如何計算出來的。建議讀者寫相似程式碼測試一下,這樣才能理解得更深刻。
測試程式碼:
package com.qimingnan.adjust;
import org.openjdk.jol.info.ClassLayout;
public class Test1 {
int a = 10;
int b = 20;
static int[] arr = {0, 1, 2};
public static void main(String[] args) {
Test1 test1 = new Test1();
System.out.printf(ClassLayout.parseInstance(test1).toPrintable());
System.out.printf(ClassLayout.parseInstance(arr).toPrintable());
}
}
普通物件
一、未開啟指標壓縮
24B = 8B(Mark Word)+ 8B(KClass Pointer)+ 4B + 4B
二、開啟指標壓縮
24B = 8B(Mark Word)+ 4B(KClass Pointer)+ 4B(int a)+ 4B(int b)+ 4B(Padding)
陣列物件
一、未開啟指標壓縮
40B = 8B(Mark Word)+ 8B(KClass Pointer)+ 4B(陣列長度)+ 4B(頭部Padding)+ 12B(3個int)+ 4B(物件Padding)
二、開啟指標壓縮
32B = 8B(Mark Word)+ 4B(KClass Pointer)+ 4B(陣列長度)+ 12B(3個int)+ 4B(物件Padding)
如何計算物件大小大家學會了嗎?
介紹一款工具,jol,大家在做測試的時候可以藉助這個工具
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
這篇文章有些地方可能不太好理解,讀者有問題的話,留言提問吧。我都會一一回復。
後續我將為大家新開《手寫JVM》的系列專欄,如果你對此感興趣的話,那麼就關注我吧。