瞭解Java物件,簡單聊聊JVM調優分析

@milo發表於2020-12-15

瞭解Java物件,簡單聊聊JVM調優分析

image-20201214234112356

1、oop模型

​ Klass模型請看jvm底層之類載入,它是Java類的元資訊在JVM中的存在形式。而oop模型是Java物件在JVM中的存在形式,在 Java 程式執行的過程中,每建立一個新的物件,在 JVM 內部就會相應地建立一個對應型別的 oop(普通物件指標) 物件。各種 oop 類的共同基類為 oopDesc 類。

​ 在 JVM 內部,一個 Java 物件在記憶體中的佈局可以連續分成兩部分:物件頭(instanceOopDesc) 和例項資料(成員變數)。

​ sychronized的底層實現就是MarkOopDesc,InstanceOopDesc對應InstanceKlass,arrayOopDesc對應ArrayKlass,typeArrayOopDesc對應TypeArrayKlass,objArrayOopDesc對應ObjArrayKlass。

image-20201213144144172

2、物件的記憶體佈局

​ 下面圖是大家眼中的記憶體佈局圖,但是還有另外一種情況,具體細節後面談。

img

2.1、物件頭

2.1.1、Mark Word

​ 就是標記字,用於儲存物件自身的執行時資料,如雜湊碼(hashCode),GC分代年齡,執行緒鎖狀態標誌,執行緒持有的鎖,偏向執行緒ID,偏向時間戳等;

​ 32bit機下佔4B,64bit機下佔8B

2.1.2、型別指標

Klass pointer :物件所屬的類的元資訊的例項指標(instanceKlass在方法區的地址)即instanceKlass在方法區的地址,即物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。

​ 大小與指標壓縮有關係:開啟的話佔4B,關閉的話8B

2.1.3、陣列長度

​ 如果這個物件不是陣列,佔0B,如果這個物件是陣列,佔4B。

​ 因此可以推出一個陣列最多隻有2^32-1個元素

2.2、例項資料

​ 儲存物件真正有效資訊,如果是基本型別,直接儲存下來,如果是引用型別,儲存的是指向堆區中物件的引用指標類的。

非靜態屬性,生成物件時就是例項資料

​ 8種基本型別:4種整形byte、short、int、long;2種浮點型別float、double;1種Unicode編碼的字元單元的字元型char;1中Boolean型別float;

​ 而引用型別,開啟指標壓縮佔4個位元組,未開啟指標壓縮佔8個位元組

型別佔用位元組B(byte)佔用位數(bit)
byte18
short216
int432
long864
float432
double864
char216
boolean18

2.3、對齊填充

​ Java中所有的物件大小都是8位元組對齊 說白就是8的整數倍,

​ Hotspot的自動記憶體管理系統,要求物件的起始地址必須是8位元組的整數倍,換句話說,物件的大小必須是8位元組的整數倍,而物件頭部分正好是8位元組的整數倍(1倍或2倍),那麼如果物件大小不是8位元組整數倍怎麼辦,就用對齊填充來補充到最近的8位元組整數倍大小。

​ 比如一個物件佔30B + JVM底層會補2B(對齊填充),湊成32位元組,達到8位元組對齊

3、計算物件大小

​ 用jol-core包或者HSDB都可以看,區別是HSDB只能看基於普通類生成物件的大小,java中的陣列因為是執行時生成的,故它的大小隻有執行時才能知曉。

3.1、空物件

​ 沒有例項屬性的物件

public class CountEmptyObjectSize {

