前言
一直以來,對java對象大小的概念停留在基礎數據類型,比如byte占1字節,int占4字節,long占8字節等,但是一個對象包含的內存空間肯定不只有這些。
假設有類A和B,當new A()或者new B()后,實際占用的java內存是多大呢?下面就對此進行詳細分析。
static class A{ String s = new String(); int i = 0; } static class B{ String s; int i; }
對象大小分析
如圖1,java對象在內存中占用的空間分為3類, 1. 對象頭(Header); 2. 實例數據(Instance Data); 3. 對齊填充(Padding)。而我們常說的基礎數據類型大小主要是指第二類實例數據。
圖1
對象頭
HotSpot虛擬機的對象頭包括兩部分信息:
markword和klass 。第一部分markword,用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等。 另外一部分是klass類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
數組長度
如果對象是一個數組, 那在對象頭中還必須有一塊數據用於記錄數組長度,也就是一個int類型的對象,占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個字節
實例數據
實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。
對齊填充
最后一塊對齊填充空間並不是必然存在的,也沒有特別的含義,它僅僅起着占位符的作用。這是由於HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。
如何查看java對象大小
1. 基於JDK1.8
JDK1.8有一個類`jdk.nashorn.internal.ir.debug.ObjectSizeCalculator`可以評估出對象的大小
// 直接調用靜態方法即可使用
ObjectSizeCalculator.getObjectSize(obj)
2. spark庫
spark庫中有一個類`org.apache.spark.util.SizeEstimator`
// 直接調用靜態方法即可使用
SizeEstimator.estimate(obj)
3. 基於JDK1.5的Instrumentation
// 需要編譯成jar調用,沒有前者方便
案例
分析完對象的組成結構后,再回頭來看那個問題
// 對象A: 對象頭12B + 內部對象s引用 4B + 內部對象i 基礎類型int 4B + 對齊 4B = 24B // 內部對象s 對象頭12B + 2個內部的int類型8B + 內部的char[]引用 4B + 對齊0B = 24B // 內部對象str的內部對象char數組 對象頭12B + 數組長度4B + 對齊0B = 16B // 總: 對象A 24+ 內部對象s 24B + 內部對象s的內部對象char數組 16B =64B class A { String s = new String(); int i = 0; } // 對象B:對象頭12B + 內部對象s引用 4B + 內部對象i 基礎類型int 4B + 對齊 4B = 24B // s沒有被分配堆內存空間 // 總: 對象B 24B class B { String s; int i = 0; }
總結
對象在jvm中不是完全連續的,這是由於GC的原因,總會出現散亂的內存。這就導致了jvm必須為每個對象分配一段內存空間來存儲其引用的指針,再結合對象的其他必須的元數據,使得對象在持有真實數據的基礎上還需要維護額外的數據。
在寫java代碼需要小心這些jvm內存陷阱。
參考
// stackoverflow給出的幾種計算對象大小方法