Java對象佔用堆內存大小計算

歡迎關注本人公衆號

在這裏插入圖片描述

概述

最近在看hbase源碼,裏面有對象佔用內存大小的計算。正好筆記記錄一下。

一般來說,int佔4個字節,long佔8個字節,等等。但是對象在堆中的存儲不止其包含的字段所佔用的空間,還包括對象頭,對齊填充等信息。接下來就結合hbase源碼分析一下對象在堆中的存儲情況。

原生類型(primitive type)的內存佔用

類型 佔用空間
boolean 在數組中佔1個字節,單獨使用時佔4個字節
byte 1
short 2
char 2
int 4
float 4
long 8
double 8

boolean佔用內存空間說明

《Java虛擬機規範》一書中的描述:“雖然定義了boolean這種數據類型,但是隻對它提供了非常有限的支持。在Java虛擬機中沒有任何供boolean值專用的字節碼指令,Java語言表達式所操作的boolean值,在編譯之後都使用Java虛擬機中的int數據類型來代替,而boolean數組將會被編碼成Java虛擬機的byte數組,每個元素boolean元素佔8位”。這樣我們可以得出boolean類型佔了單獨使用是4個字節,在數組中又是1個字節。

那虛擬機爲什麼要用int來代替boolean呢?爲什麼不用byte或short,這樣不是更節省內存空間嗎。大多數人都會很自然的這樣去想,我同樣也有這個疑問,經過查閱資料發現,使用int的原因是,對於當下32位的處理器(CPU)來說,一次處理數據是32位(這裏不是指的是32/64位系統,而是指CPU硬件層面),具有高效存取的特點。

另外也解決了僞共享問題,boolean類型的數據不會因爲佔用空間小而與其他數據類型的數據一起使用,注意這一點是我個人的一個猜測。歡迎來辯。

對象分佈基本概念

如圖,java對象在內存中佔用的空間分爲3類,

  1. 對象頭(Header);
  2. 實例數據(Instance Data);
  3. 對齊填充(Padding)。

而我們常說的基礎數據類型大小主要是指第二類實例數據。

在這裏插入圖片描述

對象頭

上圖中可以看到,對象頭分爲三個部分:

  1. Mark Word
  2. 指向類的指針
  3. 數組長度(只有數組對象纔有)

Mark Word

Mark Word記錄了對象和鎖有關的信息,當這個對象被synchronized關鍵字當成同步鎖時,圍繞這個鎖的一系列操作都和Mark Word有關。

Mark Word在32位JVM中的長度是32bit,在64位JVM中長度是64bit。

Mark Word在不同的鎖狀態下存儲的內容不同,在32位JVM中是這麼存的:


其中無鎖和偏向鎖的鎖標誌位都是01,只是在前面的1bit區分了這是無鎖狀態還是偏向鎖狀態。

JDK1.6以後的版本在處理同步鎖時存在鎖升級的概念,JVM對於同步鎖的處理是從偏向鎖開始的,隨着競爭越來越激烈,處理方式從偏向鎖升級到輕量級鎖,最終升級到重量級鎖。

JVM一般是這樣使用鎖和Mark Word的:

1,當沒有被當成鎖時,這就是一個普通的對象,Mark Word記錄對象的HashCode,鎖標誌位是01,是否偏向鎖那一位是0。

2,當對象被當做同步鎖並有一個線程A搶到了鎖時,鎖標誌位還是01,但是否偏向鎖那一位改成1,前23bit記錄搶到鎖的線程id,表示進入偏向鎖狀態。

3,當線程A再次試圖來獲得鎖時,JVM發現同步鎖對象的標誌位是01,是否偏向鎖是1,也就是偏向狀態,Mark Word中記錄的線程id就是線程A自己的id,表示線程A已經獲得了這個偏向鎖,可以執行同步鎖的代碼。

4,當線程B試圖獲得這個鎖時,JVM發現同步鎖處於偏向狀態,但是Mark Word中的線程id記錄的不是B,那麼線程B會先用CAS操作試圖獲得鎖,這裏的獲得鎖操作是有可能成功的,因爲線程A一般不會自動釋放偏向鎖。如果搶鎖成功,就把Mark Word裏的線程id改爲線程B的id,代表線程B獲得了這個偏向鎖,可以執行同步鎖代碼。如果搶鎖失敗,則繼續執行步驟5。

