一個Java對象到底佔用多大記憶體?

  • 2019 年 11 月 20 日
  • 筆記

最近在調研MAT和VisualVM源碼實現,遇到一個可疑問題,兩者計算出來的對象大小不一致,才有了這樣疑惑。

一個Java對象到底佔用多大記憶體?

為了復現這個問題,準備了4個最簡單類:

class AAAAA {}  class BBBBB {    int a = 1;}  class CCCCC {    long a = 1L;}  class DDDDD {    String s = "hello";}

當然了,再來個主函數:

final List<AAAAA> aaa = new ArrayList<>(100000);final List<BBBBB> bbb = new ArrayList<>(100000);final List<CCCCC> ccc = new ArrayList<>(100000);final List<DDDDD> ddd = new ArrayList<>(100000);for (int i = 0; i < 100000; i++) {    aaa.add(new AAAAA());    bbb.add(new BBBBB());    ccc.add(new CCCCC());    ddd.add(new DDDDD());}

本地的執行環境是64位的JDK8,且使用默認的啟動參數,運行之後通過 jmap-dump命令生成dump文件,分別用MAT和VisualVM打開。

MAT

通過MAT打開,可以發現ABD對象大小都是16位元組,而C對象大小為24位元組

VisualVM

通過Vis打開,可以發現其顯示的大小和MAT有蠻大的差別。

好奇怪,哪個是對的?

要回答這個問題,首先得清楚的知道JVM中對象的記憶體布局。

在Hotspot中,一個對象包含3個部分:對象頭、實例數據和對齊填充。

對象頭

這裡不講對象頭是個什麼東西,感興趣的同學可以看我的其它文章。 對象頭的大小一般和系統的位數有關,也和啟動參數 UseCompressedOops有關:

  • 32位系統,佔用 8 位元組
  • 64位系統,開啟 UseCompressedOops時,佔用 12 位元組,否則是16位元組

實例數據

原生類型的記憶體佔用情況如下:

  • boolean 1
  • byte 1
  • short 2
  • char 2
  • int 4
  • float 4
  • long 8
  • double 8

引用類型的記憶體佔用和系統位數以及啟動參數 UseCompressedOops有關

  • 32位系統佔4位元組
  • 64位系統,開啟 UseCompressedOops時,佔用4位元組,否則是8位元組

對齊填充

在Hotspot中,為了更加容易的管理記憶體,一般會使用8位元組進行對齊。

意思是每次分配的記憶體大小一定是8的倍數,如果對象頭+實例數據的值不是8的倍數,那麼會重新計算一個較大值,進行分配。

結果

有了對象各部分的記憶體佔用大小,可以很輕鬆的計算出ABCD各對象在64位系統,且開啟 UseCompressedOops參數時的大小。

  • A對象只包含一個對象頭,大小佔12位元組,不是8的倍數,需要4位元組進行填充,一共佔16位元組
  • B對象包含一個對象頭和int類型,12+4=16,正好是8的倍數,不需要填充。
  • C對象包含一個對象頭和long類型,12+8=20,不是8的倍數,需要4個位元組進行填充,佔24位元組
  • D對象包含一個對象頭和引用類型,12+4=16,正好是8的倍數,不需要填充。

可以得出,VisualVM的顯示結果有點問題,主要因為以下兩點:

  • 首先,沒有考慮是否開啟 UseCompressedOops
  • 其次,沒有考慮記憶體對齊填充的情況

感興趣的同學,可以動手實踐一下,這樣可以加深對象記憶體布局的理解。

經過這段時間對MAT和VisualVM的源碼研究,發現MAT的功能不是強大一點點,建議大家以後盡量使用MAT。

-------- END ---------

Tags: