
如何計算Java對象占堆內存中的??
李強,2018年5??職Qunar,現任?車票技術部Java開發?程師。參與構建?車票業務系統的底層技術?持體系,個?
對并發編程、分布式系統等技術點感興趣。
前?
在實際?作中,我們可能會遇到需要計算某個對象占?堆內存空間??的問題。這時,就需要我們了解 Java 對象在堆
內存中的實際存儲?式和存儲格式。本?就針對此問題展開分析,詳細介紹了 Java 對象在堆內存中的結構,從?讓?
家能夠?便的計算出對象占?內存的??。注意:本?默認環境為 64 位操作系統,JDK 為 1.8,JVM 為 HotSpot。
Oop-Klass模型
我們知道 Java 對象在內存中的訪問?式,如下圖所?:
通過上圖,我們可以知道?個 Java 實例對象由 Java 堆中的實例數據和?法區的類型數據兩部分組成。對應到 JVM 內
部,HotSpot 設計了?個 Oop-Klass 的?分模型來表??個 Java 實例對象的兩部分:
Klass 簡單來說就是 Java 類在 HotSpot 中的 C++ 對等體,主要?于描述對象實例的具體類型。?般 JVM 在加
載 class ?件時,會在?法區創建 Klass ,表?類的元數據,其包括常量池、字段、?法等。
Oop指的是 Ordinary Object Pointer(普通對象指針)。其在 Java 創建對象實例的時候創建,?于表?對象的實
例信息。也就是說,在 Java 應?程序運?中每創建?個 Java 對象,在 JVM 內部都會創建?個 Oop 對象來表? Java
對象。這?有的同學可能會產?這樣的疑問:C++ 也是?種?向對象的編程語?,HotSpot 為什么不將 Java 對象直接
映射成?個 C++ 對象,?是要拆分成兩個呢?這是因為在 C++ 中有?個概念叫虛函數表,每?個 C++ 對象都會有?個
虛函數表。HotSpot 為了提?效率,復?虛函數表,才設計了這種 Oop-Klass 的?分模式。這個模型參考了 Smalltalk
,詳細論證見 Wiki。
Oop
JVM 中,Oop 的共同基類型 為oopDesc 。另外根據表?的對象類型的不同,JVM 中具有多種 oopDesc ?類,每個
oopDesc 的?類都代表?個在 JVM 內部使?的特定對象類型。JVM 中 Oop 主要由如下?種類型組成:
(詳細代碼見 JDK1.8 源碼:./src/hotspot/share/oops/ )
1.//定義Oop的抽象基類
1.//定義Oop的抽象基類
2.typedef class oopDesc* oop;
3.//表??個Java實例對象
4.typedef class instanceOopDesc* instanceOop;
5.//定義數組Oop的抽象基類
6.typedef class arrayOopDesc* arrayOop;
7.//表?Java對象數組
8.typedef class objArrayOopDesc* objArrayOop;
9.//表?基本類型的數組
f class typeArrayOopDesc* typeArrayOop;
例如,當我們使? new 創建?個 Java 對象實例的時候,JVM 會創建?個 instanceOopDesc 對象來表?這個 Java 對
象;同理,當我們使? new 創建?個 Java 數組實例的時候,JVM 會創建?個 arrayOopDes 對象來表?這個數組對
象。
Klass
同 Oop ?樣,JVM 中定義了如下的 Klass 類型:
1.//所有Klass類的基類
2.class Klass;
3.//描述?個Java類
4.class InstanceKlass;
5.//專有的InstanceKlass,表?的Klass
6.class InstanceMirrorKlass;
7.//專有的InstanceKlass,?于類加載器
8.class InstanceClassLoaderKlass;
9.//專有的InstanceKlass,?于表?nce的?類的Klass
InstanceRefKlass;
11.//描述數組類型的類的基類
ArrayKlass;
13.//描述對象數組類
ObjArrayKlass;
15.//描述基本類型數組類
TypeArrayKlass;
這?需要額外說明?點:1.8 中去掉了永久代(Perm),?改為了元空間(MetaSpace)。因此 JDK1.8 的 Oop-Klass 模
型與 JDK1.7 存在較?的區別,本?以 JDK1.8 為準,JDK1.7 到 1.8 的詳細區別可參考:1.7到1.8 Oop-Klass模型。
Java對象在堆中的布局 源碼解析
上?我們介紹到,JVM 中通過?個 instanceOopDesc 對象來表??個 Java 實例對象。想要了解 Java 對象在內存中的
布局,只需要了解 instanceOopDesc 類的結構即可。由于 instanceOopDesc 類是 oopDesc 類的?類,其代碼?部分
位于 oopDesc 類中。
我們直接來看 oopDesc 類的代碼:./src/hotspot/share/oops/ 中,它包含兩個數據成員:
1.class oopDesc {
2.private:
3.volatile markOop _mark;
4.union _metadata {
5.Klass* _klass;
6.narrowKlass _compresd_klass;
7.} _metadata;
8.}
1._mark _mark?于存儲對象的 HashCode、鎖標記、偏向鎖、?旋時間、分代年齡等信息。
2._metadata _metadata是?個指向 Klass 的指針,是?個共?體( union )。也就是說它要么是 klass 字段要么是
compresdklass 。
compresdklass 。
當 JVM 開啟了-XX:+UCompresdClassPointers( 表?啟? Klass 指針壓縮選項, JVM 默認開啟 )選項時使?
commpresdklass 來存儲 Klass 引?了,否則使? _klass 存儲 Klass 引?。
注意:在數組相關的 Oop 類中,除了上述兩個數據成員外,還有?個 int 類型數據成員 length ,?于表?數組的長度。
詳細代碼見:./src/hotspot/share/oops/。 oopDesc 類本?的數據成員,我們稱之為對象頭。除了對象頭
之外,oopDesc 還需要保存 Java 對象的實例字段。這些實例字段緊跟對象頭存儲,其起始偏移地址可通過
instanceOopDesc 類中的函數( 位于:./src/hotspot/share/oops/)計算得出:
1.// If compresd, the offt of the fields of the instance may not be aligned.
2.static int ba_offt_in_bytes() {
3.// offt computation code breaks if UCompresdClassPointers
4.// only is true
5.return (UCompresdOops && UCompresdClassPointers) ?
6.klass_gap_offt_in_bytes() :
7.sizeof(instanceOopDesc);
8.}
9.//./src/hotspot/share/oops/
int klass_gap_offt_in_bytes() {
(has_klass_gap(), "only applicable to compresd klass pointers");
klass_offt_in_bytes() + sizeof(narrowKlass);
13.}
14.
int klass_offt_in_bytes() { return offt_of(oopDesc, _metadata._klass); }
?例
通過對源碼的分析,我們基本弄清了 Java 對象在 JVM 內部的內存分布,下?我們通過?個例?來更直觀地說明。 假
如有如下代碼:
1.public class Model {
2.
3.public static int a = 1;
4.
5.public int b;
6.
7.public Model(int b) {
8.this.b = b;
9.}
10.
11.}
12.
static void main(String[] args) {
14.
c = 10;
modela = new Model(2);
17.
modelb = new Model(3);
19.}
那么其在內存中的布局如下圖所?:
總結
根據上?的學習,我們可以總結出?個 Java 對象在堆內存中的布局?致如下所?:
注意:這?的 Padding ( 對齊填充 )是作為填充字段,為滿? Java 對象所占內存必須為 8 字節的倍數?存在的。
1.對象頭:對象頭是 Java 對象存儲結構中最復雜的部分。它由下述?部分組成:
(1)mark word:在 64 位系統下? 8 字節表?;32 位系統為 4 字節。
(2)metadata:64 位系統下,若 JVM 開啟 Klass 指針壓縮選項( -XX:+UCompresdClassPointers,JVM 默認開啟
此選項 ),則? 4 字節表?;若不開啟指針壓縮( -XX:-UCompresdOops )則? 8 字節表?;32 位系統則使? 4
字節表?。 指針壓縮的?的就是為了節省內存,若有同學對具體的壓縮算法感興趣可參考:CompresdOops。
(3)Length:若當前對象為數組,那么對象頭中除了上述兩部分內容外,還會有 4 字節的內容?于表?存儲數組的長度
信息;若當前對象不為數組,則對象頭中不存在此項信息。
2.實例數據:實例數據中存儲的就是當前對象的實例字段。字段的存儲類型有基本類型和引?類型兩種,它們對應的存
儲??如下圖:
其中 ref 表?引?類型,引?類型實際上是?個地址指針,其在 32 位系統中,占? 4 字節,64 位系統中,如果開啟了
指針壓縮( -XX:+UCompresdOops ,JVM 默認開啟此選項 )則占? 4 字節,若不開啟則占? 8 字節。 此外,實例
數據中的字段既包括從?類繼承的,也包括?類本?定義的。這些字段在內存中的存儲順序會受到虛擬機分配策略參數
( FieldsAllocationStyle )和字段在 Java 源碼中定義順序的影響。HotSpot 中有三種虛擬機分配策略,見如下代碼注
釋(源碼位置:./hotspot/share/classfile/):
1.// Rearrange fields for a given allocation style
2.if( allocation_style == 0 ) {
3.// Fields order: oops, longs/doubles, ints, shorts/chars, bytes, padded fields
4.......
5.} el if( allocation_style == 1 ) {
6.// Fields order: longs/doubles, ints, shorts/chars, bytes, oops, padded fields
7.......
8.} el if( allocation_style == 2 ) {
9.// Fields allocation: oops fields in super and sub class are together.
10.......
11.}
從中可以看出這三種策略的字段排列順序不同:
1.策略0:oops ( Ordinary Object Pointers ,普通對象指針,也就是引?類型 )在基本數據類型前?, 其后依次是
longs/doubles, ints, shorts/chars, bytes , 最后是填充字段, 以滿?對齊要求。
2.策略1:oops 在基本數據類型之后。
3.策略2:?類中的引?類型與?類中的引?類型放在?起。JVM 默認分配策略為 1 ,可通過參數 -
XX:FieldsAllocationStyle=2 ,將分配策略變更為 2 。策略 0 和策略 1 區別不?,都是將基本數據類型按照從?到?的
?式排序,這樣可以降低空間開銷。?策略 3 將?類和?類的引?放在?起可以增加 GC 效率,試想在 GC 掃描引?
時,由于?類和?類的引?連續,可能只需要掃描?個 cache line 即可,若?類和?類的引?不連續,則需要掃描多個
cache line ;另外連續的內存引?還可減少 OopMap 的個數,從?達到提? GC 效率的?的。關于虛擬機內存分配策略
的詳細代碼解讀可參考:jvm源碼分析之oop-klass對象模型,這?不再展開細說。在滿?上述的虛擬機分配策略前提條
件下,?般?類的字段會在?類的字段之前,但是 JVM 也提供了參數 -XX:+/-CompactFields ( 默認開啟 ),來允許將?
類實例字段中的?對象插?到?類變量的縫隙中。 總體來看,實例數據的??就是對象的各個實例字段的??之和。
對齊填充:JVM 要求對象的??必須是 8 字節的整數倍,因此當“對象頭+實例數據”的??不滿? 8 字節的整數倍時,
就需要增加填充數據,以滿?此條件。按照 8 字節對齊,是底層 CPU 數據總線讀取內存數據的要求。通常 CPU 按照
字長來讀取數據,?個數據若不對齊則可能需要 CPU 讀取兩次,若進?了對齊,則?次性即可取出?標數據,這將會
??節省 CPU 資源,因此對象??需要對齊。
通過上?的介紹,我們基本就可以計算出?個 Java 對象占?堆內存的??了。假如有如下代碼:
1.public class People {
2.int age = 20;
3.
4.String name = "XiaoMing";
5.}
6.public class Person extends People {
7.boolean married = fal;
8.
9.long birthday = 3283520940L;
10.
tag = 'a';
12.
sallary = 4700.00d;
14.}
那么?個 Person 對象的堆內存??可以計算得出:
對象頭:8( mark )+4( Klass 指針 )=12
對象頭:8( mark )+4( Klass 指針 )=12
實例數據:4( age )+4( name )+1( married )+8( birthday )+2( tag )+8( sallay )=27那么此時 Person 對象的??為:
12+27+1( padding )=40; 此時計算的 name 是?個指向 String 對象的指針,我們還需要加上 name 對象的??:8(
mark )+4( Klass 指針 )+4( hash )+4( value[] )+4( padding )=24; 其中 value[] 是?個 char 數組的指針,因此需要再加上
此數組的長度:8( mark )+4( Klass 指針 )+4( length )+8*2( 8 個char )+0( padding )=32。綜上,?個 Person 對象占?堆
內存的??為 40+24+32=96 字節。
HSDB
通過上?章節的學習,我們已經 get 到如何計算?個 Java 對象占?的堆內存??,但是如何來驗證我們計算的是否正
確呢?其實 HotSpot 已經為我們提供了?個?具來查詢運?時對象的 Oops 結構,那就是 HSDB 。
1.我們在命令?中運?如下程序:
1.public static void main(String[] args) throws InterruptedException {
2.
3.Person person = new Person();
4.
5.(60*1000);
6.
7.n("end");
8.}
2.使? JPS 獲取運?的 Java 進程號,運?如下指令啟動 HSDB sudo java -cp $JAVA_HOME/lib/
,然后選擇 File->Attach to HotSpot process并輸?進程 ID :
3.此時會顯?對應進程的線程信息。點擊Tools->Object Histogram即可打開堆的對象列表。在列表中輸?想要查看的類
名稱進?搜索:
雙擊對應的對象,然后點擊 Inspect 按鈕即可看到該對象的 Oop 結構信息:
4.也可以點擊 Tools->Class Browr打開類信息列表,并搜索想查看類信息,也可對象中各個字段的偏移量。
代碼計算內存占?的?法
前?我們介紹了 Java 對象在堆中的內存布局之后,就可?動計算出?個 Java 對象占?的堆內存??,但是假如我們需
要在代碼中計算?個對象的內存占??該如何進?呢?這?總結了三種?式供?家參考:
Instrumentation
使?ectSize()?法可以?便地計算出?個運?時對象的??。關于如何獲取
Instrumentation 這?不再贅述,我們關注?下 getObjectSize()?法的注釋:
1./**
2.* Returns an implementation-specific approximation of the amount of storage consumed by
3.* the specified object. The result may include some or all of the object's overhead,
4.* and thus is uful for comparison within an implementation but not between implementations.
5.*
6.* The estimate may change during a single invocation of the JVM.
7.*
8.* @param objectToSize the object to size
9.* @return an implementation-specific approximation of the amount of storage consumed by the specified object
10.* @throws interException if the supplied Object is null.
11.*/
getObjectSize(Object objectToSize);
通過注釋我們可知,此?法求出的值是?個近似值,并不準確。因此這種?法只適?于同?個對象多次求??并進??
較,不適??兩個對象之間?較。
使?Unsafe
java 中的 類,有?個 objectFieldOfft(Field f) ?法,表?獲取指定字段在所在實例中的起始地址偏移
量。因此我們可以獲取指定的對象中每個字段的偏移量,并求出其中的最?值。偏移量最?的字段肯定位于實例數據中
的最后,再使?該字段的偏移量加上該字段的實際??,就能知道該對象整體的??。 例如有如下類:
1.public class Example {
2.int size = 20;
3.String name = "XiaoMing";
4.}
使?如下代碼計算其字段的偏移地址:
使?如下代碼計算其字段的偏移地址:
1.public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
2.Field field = laredField("theUnsafe");
3.essible(true);
4.Unsafe unsafe = (Unsafe) (null);
5.
6.Example example = new Example();
7.
8.Field sizeField = ss().getDeclaredField("size");
9.long sizeAddr = FieldOfft(sizeField);
n("size offt is:" + sizeAddr);
11.
nameField = ss().getDeclaredField("name");
nameAddr = FieldOfft(nameField);
n("name offt is:" + nameAddr);
15.}
結果:
1.size offt is:12
2.name offt is:16
由上可知 name 的 offt 最?為 16 ,在加上其本?的長度 4 ( String 對象的指針 ),則 Example 對象的實際??為:
16+4+4( Padding )=24。
但是上述這種?式計算的只能是對象本?的??,并沒有計算對象中的引?類型所引?的對象的??。若想要獲取?個
對象的完整??,則還需要寫代碼進?遞歸計算。
使?第三??具
這?提供?個第三?專門計算堆內存占???的?具類:
1.
2.
3.
4.
5.
其中 RamUsageEstimator類提供獲取對象堆內存占???的?法,如下所?驗證我們前?章節計算的對象??:
1.public static void main(String[] args) {
2.People people = new People();
3.
4.Person person = new Person();
5.
6.n("people:"+(people));
7.n("person:"+(person));
8.
9.}
10.結果:
:80
:96
RamUsageEstimator 類主要就是根據 JVM 規范計算對象的??,并不是根據實際的內存地址計算。因此,它存在?個
缺點,那就是可能存在與實際??不符合的情況。
參考資料
1.《深?理解 Java 虛擬機》第2版,周志明,機械?業出版社;
2. Hotspot :oops,klass 與 handle;
3. 如何計算 Java 對象所占內存的??;
4. Hotspot GC研究- 開篇&對象內存布局;
5. Java 對象內存結構。
總結
本??先介紹了 JVM 的 Oop-Klass 模型,這是理解 Java 內存布局的基礎,接下來講解了?個 Java 對象在堆中的結
構,然后?簡單介紹了如何通過 HSDB 查看對象的 Oop 結構,最后介紹了?種通過代碼計算 Java 對象內存布局的?
式。 掌握對象的內存布局是解決?作中遇到的?些諸如:評估本地內存的占?量、定位內存泄漏 Bug 等問題的基礎。
希望?家通過閱讀本?能夠有所收獲。 本?參考了多??資料,并做了?些總結,可能會有些不正確的地?敬請指正。

本文發布于:2023-05-26 04:34:00,感謝您對本站的認可!
本文鏈接:http://m.newhan.cn/zhishi/a/1685046841179303.html
版權聲明:本站內容均來自互聯網,僅供演示用,請勿用于商業和其他非法用途。如果侵犯了您的權益請與我們聯系,我們將在24小時內刪除。
本文word下載地址:如何計算Java對象占堆內存中的大小.doc
本文 PDF 下載地址:如何計算Java對象占堆內存中的大小.pdf
| 留言與評論(共有 0 條評論) |