5,偏向鎖狀態搶鎖失敗,代表當前鎖有一定的競爭,偏向鎖將升級爲輕量級鎖。JVM會在當前線程的線程棧中開闢一塊單獨的空間,裏面保存指向對象鎖Mark Word的指針,同時在對象鎖Mark Word中保存指向這片空間的指針。上述兩個保存操作都是CAS操作,如果保存成功,代表線程搶到了同步鎖,就把Mark Word中的鎖標誌位改成00,可以執行同步鎖代碼。如果保存失敗,表示搶鎖失敗,競爭太激烈,繼續執行步驟6。

6,輕量級鎖搶鎖失敗,JVM會使用自旋鎖,自旋鎖不是一個鎖狀態,只是代表不斷的重試,嘗試搶鎖。從JDK1.7開始,自旋鎖默認啓用,自旋次數由JVM決定。如果搶鎖成功則執行同步鎖代碼,如果失敗則繼續執行步驟7。

7,自旋鎖重試之後如果搶鎖依然失敗,同步鎖會升級至重量級鎖,鎖標誌位改爲10。在這個狀態下,未搶到鎖的線程都會被阻塞。

指向類的指針

該指針在32位JVM中的長度是32bit,在64位JVM中長度是64bit。

Java對象的類數據保存在方法區(java8以後在元數據區)。

數組長度

只有數組對象保存了這部分數據。

該數據在32位和64位JVM中長度都是32bit。

實例數據

我們在java類中定義的屬性。

對齊填充

HotSpot的對齊方式爲8字節對齊。
8字節填充的含義也就是不足8字節(或8字節的倍數)則將其填充至8字節(或8字節的倍數,這裏指的是大於當前值的最小一個8的倍數)。如:

原始長度 8字節對齊後長度
6 8
7 8
8 8
9 16
10 16

我們來看一下hbase的計算對齊填充後長度的算法:

/**
 * Aligns a number to 8.
 * @param num number to align to 8
 * @return smallest number >= input that is a multiple of 8
 */
public long align(long num) {
  //The 7 comes from that the alignSize is 8 which is the number of bytes
  //stored and sent together
  return  ((num + 7) >> 3) << 3;//將數字加7後抹0(後3位0)
}

這裏採用的是二進制位運算算法,位運算在計算機內是性能最高的算法,所以在閱讀開源軟件時,會大量看到二進制位運算。

8字節填充的長度計算其實很簡單,將數值先轉爲2進制,然後將倒數第四位置爲1,如果本來是1則不變。然後將後三位置爲0即可。

以26爲例,26的二進制表述爲11010,那麼如何才能將倒數第四位置爲1呢?上面算法中加上7,即可以完成進位操作,7是比8小的最大的一個數,二進制是111。然後進位操作以後,如何清除後面的三位置爲0呢?簡單,右移3位在左移3位即可完成。這也就是上述算法的實現原理。

指針壓縮

不壓縮時指針佔8字節。在64位開啓指針壓縮的情況下 -XX:+UseCompressedOops,存放Class指針的空間大小是4字節。

對象頭佔用空間總結

  1. 在32位系統下,存放Class指針的空間大小是4字節,MarkWord是4字節,對象頭爲8字節。
  2. 在64位系統下,存放Class指針的空間大小是8字節,MarkWord是8字節,對象頭爲16字節。
  3. 在64位開啓指針壓縮的情況下 -XX:+UseCompressedOops,存放Class指針的空間大小是4字節,MarkWord是8字節,對象頭爲12字節。
  4. 如果對象是數組,那麼額外增加4個字節。

hbase源碼分析–對象佔用空間計算

這裏先簡單介紹一下下面示例的背景。
以hbase 讀取wal日誌文件輸出爲例,裏面計算了對象佔用堆內存大小情況(裏面的total_size_sum和edit heap size就是指對象佔用空間的就算結果):

