
如何計(jì)算Java對象占堆內(nèi)存中的??
李強(qiáng),2018年5??職Qunar,現(xiàn)任?車票技術(shù)部Java開發(fā)?程師。參與構(gòu)建?車票業(yè)務(wù)系統(tǒng)的底層技術(shù)?持體系,個(gè)?
對并發(fā)編程、分布式系統(tǒng)等技術(shù)點(diǎn)感興趣。
前?
在實(shí)際?作中,我們可能會(huì)遇到需要計(jì)算某個(gè)對象占?堆內(nèi)存空間??的問題。這時(shí),就需要我們了解 Java 對象在堆
內(nèi)存中的實(shí)際存儲(chǔ)?式和存儲(chǔ)格式。本?就針對此問題展開分析,詳細(xì)介紹了 Java 對象在堆內(nèi)存中的結(jié)構(gòu),從?讓?
家能夠?便的計(jì)算出對象占?內(nèi)存的??。注意:本?默認(rèn)環(huán)境為 64 位操作系統(tǒng),JDK 為 1.8,JVM 為 HotSpot。
Oop-Klass模型
我們知道 Java 對象在內(nèi)存中的訪問?式,如下圖所?:
通過上圖,我們可以知道?個(gè) Java 實(shí)例對象由 Java 堆中的實(shí)例數(shù)據(jù)和?法區(qū)的類型數(shù)據(jù)兩部分組成。對應(yīng)到 JVM 內(nèi)
部,HotSpot 設(shè)計(jì)了?個(gè) Oop-Klass 的?分模型來表??個(gè) Java 實(shí)例對象的兩部分:
Klass 簡單來說就是 Java 類在 HotSpot 中的 C++ 對等體,主要?于描述對象實(shí)例的具體類型。?般 JVM 在加
載 class ?件時(shí),會(huì)在?法區(qū)創(chuàng)建 Klass ,表?類的元數(shù)據(jù),其包括常量池、字段、?法等。
Oop指的是 Ordinary Object Pointer(普通對象指針)。其在 Java 創(chuàng)建對象實(shí)例的時(shí)候創(chuàng)建,?于表?對象的實(shí)
例信息。也就是說,在 Java 應(yīng)?程序運(yùn)?中每創(chuàng)建?個(gè) Java 對象,在 JVM 內(nèi)部都會(huì)創(chuàng)建?個(gè) Oop 對象來表? Java
對象。這?有的同學(xué)可能會(huì)產(chǎn)?這樣的疑問:C++ 也是?種?向?qū)ο蟮木幊陶Z?,HotSpot 為什么不將 Java 對象直接
映射成?個(gè) C++ 對象,?是要拆分成兩個(gè)呢?這是因?yàn)樵?/span> C++ 中有?個(gè)概念叫虛函數(shù)表,每?個(gè) C++ 對象都會(huì)有?個(gè) 虛函數(shù)表。HotSpot 為了提?效率,復(fù)?虛函數(shù)表,才設(shè)計(jì)了這種 Oop-Klass 的?分模式。這個(gè)模型參考了 Smalltalk ,詳細(xì)論證見 Wiki。 Oop JVM 中,Oop 的共同基類型 為oopDesc 。另外根據(jù)表?的對象類型的不同,JVM 中具有多種 oopDesc ?類,每個(gè) oopDesc 的?類都代表?個(gè)在 JVM 內(nèi)部使?的特定對象類型。JVM 中 Oop 主要由如下?種類型組成: (詳細(xì)代碼見 JDK1.8 源碼:./src/hotspot/share/oops/ ) 1.//定義Oop的抽象基類 1.//定義Oop的抽象基類 2.typedef class oopDesc* oop; 3.//表??個(gè)Java實(shí)例對象 4.typedef class instanceOopDesc* instanceOop; 5.//定義數(shù)組Oop的抽象基類 6.typedef class arrayOopDesc* arrayOop; 7.//表?Java對象數(shù)組 8.typedef class objArrayOopDesc* objArrayOop; 9.//表?基本類型的數(shù)組 f class typeArrayOopDesc* typeArrayOop; 例如,當(dāng)我們使? new 創(chuàng)建?個(gè) Java 對象實(shí)例的時(shí)候,JVM 會(huì)創(chuàng)建?個(gè) instanceOopDesc 對象來表?這個(gè) Java 對 象;同理,當(dāng)我們使? new 創(chuàng)建?個(gè) Java 數(shù)組實(shí)例的時(shí)候,JVM 會(huì)創(chuàng)建?個(gè) arrayOopDes 對象來表?這個(gè)數(shù)組對 象。 Klass 同 Oop ?樣,JVM 中定義了如下的 Klass 類型: 1.//所有Klass類的基類 2.class Klass; 3.//描述?個(gè)Java類 4.class InstanceKlass; 5.//專有的InstanceKlass,表?的Klass 6.class InstanceMirrorKlass; 7.//專有的InstanceKlass,?于類加載器 8.class InstanceClassLoaderKlass; 9.//專有的InstanceKlass,?于表?nce的?類的Klass InstanceRefKlass; 11.//描述數(shù)組類型的類的基類 ArrayKlass; 13.//描述對象數(shù)組類 ObjArrayKlass; 15.//描述基本類型數(shù)組類 TypeArrayKlass; 這?需要額外說明?點(diǎn):1.8 中去掉了永久代(Perm),?改為了元空間(MetaSpace)。因此 JDK1.8 的 Oop-Klass 模 型與 JDK1.7 存在較?的區(qū)別,本?以 JDK1.8 為準(zhǔn),JDK1.7 到 1.8 的詳細(xì)區(qū)別可參考:1.7到1.8 Oop-Klass模型。 Java對象在堆中的布局 源碼解析 上?我們介紹到,JVM 中通過?個(gè) instanceOopDesc 對象來表??個(gè) Java 實(shí)例對象。想要了解 Java 對象在內(nèi)存中的 布局,只需要了解 instanceOopDesc 類的結(jié)構(gòu)即可。由于 instanceOopDesc 類是 oopDesc 類的?類,其代碼?部分 位于 oopDesc 類中。 我們直接來看 oopDesc 類的代碼:./src/hotspot/share/oops/ 中,它包含兩個(gè)數(shù)據(jù)成員: 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?于存儲(chǔ)對象的 HashCode、鎖標(biāo)記、偏向鎖、?旋時(shí)間、分代年齡等信息。 2._metadata _metadata是?個(gè)指向 Klass 的指針,是?個(gè)共?體( union )。也就是說它要么是 klass 字段要么是 compresdklass 。 compresdklass 。 當(dāng) JVM 開啟了-XX:+UCompresdClassPointers( 表?啟? Klass 指針壓縮選項(xiàng), JVM 默認(rèn)開啟 )選項(xiàng)時(shí)使? commpresdklass 來存儲(chǔ) Klass 引?了,否則使? _klass 存儲(chǔ) Klass 引?。 注意:在數(shù)組相關(guān)的 Oop 類中,除了上述兩個(gè)數(shù)據(jù)成員外,還有?個(gè) int 類型數(shù)據(jù)成員 length ,?于表?數(shù)組的長度。 詳細(xì)代碼見:./src/hotspot/share/oops/。 oopDesc 類本?的數(shù)據(jù)成員,我們稱之為對象頭。除了對象頭 之外,oopDesc 還需要保存 Java 對象的實(shí)例字段。這些實(shí)例字段緊跟對象頭存儲(chǔ),其起始偏移地址可通過 instanceOopDesc 類中的函數(shù)( 位于:./src/hotspot/share/oops/)計(jì)算得出: 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 內(nèi)部的內(nèi)存分布,下?我們通過?個(gè)例?來更直觀地說明。 假 如有如下代碼: 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.} 那么其在內(nèi)存中的布局如下圖所?: 總結(jié) 根據(jù)上?的學(xué)習(xí),我們可以總結(jié)出?個(gè) Java 對象在堆內(nèi)存中的布局?致如下所?: 注意:這?的 Padding ( 對齊填充 )是作為填充字段,為滿? Java 對象所占內(nèi)存必須為 8 字節(jié)的倍數(shù)?存在的。 1.對象頭:對象頭是 Java 對象存儲(chǔ)結(jié)構(gòu)中最復(fù)雜的部分。它由下述?部分組成: (1)mark word:在 64 位系統(tǒng)下? 8 字節(jié)表?;32 位系統(tǒng)為 4 字節(jié)。 (2)metadata:64 位系統(tǒng)下,若 JVM 開啟 Klass 指針壓縮選項(xiàng)( -XX:+UCompresdClassPointers,JVM 默認(rèn)開啟 此選項(xiàng) ),則? 4 字節(jié)表?;若不開啟指針壓縮( -XX:-UCompresdOops )則? 8 字節(jié)表?;32 位系統(tǒng)則使? 4 字節(jié)表?。 指針壓縮的?的就是為了節(jié)省內(nèi)存,若有同學(xué)對具體的壓縮算法感興趣可參考:CompresdOops。 (3)Length:若當(dāng)前對象為數(shù)組,那么對象頭中除了上述兩部分內(nèi)容外,還會(huì)有 4 字節(jié)的內(nèi)容?于表?存儲(chǔ)數(shù)組的長度 信息;若當(dāng)前對象不為數(shù)組,則對象頭中不存在此項(xiàng)信息。 2.實(shí)例數(shù)據(jù):實(shí)例數(shù)據(jù)中存儲(chǔ)的就是當(dāng)前對象的實(shí)例字段。字段的存儲(chǔ)類型有基本類型和引?類型兩種,它們對應(yīng)的存 儲(chǔ)??如下圖: 其中 ref 表?引?類型,引?類型實(shí)際上是?個(gè)地址指針,其在 32 位系統(tǒng)中,占? 4 字節(jié),64 位系統(tǒng)中,如果開啟了 指針壓縮( -XX:+UCompresdOops ,JVM 默認(rèn)開啟此選項(xiàng) )則占? 4 字節(jié),若不開啟則占? 8 字節(jié)。 此外,實(shí)例 數(shù)據(jù)中的字段既包括從?類繼承的,也包括?類本?定義的。這些字段在內(nèi)存中的存儲(chǔ)順序會(huì)受到虛擬機(jī)分配策略參數(shù) ( FieldsAllocationStyle )和字段在 Java 源碼中定義順序的影響。HotSpot 中有三種虛擬機(jī)分配策略,見如下代碼注 釋(源碼位置:./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 ,普通對象指針,也就是引?類型 )在基本數(shù)據(jù)類型前?, 其后依次是 longs/doubles, ints, shorts/chars, bytes , 最后是填充字段, 以滿?對齊要求。 2.策略1:oops 在基本數(shù)據(jù)類型之后。 3.策略2:?類中的引?類型與?類中的引?類型放在?起。JVM 默認(rèn)分配策略為 1 ,可通過參數(shù) - XX:FieldsAllocationStyle=2 ,將分配策略變更為 2 。策略 0 和策略 1 區(qū)別不?,都是將基本數(shù)據(jù)類型按照從?到?的 ?式排序,這樣可以降低空間開銷。?策略 3 將?類和?類的引?放在?起可以增加 GC 效率,試想在 GC 掃描引? 時(shí),由于?類和?類的引?連續(xù),可能只需要掃描?個(gè) cache line 即可,若?類和?類的引?不連續(xù),則需要掃描多個(gè) cache line ;另外連續(xù)的內(nèi)存引?還可減少 OopMap 的個(gè)數(shù),從?達(dá)到提? GC 效率的?的。關(guān)于虛擬機(jī)內(nèi)存分配策略 的詳細(xì)代碼解讀可參考:jvm源碼分析之oop-klass對象模型,這?不再展開細(xì)說。在滿?上述的虛擬機(jī)分配策略前提條 件下,?般?類的字段會(huì)在?類的字段之前,但是 JVM 也提供了參數(shù) -XX:+/-CompactFields ( 默認(rèn)開啟 ),來允許將? 類實(shí)例字段中的?對象插?到?類變量的縫隙中。 總體來看,實(shí)例數(shù)據(jù)的??就是對象的各個(gè)實(shí)例字段的??之和。 對齊填充:JVM 要求對象的??必須是 8 字節(jié)的整數(shù)倍,因此當(dāng)“對象頭+實(shí)例數(shù)據(jù)”的??不滿? 8 字節(jié)的整數(shù)倍時(shí), 就需要增加填充數(shù)據(jù),以滿?此條件。按照 8 字節(jié)對齊,是底層 CPU 數(shù)據(jù)總線讀取內(nèi)存數(shù)據(jù)的要求。通常 CPU 按照 字長來讀取數(shù)據(jù),?個(gè)數(shù)據(jù)若不對齊則可能需要 CPU 讀取兩次,若進(jìn)?了對齊,則?次性即可取出?標(biāo)數(shù)據(jù),這將會(huì) ??節(jié)省 CPU 資源,因此對象??需要對齊。 通過上?的介紹,我們基本就可以計(jì)算出?個(gè) Java 對象占?堆內(nèi)存的??了。假如有如下代碼: 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.} 那么?個(gè) Person 對象的堆內(nèi)存??可以計(jì)算得出: 對象頭:8( mark )+4( Klass 指針 )=12 對象頭:8( mark )+4( Klass 指針 )=12 實(shí)例數(shù)據(jù):4( age )+4( name )+1( married )+8( birthday )+2( tag )+8( sallay )=27那么此時(shí) Person 對象的??為: 12+27+1( padding )=40; 此時(shí)計(jì)算的 name 是?個(gè)指向 String 對象的指針,我們還需要加上 name 對象的??:8( mark )+4( Klass 指針 )+4( hash )+4( value[] )+4( padding )=24; 其中 value[] 是?個(gè) char 數(shù)組的指針,因此需要再加上 此數(shù)組的長度:8( mark )+4( Klass 指針 )+4( length )+8*2( 8 個(gè)char )+0( padding )=32。綜上,?個(gè) Person 對象占?堆 內(nèi)存的??為 40+24+32=96 字節(jié)。 HSDB 通過上?章節(jié)的學(xué)習(xí),我們已經(jīng) get 到如何計(jì)算?個(gè) Java 對象占?的堆內(nèi)存??,但是如何來驗(yàn)證我們計(jì)算的是否正 確呢?其實(shí) HotSpot 已經(jīng)為我們提供了?個(gè)?具來查詢運(yùn)?時(shí)對象的 Oops 結(jié)構(gòu),那就是 HSDB 。 1.我們在命令?中運(yùn)?如下程序: 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 獲取運(yùn)?的 Java 進(jìn)程號,運(yùn)?如下指令啟動(dòng) HSDB sudo java -cp $JAVA_HOME/lib/ ,然后選擇 File->Attach to HotSpot process并輸?進(jìn)程 ID : 3.此時(shí)會(huì)顯?對應(yīng)進(jìn)程的線程信息。點(diǎn)擊Tools->Object Histogram即可打開堆的對象列表。在列表中輸?想要查看的類 名稱進(jìn)?搜索: 雙擊對應(yīng)的對象,然后點(diǎn)擊 Inspect 按鈕即可看到該對象的 Oop 結(jié)構(gòu)信息: 4.也可以點(diǎn)擊 Tools->Class Browr打開類信息列表,并搜索想查看類信息,也可對象中各個(gè)字段的偏移量。 代碼計(jì)算內(nèi)存占?的?法 前?我們介紹了 Java 對象在堆中的內(nèi)存布局之后,就可?動(dòng)計(jì)算出?個(gè) Java 對象占?的堆內(nèi)存??,但是假如我們需 要在代碼中計(jì)算?個(gè)對象的內(nèi)存占??該如何進(jìn)?呢?這?總結(jié)了三種?式供?家參考: Instrumentation 使?ectSize()?法可以?便地計(jì)算出?個(gè)運(yùn)?時(shí)對象的??。關(guān)于如何獲取 Instrumentation 這?不再贅述,我們關(guān)注?下 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 11.*/ getObjectSize(Object objectToSize); 通過注釋我們可知,此?法求出的值是?個(gè)近似值,并不準(zhǔn)確。因此這種?法只適?于同?個(gè)對象多次求??并進(jìn)?? 較,不適??兩個(gè)對象之間?較。 使?Unsafe java 中的 類,有?個(gè) objectFieldOfft(Field f) ?法,表?獲取指定字段在所在實(shí)例中的起始地址偏移 量。因此我們可以獲取指定的對象中每個(gè)字段的偏移量,并求出其中的最?值。偏移量最?的字段肯定位于實(shí)例數(shù)據(jù)中 的最后,再使?該字段的偏移量加上該字段的實(shí)際??,就能知道該對象整體的??。 例如有如下類: 1.public class Example { 2.int size = 20; 3.String name = "XiaoMing"; 4.} 使?如下代碼計(jì)算其字段的偏移地址: 使?如下代碼計(jì)算其字段的偏移地址: 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.} 結(jié)果: 1.size offt is:12 2.name offt is:16 由上可知 name 的 offt 最?為 16 ,在加上其本?的長度 4 ( String 對象的指針 ),則 Example 對象的實(shí)際??為: 16+4+4( Padding )=24。 但是上述這種?式計(jì)算的只能是對象本?的??,并沒有計(jì)算對象中的引?類型所引?的對象的??。若想要獲取?個(gè) 對象的完整??,則還需要寫代碼進(jìn)?遞歸計(jì)算。 使?第三??具 這?提供?個(gè)第三?專門計(jì)算堆內(nèi)存占???的?具類: 1. 2. 3. 4. 5. 其中 RamUsageEstimator類提供獲取對象堆內(nèi)存占???的?法,如下所?驗(yàn)證我們前?章節(jié)計(jì)算的對象??: 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.結(jié)果: :80 :96 RamUsageEstimator 類主要就是根據(jù) JVM 規(guī)范計(jì)算對象的??,并不是根據(jù)實(shí)際的內(nèi)存地址計(jì)算。因此,它存在?個(gè) 缺點(diǎn),那就是可能存在與實(shí)際??不符合的情況。 參考資料 1.《深?理解 Java 虛擬機(jī)》第2版,周志明,機(jī)械?業(yè)出版社; 2. Hotspot :oops,klass 與 handle; 3. 如何計(jì)算 Java 對象所占內(nèi)存的??; 4. Hotspot GC研究- 開篇&對象內(nèi)存布局; 5. Java 對象內(nèi)存結(jié)構(gòu)。 總結(jié) 本??先介紹了 JVM 的 Oop-Klass 模型,這是理解 Java 內(nèi)存布局的基礎(chǔ),接下來講解了?個(gè) Java 對象在堆中的結(jié) 構(gòu),然后?簡單介紹了如何通過 HSDB 查看對象的 Oop 結(jié)構(gòu),最后介紹了?種通過代碼計(jì)算 Java 對象內(nèi)存布局的? 式。 掌握對象的內(nèi)存布局是解決?作中遇到的?些諸如:評估本地內(nèi)存的占?量、定位內(nèi)存泄漏 Bug 等問題的基礎(chǔ)。 希望?家通過閱讀本?能夠有所收獲。 本?參考了多??資料,并做了?些總結(jié),可能會(huì)有些不正確的地?敬請指正。null.

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