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

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

可以直接進入正題對象的組成這一節

byte與bit

bit: 位,比特。 信息的最小單位,二進制數中的一個位數(二進制位),其值爲“0”或“1”;

byte: 字節。計算機文件大小的基本計算單位;

  • 原碼:正數本身,負數,正數對應的二進制最高位爲1(負);
  • 反碼:正數本身,負數,符號位不變,其餘各位取反;
  • 補碼:正數本身,負數:反碼+1
  • 0的反碼、補碼都爲零

注意: 計算機處理是以補碼形式,我們最終看到的是原碼形式

例如:

System.out.println((byte)233); // -23
System.out.println((byte)-233); // 23
System.out.println("~b2: " + ~10); // -11

列表

在這裏插入圖片描述

注意:

  • 對於數組32,默認爲int型,32B爲byte類型,32S爲short類型,32L爲long型
  • 基本數據類型自動轉換(低轉高,高轉低會丟失精度)
    • byte -> short
    • char -> int -> long
    • float -> double
    • int -> float
    • long -> double

後面會出一篇關於低轉高,高轉低的計算博文,敬請期待!

Java中對象佔用內存大小

對象的組成

可用如下一張圖來概括

對象組成圖

具體大小

名稱(單位byte) 32位 64位 開啓指針壓縮後(指針對64位有效且默認開啓)
對象頭(Header) 8 16 12
數組對象頭 12 24 16
引用(reference) 4 8 4
  • 開啓指針壓縮指令-XX:+UseCompressedOops,關閉指令-XX:-UseCompressedOops,只在64位纔有效且默認開啓;
  • 數組對象頭比普通對象多了個數組長度;
對象頭(Header)

“用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64bit,官方稱它爲"Mark Word”

實例數據(Instance Data)

“對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來”

對其補充(Padding)

“第三部分對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起着佔位符的作用。由於HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或者2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。”

HotSpot的對齊方式爲8字節對齊:

(Header + Instance Data + Paddding) % 8 = 0 並且 0 <= padding < 8

實戰

ShallowRetained區別

再看具體的例子之前,需要了解下兩個名詞,下面使用Java性能監控工具Jprofile會用到

  • Shallow Size 對象自身佔用的內存大小,不包括它引用的對象
    • 針對非數組類型的對象,它的大小就是對象與它所有的成員變量大小的總和。當然這裏面還會包括一些java語言特性的數據存儲單元。
    • 針對數組類型的對象,它的大小是數組元素對象的大小總和。
  • Retained Size Retained Size=當前對象大小+當前對象可直接或間接引用到的對象的大小總和。(間接引用的含義:A->B->C, C就是間接引用)

注意:以下實驗均在此環境下進行:

java version "1.8.0_171"
Java(TM) SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)

實例一:

public class A {
	private int i;

	public static void main(String[] args) throws InterruptedException {
		A a = new A();
		Thread.sleep(1000 * 1000);
		System.out.println(a);
	}
}

分析: 64位下默認開啓指針壓縮,對象頭位12byte, i4byte,此時12 + 4 = 16 可以整除8,所以padding=0,最終

12(header) + 4(instance data)+0(padding)=16byte jprofile結果如下:

在這裏插入圖片描述

如果我們關掉指針壓縮,

16(header) + 4(instance data)+4(padding)=24byte 16+4=20,不能整除8,需要再加上4Jprofile如下

實例二

public class B {
   private int i = 5;
   private Integer ii = 128;

   public static void main(String[] args) throws InterruptedException {
      B b = new B();
      Thread.sleep(1000 * 1000);
      System.out.println(b);
   }
}

比較特殊的地方時這裏複製了,因爲包裝類型有自己的緩存,可以看這裏*

開啓指針壓縮,計算內存大小

Shallow Size: 12(B Header) + 4 (i instance) + 4 (ii reference) + 4(padding) = 24bytes

Retained Size: 12(B Header) + 4 (i instance) + 4 (ii reference) + (12(ii header) + 4(instance)+ 0(padding)) + 4(padding) = 40bytes

Jprofile如下

實例三

public class C {
   private int i;
   private char[] cc;

   public C() {
      i = 5;
      cc = new char[]{'a', 'b', 'c'};
   }

   public static void main(String[] args) throws InterruptedException {
      C c = new C();
      Thread.sleep(1000 * 1000);
      System.out.println(c);
   }
}

多了數組,注意數組自己本身的padding

Shallow Size: 12(C Header) + 4 (i instance) + 4 (cc reference) + 4(padding) = 24bytes

Retained Size: 12(C Header) + 4 (i instance) + 4 (cc reference) + (16(cc header) + 2(instance) * 3+ 2(padding)) + 4(padding) = 48bytes

Jprofile如下

實例四

public class D {
	private Map<String, String> map;

	public D() {
		map = new HashMap<>();
		map.put("A", "A");
	}

	public static void main(String[] args) throws InterruptedException {
		D d = new D();
		Thread.sleep(1000 * 1000);
		System.out.println(d);
	}
}

Shallow Size: 12(D header) + 4(map reference) + 0(padding) = 16bytes

基本上Shallow Size很容易就算出來

但是Retained Size就異常的複雜,首先我們要去了解HashMap的結構

transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;
int threshold;
final float loadFactor

// 來自AbstractMap
transient Set<K>        keySet;
transient Collection<V> values;

Node的結構如下:

final int hash;
final K key;
V value;
Node<K,V> next;

注:此處的K,V是String類型

再看String的結構

    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

以下是自己的計算過程,結果是對了,但不知道具體過程是否正確,僅供參考!!!

  • D對象12(D header) + 4(map reference)=16

  • HashMap:12(header) + 4(table ref) + 4(entrySet ref) + 4(size) + 4(modCount) + 4(thresload) + 4(float) + 4(keySet ref) + 4(values ref) + 4(padding)=48

  • Node[]: 16(header) + 4(hash) + 4(key ref) + 4(val ref) + 4(next ref) + 0(padding)=32

  • K與V:(12(hader) + 4(hash) + 4(value ref) + (16(value header) + 1 * 2(char)) + 2(padding) = 40) * 2=80

最終:16 + 48 + 32 + 80 = 176!!!

在這裏插入圖片描述

總結

最後總結一下,計算一個對象的大小需要注意一下幾步:

  • Shallow 與Retained的區別;

  • 對象本身的大小;

  • 對象引用的對象也要注意對其補充,保證其也是被8整除的;

  • 複雜對象需要深入分析

  • 機器位數

能快速估算出每一個對象佔用空間的大小可以在編程的時候正確選擇數據結構,有興趣得可以看一下這一節

“5.2.6 不恰當數據結構導致內存佔用過大”
摘錄來自: 周志明. “深入理解Java虛擬機:JVM高級特性與最佳實踐。” iBooks.

當然了,就算不知道怎麼估算,最終還是有神器Jprofile幫助我們監控性能與優化~

參考:

https://www.cnblogs.com/zhanjindong/p/3757767.html
https://blog.csdn.net/ITer_ZC/article/details/41822719
https://segmentfault.com/a/1190000006933272
https://www.jianshu.com/p/40faea07d4d2

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