{
	"sequence": 15,
	"region": "c06475acdfec83fd5a6bf6d06c796bd1",
	"actions": [
		{
			"qualifier": "HBASE::FLUSH",
			"vlen": 103,
			"row": "\\x00",
			"family": "METAFAMILY",
			"value": "\\x08\\x00\\x12\\x02t2\\x1A c06475acdfec83fd5a6bf6d06c796bd1 \\x0E*\\x06\\x0A\\x010\\x12\\x01023t2,,1574775527767.c06475acdfec83fd5a6bf6d06c796bd1.",
			"timestamp": 1574840582064,
			"total_size_sum": 200
		}
	],
	"table": {
		"name": "dDI=",
		"nameAsString": "t2",
		"namespace": "ZGVmYXVsdA==",
		"namespaceAsString": "default",
		"qualifier": "dDI=",
		"qualifierAsString": "t2",
		"systemTable": false,
		"nameWithNamespaceInclAsString": "default:t2"
	}
}
edit heap size: 240
position: 283

給出對象的java代碼:

public class KeyValue implements ExtendedCell, Cloneable {
	protected byte [] bytes = null;  // an immutable byte array that contains the KV
	protected int offset = 0;  // offset into bytes buffer KV starts at
	protected int length = 0;  // length of the KV starting from offset.
	private long seqId = 0;
//省略其他代碼
}

背景:byte數組內存儲的實際字節大小爲146。

那麼根據上面第一節的描述,KeyValue對象佔用,情況爲:

對象頭:8(Mark Word)+4(指向類的指針,啓用了壓縮,所以指針佔4字節,下同)=12
byte[] 數組對象頭:8(Mark Word)+4(指向類的指針) + 4(數組長度,佔4字節)=16
keyvalue大小 = 12(KeyValue對象頭) + 4(bytes屬性對byte[]數組引用) + 4(int類型 offset) + 4(int類型 length ) + 8(long類型 seqId )=32
byte數組字節實際長度:146+16=162. byte數組是一個獨立的對象,所以需要對齊填充。162需要對齊填充,填充結果爲 168
total_size_sum =
keyvalue大小 + bytes數組大小 = 32 + 168(byte[] 數組對象實際佔用存儲空間) = 200

edit heap size = 240 是統計的WALEdit ,源碼在下面。cells 是一個ArrayList,所以在計算的時候,除了上面的200字節,還要加上ArrayList佔用的空間。計算方式跟上面一樣。

public class WALEdit implements HeapSize {
	  private ArrayList<Cell> cells = null; // KeyValue是Cell的實現類
}

由於不同的JVM實現,或者不同的機器,不同的啓動參數,都可能造成實際佔用空間不同,所以需要再實際運行過程中統計真實的佔用情況,而不是簡單的使用上面的公式計算。

在hbase中有專門統計對象大小的工具類ClassSize:

REFERENCE = memoryLayout.oopSize();

    OBJECT = memoryLayout.headerSize();

    ARRAY = memoryLayout.arrayHeaderSize();

    ARRAYLIST = align(OBJECT + REFERENCE + (2 * Bytes.SIZEOF_INT)) + align(ARRAY);

//headerSize源碼:
private static class UnsafeLayout extends MemoryLayout {
    @SuppressWarnings("unused")
    private static final class HeaderSize {
      private byte a;
    }

    public UnsafeLayout() {
    }

    @Override
    int headerSize() {
      try {//這裏使用Unsafe本地方法,來獲取對象中第一個元素的偏移量。
        return (int) UnsafeAccess.theUnsafe.objectFieldOffset(
          HeaderSize.class.getDeclaredField("a"));
      } catch (NoSuchFieldException | SecurityException e) {
        LOG.error(e.toString(), e);
      }
      return super.headerSize();
    }
    //省略其他代碼
}

ObjectFieldOffSet

JAVA中對象的字段的定位可能通過staticFieldOffset方法實現,該方法返回給定field的內存地址偏移量,這個值對於給定的filed是唯一的且是固定不變的。

第一個屬性之前的空間也就是對象頭佔用的空間。以此來判斷對象的大小情況更加準確一些。
其他的也是採用類似運行時動態獲取佔用空間的方式,具體可以閱讀hbase源碼查看。

end.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章