    public static void main(String[] args) {
        CountEmptyObjectSize obj = new CountEmptyObjectSize();

        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

image-20201213161234300

image-20201213161157760

​ 結論:開啟和關閉指標壓縮大小都是相通的16B

​ 開啟:16B = 8B(Mark word) + 4B(型別指標) + 0B(陣列長度) + 0B(例項資料) + 4B(對齊填充)

​ 關閉:16B = 8B(Mark word) + 8B(型別指標) + 0B(陣列長度) + 0B(例項資料) + 0B(對齊填充)

3.2、普通物件

public class CountObjectSize {

    int a = 10;
    int b = 20;

    public static void main(String[] args) {
        CountObjectSize object = new CountObjectSize();

        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

image-20201213162050858

image-20201213162021471

​ 結論:開啟和關閉指標壓縮大小都是相通的16B

​ 開啟:24B = 8B(Mark word) + 4B(型別指標) + 0B(陣列長度) + 8B(例項資料) + 4B(對齊填充)

​ 關閉:24B = 8B(Mark word) + 8B(型別指標) + 0B(陣列長度) + 8B(例項資料) + 0B(對齊填充)

3.3、陣列物件

public class CountSimpleObjectSize {

    static int[] arr = {0, 1, 2};

    public static void main(String[] args) {
        CountSimpleObjectSize test1 = new CountSimpleObjectSize();

        System.out.printf(ClassLayout.parseInstance(arr).toPrintable());
    }
}

image-20201213162321599

image-20201213162425385

​ 結論:開啟和關閉指標壓縮大小都是相通的16B

​ 開啟:32B = 8B(Mark word) + 4B(型別指標) + 4B(陣列長度) + 12B(例項資料) + 4B(對齊填充)

​ 關閉:40B = 8B(Mark word) + 8B(型別指標) + 4B(陣列長度) + 4B(對齊填充) + 12B(例項資料) + 4B(對齊填充)

​ 可以看到上面有兩段對齊填充,第一段是物件頭的,第二段是物件的; 即陣列物件,在關閉指標壓縮的情況下,物件頭也會進行對齊填充(8位元組對齊),這樣的話,物件的記憶體模型圖改為如下

img

其實不止陣列物件,在關閉指標壓縮的情況下會出現兩端填充

開啟指標壓縮,沒有例項資料情況下,陣列物件大小是16,關閉指標壓縮,陣列物件大小膨脹到24,陣列長度由4個位元組變為8個位元組,Pedding對齊4個位元組

4、指標壓縮

​ jdk6以後引入的技術,預設是開啟的,可以調優:-XX:+/-UseCompressedOops指標壓縮的目的就是節省記憶體,定址更高效。

​ 可以看下:https://juejin.cn/post/6844903768077647880

​ 對於32位機器,程式能使用的最大記憶體是4G。如果程式需要使用更多的記憶體,需要使用64位機器。

​ 對於Java程式,在oop只有32位時,只能引用4G記憶體。因此,如果需要使用更大的堆記憶體,需要部署64位JVM。這樣,oop為64位,可引用的堆記憶體就更大了。

​ 注:oop(ordinary object pointer),即普通物件指標,是JVM中用於代表引用物件的控制程式碼。

​ 在堆中,32位的物件引用佔4個位元組,而64位的物件引用佔8個位元組。也就是說,64位的物件引用大小是32位的2倍。

4.1、指標壓縮的實現原理

​ java中所有的物件都是8位元組對齊的。所以8位元組對齊有一個規律那就是所有物件的指標後三位永遠是0

舉例

比如三個物件:
test1 = 16B
test2 = 24B
test3 = 32B

地址儲存(從0地址開始順序儲存)
test1 = 0 000
test2 = 16(十進位制)10 000
test3 = 40(十進位制)101 000

儲存的時候,後三位0抹除(儲存時指從暫存器讀出)
test1 = 0
test2 = 10
test3 = 101

使用的時候,後三位補0(使用時指存入暫存器)
test1 = 0 000
test2 = 10 000
test3 = 101 000
public class CompressedTest {
    public static void main(String[] args) {
        CompressedTest test = new CompressedTest();
        while(true);
    }
}

不開啟指標壓縮如下圖: _klass

image-20201213171912907

開啟指標壓縮如下圖: _compressed_klass

image-20201213172806128

對應處理方式在jdk的\openjdk\hotspot\src\share\vm\oops\oop.inline.hpp

class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;  //不開啟
    narrowKlass _compressed_klass; //開啟
  } _metadata
...

4.2、開啟指標壓縮的情況下,一個oop(物件指標)表示的最大堆空間是多少

​ 我們知道開啟指標壓縮的情況下,型別指標佔4位元組,也就是32位,JVM儲存時會抹掉後面的3位,也就是可以儲存32 + 3 = 35位,最大記憶體空間也就是2的35次方,32G。

​ 32位機子,一共有2^32個狀態,一個狀態是一個記憶體地址,那麼32位機子能表示最大記憶體是4GB
​ 64位機子,實際表示的是35位

4.3、oop(物件指標)如何擴容

​ 前面說到JVM採用8位元組對齊,會抹掉後面的3位,如果我們讓它採用16位元組對齊,那麼是不是可以抹掉最後面的4位,oop(物件指標)所能表示的堆空間則為2的32 + 4次方即64G

4.4、為什麼沒這樣做(16位元組對齊)

​ 1、增加了GC開銷,坦白講cpu的效能有限,現在的GC演算法處理32G的堆已經是極限了,所以還是cpu的瓶頸。64位物件引用需要佔用更多的堆空間,留給其他資料的空間將會減少,(說白了就是費空間)從而加快了GC的發生,更頻繁的進行GC。

​ 2、降低CPU快取命中率,64位物件引用增大了,CPU能快取的oop將會更少,從而降低了CPU快取的效率。

5、聊聊JVM調優分析

5.1、調優階段

  • 在專案部署到線上之前,基於可能的併發量進行預估調優
  • 專案上線初期,在專案執行過程中,部署監控收集效能資料,平時分析日誌進行調優
  • 線上出現OOM,、頻繁full gc,進行問題排查,做徹底的調優。

5.2、為什麼要調優

  • 防止出現OOM
  • 解決OOM
  • 減少full gc出現的頻率

5.3、到底調什麼

  • 方法區
  • 虛擬機器棧
  • 堆區
  • 熱點程式碼緩衝區

6、案例:億級流量系統調優分析

這裡以億級流量秒殺電商系統為例:

1、如果每個使用者平均訪問20個商品詳情頁,那訪客數約等於500w(一億 / 20)

2、如果按轉化率10%來算,那日均訂單約等於50w(500w * 10%)

3、如果30%的訂單是在秒殺前兩分鐘完成的,那麼每秒產生1200筆訂單(50w * 30% / 120s)

4、訂單支付又涉及到發起支付流程、物流、優惠券、推薦、積分等環節,導致產生大量物件,這裡我們假設整個支付流程生成的物件約等於20K,那每秒在Eden區生成的物件約等於20M(1200筆 * 20K)

5、在生產環境中,訂單模組還涉及到百萬商家查詢訂單、改價、包郵、發貨等其他操作,又會產生大量物件,我們放大10倍,即每秒在Eden區生成的物件約等於200M(其實這裡就是在大併發時刻可以考慮服務降級的地方,架構其實就是取捨)

這裡的假設資料都是大部分電商系統的通用概率,是有一定代表性的。

我機器記憶體32G

堆區:最小是實體記憶體的1/64,最大是實體記憶體的1/4

image-20201213144336044

假設一個操作需要3秒完成,就有600M進入Eden區,

所以差不多每2700 / 200 = 14秒發生一次young gc,

這600M物件還在用,無法被回收,from to區放不下, 此時會觸發空間擔保直接進入老年代(5400M),所以大概(9*14 = 126)2分鐘就會發生一次full gc。

所以將這些物件儘量在young gc階段被回收掉,少觸發full gc。當然加機器也是一種辦法。

後期再總結具體調優方式吧。。。

相關文章