如何计算Java对象所占内存的大小
摘要
本文以如何計(jì)算Java對(duì)象占用內(nèi)存大小為切入點(diǎn),在討論計(jì)算Java對(duì)象占用堆內(nèi)存大小的方法的基礎(chǔ)上,詳細(xì)討論了Java對(duì)象頭格式并結(jié)合JDK源碼對(duì)對(duì)象頭中的協(xié)議字段做了介紹,涉及內(nèi)存模型、鎖原理、分代GC、OOP-Klass模型等內(nèi)容。最后推薦JDK自帶的Hotspot Debug工具——HSDB,來(lái)查看對(duì)象在內(nèi)存中的具體存在形式,以論證文中所述內(nèi)容。
背景
目前我們系統(tǒng)的業(yè)務(wù)代碼中大量使用了LocalCache的方式做本地緩存,而且cache的maxSize通常設(shè)的比較大,比如10000。我們的業(yè)務(wù)系統(tǒng)中就使用了size為10000的15個(gè)本地緩存,所以最壞情況下將可緩存15萬(wàn)個(gè)對(duì)象。這會(huì)消耗掉不菲的本地堆內(nèi)存,而至于實(shí)際上到底應(yīng)該設(shè)多大容量的緩存、運(yùn)行時(shí)這大量的本地緩存會(huì)給堆內(nèi)存帶來(lái)多少壓力,實(shí)際占用多少內(nèi)存大小,會(huì)不會(huì)有較高的緩存穿透風(fēng)險(xiǎn),目前并不方便知悉。考慮到對(duì)緩存實(shí)際占用內(nèi)存的大小能有個(gè)更直觀和量化的參考,需要對(duì)運(yùn)行時(shí)指定對(duì)象的內(nèi)存占用進(jìn)行評(píng)估和計(jì)算。
要計(jì)算Java對(duì)象占用內(nèi)存的大小,首先需要了解Java對(duì)象在內(nèi)存中的實(shí)際存儲(chǔ)方式和存儲(chǔ)格式。
另一方面,大家都了解Java對(duì)象的存儲(chǔ)總得來(lái)說(shuō)會(huì)占用JVM內(nèi)存的堆內(nèi)存、棧內(nèi)存及方法區(qū),但由于棧內(nèi)存中存放的數(shù)據(jù)可以看做是運(yùn)行時(shí)的臨時(shí)數(shù)據(jù),主要表現(xiàn)為本地變量、操作數(shù)、對(duì)象引用地址等。這些數(shù)據(jù)會(huì)在方法執(zhí)行結(jié)束后立即回收掉,不會(huì)駐留。對(duì)存儲(chǔ)空間空間的占用也只是執(zhí)行函數(shù)指令時(shí)所必須的空間。通常不會(huì)造成內(nèi)存的瓶頸。而方法區(qū)中存儲(chǔ)的則是對(duì)象所對(duì)應(yīng)的類信息、函數(shù)表、構(gòu)造函數(shù)、靜態(tài)常量等,這些信息在類加載時(shí)(按需)只會(huì)在方法區(qū)中存儲(chǔ)一份,不會(huì)產(chǎn)生額外的存儲(chǔ)空間。因此本文所要討論的主要目標(biāo)是Java對(duì)象對(duì)堆內(nèi)存的占用。
內(nèi)存占用計(jì)算方法
如果讀者關(guān)心對(duì)象在JVM中的存儲(chǔ)原理,可閱讀本文后邊幾個(gè)小節(jié)中關(guān)于對(duì)象存儲(chǔ)原理的介紹。如果不關(guān)心對(duì)象存儲(chǔ)原理,而只想直接計(jì)算內(nèi)存占用的話,其實(shí)并不難,筆者這里總結(jié)了三種方法以供參考:
1. Instrumentation
使用java.lang.instrument.Instrumentation.getObjectSize()方法,可以很方便的計(jì)算任何一個(gè)運(yùn)行時(shí)對(duì)象的大小,返回該對(duì)象本身及其間接引用的對(duì)象在內(nèi)存中的大小。不過(guò),這個(gè)類的唯一實(shí)現(xiàn)類InstrumentationImpl的構(gòu)造方法是私有的,在創(chuàng)建時(shí),需要依賴一個(gè)nativeAgent,和運(yùn)行環(huán)境所支持的一些預(yù)定義類信息,我們?cè)诖a中無(wú)法直接實(shí)例化它,需要在JVM啟動(dòng)時(shí),通過(guò)指定代理的方式,讓JVM來(lái)實(shí)例化它。
具體來(lái)講,就是需要聲明一個(gè)premain方法,它和main方法的方法簽名有點(diǎn)相似,只不過(guò)方法名叫“premain”,同時(shí)方法參數(shù)也不一樣,它接收一個(gè)String類型和instrumentation參數(shù),而String參數(shù)實(shí)際上和String[]是一樣的,只不過(guò)用String統(tǒng)一來(lái)表達(dá)的。在premain函數(shù)中,將instrumentation參數(shù)賦給一個(gè)靜態(tài)變量,其它地方就可以使用了。如:
/*** @author yepei* @date 2018/04/23* @description*/ public class SizeTool {private static Instrumentation instrumentation;public static void premain(String args, Instrumentation inst) {instrumentation = inst;}public static long getObjectSize(Object o) {return instrumentation.getObjectSize(o);} }從方法名可以猜到,這里的premain是要先于main執(zhí)行的,而先于main執(zhí)行,這個(gè)動(dòng)作只能由JVM來(lái)完成了。即在JVM啟動(dòng)時(shí),先啟動(dòng)一個(gè)agent,操作如下:
假設(shè)main方法所在的jar包為:A.jar,premain方法所在的jar包為B.jar。注意為main所在的代碼打包時(shí),和其它工具類打包一樣,需要聲明一個(gè)MANIFEST.MF清單文件,如下所求:
Manifest-Version: 1.0 Main-Class: yp.tools.Main Premain-Class: yp.tools.SizeTool然后執(zhí)行java命令執(zhí)行jar文件:
java -javaagent:B.jar -jar A.jar點(diǎn)評(píng):這種方法的優(yōu)點(diǎn)是編碼簡(jiǎn)單,缺點(diǎn)就是必須啟動(dòng)一個(gè)javaagent,因此要求修改Java的啟動(dòng)參數(shù)。
2. 使用Unsafe
java中的sun.misc.Unsafe類,有一個(gè)objectFieldOffset(Field f)方法,表示獲取指定字段在所在實(shí)例中的起始地址偏移量,如此可以計(jì)算出指定的對(duì)象中每個(gè)字段的偏移量,值為最大的那個(gè)就是最后一個(gè)字段的首地址,加上該字段的實(shí)際大小,就能知道該對(duì)象整體的大小。如現(xiàn)有一Person類:
class Person{int age;String name;boolean married; }假設(shè)該類的一個(gè)實(shí)例p,通過(guò)Unsafe.objectFieldOffset()方法計(jì)算到得age/birthday/married三個(gè)字段的偏移量分別是16,21, 17,則表明p1對(duì)象中的最后一個(gè)字段是name,它的首地址是21,由于它是一個(gè)引用,所以它的大小默認(rèn)為4(開(kāi)啟指針壓縮),則該對(duì)象本身的大小就是21+4+ 7= 32字節(jié)。其中7表示padding,即為了使結(jié)果變成8的整數(shù)倍而做的padding。
但上述計(jì)算,只是計(jì)算了對(duì)象本身的大小,并沒(méi)有計(jì)算其所引用的引用類型的最終大小,這就需要手工寫(xiě)代碼進(jìn)行遞歸計(jì)算了。
點(diǎn)評(píng):使用Unsafe可以完全不care對(duì)象內(nèi)的復(fù)雜構(gòu)成,可以很精確的計(jì)算出對(duì)象頭的大小(即第一個(gè)字段的偏移)及每個(gè)字段的偏移。缺點(diǎn)是Unsafe通常禁止開(kāi)發(fā)者直接使用,需要通過(guò)反射獲取其實(shí)例,另外,最后一個(gè)字段的大小需要手工計(jì)算。其次需要手工寫(xiě)代碼遞歸計(jì)算才能得到對(duì)象及其所引用的對(duì)象的綜合大小,相對(duì)比較麻煩。
3. 使用第三方工具
這里要介紹的是lucene提供的專門用于計(jì)算堆內(nèi)存占用大小的工具類:RamUsageEstimator,maven坐標(biāo):
<dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-core</artifactId><version>4.0.0</version> </dependency>RamUsageEstimator就是根據(jù)java對(duì)象在堆內(nèi)存中的存儲(chǔ)格式,通過(guò)計(jì)算Java對(duì)象頭、實(shí)例數(shù)據(jù)、引用等的大小,相加而得,如果有引用,還能遞歸計(jì)算引用對(duì)象的大小。RamUsageEstimator的源碼并不多,幾百行,清晰可讀。這里不進(jìn)行一一解讀了。它在初始化的時(shí)候會(huì)根據(jù)當(dāng)前JVM運(yùn)行環(huán)境、CPU架構(gòu)、運(yùn)行參數(shù)、是否開(kāi)啟指針壓縮、JDK版本等綜合計(jì)算對(duì)象頭的大小,而實(shí)例數(shù)據(jù)部分則按照java基礎(chǔ)數(shù)據(jù)類型的標(biāo)準(zhǔn)大小進(jìn)行計(jì)算。思路簡(jiǎn)單,同時(shí)也在一定程度上反映出了Java對(duì)象格式的奧秘!
常用方法如下:
//計(jì)算指定對(duì)象及其引用樹(shù)上的所有對(duì)象的綜合大小,單位字節(jié) long RamUsageEstimator.sizeOf(Object obj)//計(jì)算指定對(duì)象本身在堆空間的大小,單位字節(jié) long RamUsageEstimator.shallowSizeOf(Object obj)//計(jì)算指定對(duì)象及其引用樹(shù)上的所有對(duì)象的綜合大小,返回可讀的結(jié)果,如:2KBString RamUsageEstimator.humanSizeOf(Object obj)點(diǎn)評(píng):使用該第三方工具比較簡(jiǎn)單直接,主要依靠JVM本身環(huán)境、參數(shù)及CPU架構(gòu)計(jì)算頭信息,再依據(jù)數(shù)據(jù)類型的標(biāo)準(zhǔn)計(jì)算實(shí)例字段大小,計(jì)算速度很快,另外使用較方便。如果非要說(shuō)這種方式有什么缺點(diǎn)的話,那就是這種方式計(jì)算所得的對(duì)象頭大小是基于JVM聲明規(guī)范的,并不是通過(guò)運(yùn)行時(shí)內(nèi)存地址計(jì)算而得,存在與實(shí)際大小不符的這種可能性。
Java對(duì)象格式
在HotSpot虛擬機(jī)中,Java對(duì)象的存儲(chǔ)格式也是一個(gè)協(xié)議或者數(shù)據(jù)結(jié)構(gòu),底層是用C++代碼定義的。Java對(duì)象結(jié)構(gòu)大致如下圖所示——
image
即,Java對(duì)象從整體上可以分為三個(gè)部分,對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充
對(duì)象頭:Instance Header,Java對(duì)象最復(fù)雜的一部分,采用C++定義了頭的協(xié)議格式,存儲(chǔ)了Java對(duì)象hash、GC年齡、鎖標(biāo)記、class指針、數(shù)組長(zhǎng)度等信息,稍后做出詳細(xì)解說(shuō)。
實(shí)例數(shù)據(jù):Instance Data,這部分?jǐn)?shù)據(jù)才是真正具有業(yè)務(wù)意義的數(shù)據(jù),實(shí)際上就是當(dāng)前對(duì)象中的實(shí)例字段。在VM中,對(duì)象的字段是由基本數(shù)據(jù)類型和引用類型組成的。其所占用空間的大小如下所示:
image.png
說(shuō)明:其中ref表示引用類型,引用類型實(shí)際上是一個(gè)地址指針,32bit機(jī)器上,占用4字節(jié),64bit機(jī)器上,在jdk1.6之后,如果開(kāi)啟了指針壓縮(默認(rèn)開(kāi)啟: -XX:UseCompressedOops,僅支持64位機(jī)器),則占用4字節(jié)。Java對(duì)象的所有字段類型都可映射為上述類型之一,因此實(shí)例數(shù)據(jù)部分的大小,實(shí)際上就是這些字段類型的大小之和。當(dāng)然,實(shí)際情況可能比這個(gè)稍微復(fù)雜一點(diǎn),如字段排序、內(nèi)部padding以及父類字段大小的計(jì)算等。
對(duì)齊填充:Padding,VM要求對(duì)象大小須是8的整體數(shù),該部分是為了讓整體對(duì)象在內(nèi)存中的地址空間大小達(dá)到8的整數(shù)倍而額外占用的字節(jié)數(shù)。
對(duì)象頭
對(duì)象頭是理解JVM中對(duì)象存儲(chǔ)方式的最核心的部分,甚至是理解java多線程、分代GC、鎖等理論的基礎(chǔ),也是窺探JVM底層諸多實(shí)現(xiàn)細(xì)節(jié)的出發(fā)點(diǎn)。做為一個(gè)java程序猿,這是不可不了解的一部分。那么這里提到的對(duì)象頭到底是什么呢?
參考OpenJDK中JVM源碼部分,對(duì)對(duì)象頭的C++定義如下:
class oopDesc {friend class VMStructs;private:volatile markOop _mark;union _metadata {wideKlassOop _klass;narrowOop _compressed_klass;} _metadata;... }源碼里的 _mark 和 _metadata兩個(gè)字段就是對(duì)象頭的定義,分別表示對(duì)象頭中的兩個(gè)基本組成部分,_mark用于存儲(chǔ)hash、gc年齡、鎖標(biāo)記、偏向鎖、自旋時(shí)間等,而_metadata是個(gè)共用體(union),即_klass字段或_compressed_klass,存儲(chǔ)當(dāng)前對(duì)象到所在class的引用,而這個(gè)引用的要么由“_klass”來(lái)存儲(chǔ),要么由“_compressed_klass”來(lái)存儲(chǔ),其中_compressed_klass表示壓縮的class指針,即當(dāng)JVM開(kāi)啟了 -XX:UseCompressedOops選項(xiàng)時(shí),就表示啟用指針壓縮選項(xiàng),自然就使用_commpressed_klass來(lái)存儲(chǔ)class引用了,否則使用_klass。
注意到,_mark的類型是 markOop,而_metadata的類型是union,_metadata內(nèi)部?jī)蓚€(gè)字段:_klass和_compressed_klass類型分別為wideKlassOop和narrowOop,分別表示什么意思呢?這里順便說(shuō)一個(gè)union聯(lián)合體的概念,這是在C++中的一種結(jié)構(gòu)聲明,類似struct,稱作:“聯(lián)合”,它是一種特殊的類,也是一種構(gòu)造類型的數(shù)據(jù)結(jié)構(gòu)。在一個(gè)“聯(lián)合”內(nèi)可以定義多種不同的數(shù)據(jù)類型, 一個(gè)被說(shuō)明為該“聯(lián)合”類型的變量中,允許裝入該“聯(lián)合”所定義的任何一種數(shù)據(jù),這些數(shù)據(jù)共享同一段內(nèi)存,已達(dá)到節(jié)省空間的目的。由此可見(jiàn),剛剛所說(shuō)的使用-XX:UseCompressedOops后,就自動(dòng)使用_metadata中的_compressed_klass來(lái)作為指向當(dāng)前對(duì)象的class引用,它的類型是narrowOop。可以看到,對(duì)象頭中的兩個(gè)字段的定義都包含了“Oop”字眼,不難猜出,這是一種在JVM層定義好的“類型”。
OOP-Klass模型
實(shí)際上,Java的面向?qū)ο笤谡Z(yǔ)言層是通過(guò)java的class定義實(shí)現(xiàn)的,而在JVM層,也有對(duì)應(yīng)的實(shí)現(xiàn),那就是Oop模型。所謂Oop模型,全稱:`Ordinary Object Pointer`,即普通對(duì)象指針。JVM層用于定義Java對(duì)象模型及一些元數(shù)據(jù)格式的模型就是:Oop,可以認(rèn)為是JVM層中的“類”。通過(guò)[JDK源碼](https://github.com/openjdk-mirror/jdk7u-hotspot/tree/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/oops)可以看到,有很多模型定義的名稱都是以O(shè)op結(jié)尾:`arrayOop`/`markOop`/`instanceOop`/`methodOop`/`objectArrayOop`等,什么意思呢?HotSpot是基于c++語(yǔ)言實(shí)現(xiàn)的,它最核心的地方是設(shè)計(jì)了兩種模型,分別是`OOP`和`Klass`,稱之為`OOP-Klass Model`. 其中`OOP`用來(lái)將指針對(duì)象化,比C++底層使用的"`*`"更好用,**每一個(gè)類型的OOP都代表一個(gè)在JVM內(nèi)部使用的特定對(duì)象的類型**。而`Klass`則用來(lái)描述JVM層面中對(duì)象實(shí)例的具體類型,它是java實(shí)現(xiàn)語(yǔ)言層面類型的基礎(chǔ),或者說(shuō)是**對(duì)java語(yǔ)言層類型的VM層描述**。所以看到openJDK源碼中的定義基本都以O(shè)op或Klass結(jié)尾,如圖所示:由上述定義可以簡(jiǎn)單的說(shuō),Oop就是JVM內(nèi)部對(duì)象類型,而Klass就是java類在JVM中的映射。其中關(guān)于Oop和Klass體系,參考定義:[https://github.com/openjdk-mirror/jdk7u-hotspot/blob/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/oops/oop.hpp](https://github.com/openjdk-mirror/jdk7u-hotspot/blob/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/oops/oop.hpp);JVM中把我們上層可見(jiàn)的Java對(duì)象在底層實(shí)際上表示為兩部分,分別是oop和`klass`,其中`oop`專注于表示對(duì)象的實(shí)例數(shù)據(jù),不關(guān)心對(duì)象中的實(shí)例方法(包括繼承、重載等)所對(duì)應(yīng)的函數(shù)表。而klass則維護(hù)對(duì)象到j(luò)ava class及函數(shù)表的功能,它是java class及實(shí)現(xiàn)多態(tài)的基礎(chǔ)。這里列舉幾個(gè)基礎(chǔ)的Oop和Klass——Oop:
//定義了oops共同基類 typedef class oopDesc* oop; //表示一個(gè)Java類型實(shí)例 typedef class instanceOopDesc* instanceOop; //表示一個(gè)Java方法 typedef class methodOopDesc* methodOop; //定義了數(shù)組OOPS的抽象基類 typedef class arrayOopDesc* arrayOop; //表示持有一個(gè)OOPS數(shù)組 typedef class objArrayOopDesc* objArrayOop; //表示容納基本類型的數(shù)組 typedef class typeArrayOopDesc* typeArrayOop; //表示在Class文件中描述的常量池 typedef class constantPoolOopDesc* constantPoolOop; //常量池告訴緩存 typedef class constantPoolCacheOopDesc* constantPoolCacheOop; //描述一個(gè)與Java類對(duì)等的C++類 typedef class klassOopDesc* klassOop; //表示對(duì)象頭 typedef class markOopDesc* markOop;Klass:
//klassOop的一部分,用來(lái)描述語(yǔ)言層的類型 class Klass; //在虛擬機(jī)層面描述一個(gè)Java類 class instanceKlass; //專有instantKlass,表示java.lang.Class的Klass class instanceMirrorKlass; //表示methodOop的Klass class methodKlass; //最為klass鏈的端點(diǎn),klassKlass的Klass就是它自身 class klassKlass; //表示array類型的抽象基類 class arrayKlass; //表示constantPoolOop的Klass class constantPoolKlass;結(jié)合上述JVM層與java語(yǔ)言層,java對(duì)象的表示關(guān)系如下所示:
image.png
其中OopDesc是對(duì)象實(shí)例的基類(Java實(shí)例在VM中表現(xiàn)為instanceOopDesc),Klass是類信息的基類(Java類在VM中表現(xiàn)為instanceKlass),klassKlass則是對(duì)Klass本身的描述(Java類的class對(duì)象在VM中表現(xiàn)為klassKlass)。
?
有了對(duì)上述結(jié)構(gòu)的認(rèn)識(shí),對(duì)應(yīng)到內(nèi)存中的存儲(chǔ)區(qū)域,那么對(duì)象是怎么存儲(chǔ)的,就了比較清楚的認(rèn)識(shí):對(duì)象實(shí)例(instanceOopDesc)保存在堆上,對(duì)象的元數(shù)據(jù)(instanceKlass)保存在方法區(qū),對(duì)象的引用則保存在棧上。
因此,關(guān)于本小節(jié),對(duì)OOP-Klass Model的討論,可以用一句簡(jiǎn)潔明了的話來(lái)總結(jié)其意義:一個(gè)Java類在被VM加載時(shí),JVM會(huì)為其在方法區(qū)創(chuàng)建一個(gè)instanceKlass,來(lái)表示該類的class信息。當(dāng)我們?cè)诖a中基于此類用new創(chuàng)建一個(gè)新對(duì)象時(shí),實(shí)際上JVM會(huì)去堆上創(chuàng)建一個(gè)instanceOopDesc對(duì)象,該對(duì)象保含對(duì)象頭markWord和klass指針,klass指針指向方法區(qū)中的instanceKlass,markWord則保存一些鎖、GC等相關(guān)的運(yùn)行時(shí)數(shù)據(jù)。而在堆上創(chuàng)建的這個(gè)instanceOopDesc所對(duì)應(yīng)的地址會(huì)被用來(lái)創(chuàng)建一個(gè)引用,賦給當(dāng)前線程運(yùn)行時(shí)棧上的一個(gè)變量。
關(guān)于Mark Word
mark word是對(duì)象頭中較為神秘的一部分,也是本文講述的重點(diǎn),JDK oop.hpp源碼文件中,有幾行重要的注釋,揭示了32位機(jī)器和64位機(jī)器下,對(duì)象頭的格式:
// Bit-format of an object header (most significant first, big endian layout below): // // 32 bits: // -------- // hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object) // size:32 ------------------------------------------>| (CMS free block) // PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object) // // 64 bits: // -------- // unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object) // PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object) // size:64 ----------------------------------------------------->| (CMS free block) // // unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object) // JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object) // narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object) // unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)在oop.hpp源碼文件中,有對(duì)Oop基類中mark word結(jié)構(gòu)的定義,如下:
class oopDesc {friend class VMStructs;private:volatile markOop _mark;union _metadata {wideKlassOop _klass;narrowOop _compressed_klass;} _metadata;... }其中的mark word即上述 _mark字段,它在JVM中的表示類型是markOop, 部分關(guān)鍵源碼如下所示,源碼中展示了markWord各個(gè)字段的意義及占用大小(與機(jī)器字寬有關(guān)系),如GC分代年齡、鎖狀態(tài)標(biāo)記、哈希碼、epoch、是否可偏向等信息:
... class markOopDesc: public oopDesc {private:// Conversionuintptr_t value() const { return (uintptr_t) this; }public:// Constantsenum { age_bits = 4,lock_bits = 2,biased_lock_bits = 1,max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,cms_bits = LP64_ONLY(1) NOT_LP64(0),epoch_bits = 2};// The biased locking code currently requires that the age bits be// contiguous to the lock bits.enum { lock_shift = 0,biased_lock_shift = lock_bits,age_shift = lock_bits + biased_lock_bits,cms_shift = age_shift + age_bits,hash_shift = cms_shift + cms_bits,epoch_shift = hash_shift}; ...因?yàn)閷?duì)象頭信息只是對(duì)象運(yùn)行時(shí)自身的一部分?jǐn)?shù)據(jù),相比實(shí)例數(shù)據(jù)部分,頭部分屬于與業(yè)務(wù)無(wú)關(guān)的額外存儲(chǔ)成功。為了提高對(duì)象對(duì)堆空間的復(fù)用效率,Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存儲(chǔ)盡量多的信息,它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間。
對(duì)于上述源碼,mark word中字段枚舉意義解釋如下:
hash: 保存對(duì)象的哈希碼
age: 保存對(duì)象的分代年齡
biased_lock: 偏向鎖標(biāo)識(shí)位
lock: 鎖狀態(tài)標(biāo)識(shí)位
JavaThread*: 保存持有偏向鎖的線程ID
epoch: 保存偏向時(shí)間戳
鎖標(biāo)記枚舉的意義解釋如下:
<pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 13px; display: block; padding: 0px; margin: 0px; line-height: 1.42857; color: rgb(51, 51, 51); word-break: break-all; word-wrap: break-word; background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 4px;">locked_value = 0,//00 輕量級(jí)鎖
unlocked_value = 1,//01 無(wú)鎖
monitor_value = 2,//10 監(jiān)視器鎖,也叫膨脹鎖,也叫重量級(jí)鎖
marked_value = 3,//11 GC標(biāo)記
biased_lock_pattern = 5 //101 偏向鎖</pre>
實(shí)際上,markword的設(shè)計(jì)非常像網(wǎng)絡(luò)協(xié)議報(bào)文頭:將mark word劃分為多個(gè)比特位區(qū)間,并在不同的對(duì)象狀態(tài)下賦予不同的含義, 下圖是來(lái)自網(wǎng)絡(luò)上的一張協(xié)議圖。
?
image.png
?
上述協(xié)議字段正對(duì)應(yīng)著源碼中所列的枚舉字段,這里簡(jiǎn)要進(jìn)行說(shuō)明一下。
hash
對(duì)象的hash碼,hash代表的并不一定是對(duì)象的(虛擬)內(nèi)存地址,但依賴于內(nèi)存地址,具體取決于運(yùn)行時(shí)庫(kù)和JVM的具體實(shí)現(xiàn),底層由C++實(shí)現(xiàn),實(shí)現(xiàn)細(xì)節(jié)參考OpenJDK源碼。但可以簡(jiǎn)單的理解為對(duì)象的內(nèi)存地址的整型值。
age
對(duì)象分代GC的年齡。分代GC的年齡是指Java對(duì)象在分代垃圾回收模型下(現(xiàn)在JVM實(shí)現(xiàn)基本都使用的這種模型),對(duì)象上標(biāo)記的分代年齡,當(dāng)該年輕代內(nèi)存區(qū)域空間滿后,或者到達(dá)GC最達(dá)年齡時(shí),會(huì)被扔進(jìn)老年代等待老年代區(qū)域滿后被FullGC收集掉,這里的最大年齡是通過(guò)JVM參數(shù)設(shè)定的:-XX:MaxTenuringThreshold ,默認(rèn)值是15。那這個(gè)年齡具體是怎么計(jì)算的呢?
下圖展示了該年齡遞增的過(guò)程:
1. 首先,在對(duì)象被new出來(lái)后,放在Eden區(qū),年齡都是0
image
2. 經(jīng)過(guò)一輪GC后,B0和F0被回收,其它對(duì)象被拷貝到S1區(qū),年齡增加1,注:如果S1不能同時(shí)容納A0,C0,D0,E0和G0,將被直接丟入Old區(qū)
image
3. 再經(jīng)一輪GC,Eden區(qū)中新生的對(duì)象M0,P0及S1中的B1,E1,G1不被引用將被回收,而H0,K0,N0及S1中的A1,D1被拷貝到S2區(qū)中,對(duì)應(yīng)年齡增加1
image
4. 如此經(jīng)過(guò)2、3過(guò)濾循環(huán)進(jìn)行,當(dāng)S1或S2滿,或者對(duì)象的年齡達(dá)到最大年齡(15)后仍然有引用存在,則對(duì)象將被轉(zhuǎn)移至Old區(qū)。
鎖標(biāo)記:lock/biased_lock/epoch/JavaThread*
鎖標(biāo)記位,此鎖為重量級(jí)鎖,即對(duì)象監(jiān)視器鎖。Java在使用synchronized關(guān)鍵字對(duì)方法或塊進(jìn)行加鎖時(shí),會(huì)觸發(fā)一個(gè)名為“objectMonitor”的監(jiān)視器對(duì)目標(biāo)代碼塊執(zhí)行加鎖的操作。當(dāng)然synchronized方法和synchronized代碼塊的底層處理機(jī)制稍有不同。synchronized方法編譯后,會(huì)被打上“ACC_SYNCHRONIZED”標(biāo)記符。而synchronized代碼塊編譯之后,會(huì)在同步代碼的前后分別加上“monitorenter”和“monitorexit”的指令。當(dāng)程序執(zhí)行時(shí)遇到到monitorenter或ACC_SYNCHRONIZED時(shí),會(huì)檢測(cè)對(duì)象頭上的lock標(biāo)記位,該標(biāo)記位被如果被線程初次成功訪問(wèn)并設(shè)值,則置為1,表示取鎖成功,如果再次取鎖再執(zhí)行++操作。在代碼塊執(zhí)行結(jié)束等待返回或遇到異常等待拋出時(shí),會(huì)執(zhí)行monitorexit或相應(yīng)的放鎖操作,鎖標(biāo)記位執(zhí)行--操作,如果減到0,則鎖被完全釋放掉。關(guān)于objectMonitor的實(shí)現(xiàn)細(xì)節(jié),參考JDK源碼
注意,在jdk1.6之前,synchronized加鎖或取鎖等待操作最終會(huì)被轉(zhuǎn)換為操作系統(tǒng)中線程操作原語(yǔ),如激活、阻塞等。這些操作會(huì)導(dǎo)致CPU線程上下文的切換,開(kāi)銷較大,因此稱之為重量級(jí)鎖。但后續(xù)JDK版本中對(duì)其實(shí)現(xiàn)做了大幅優(yōu)化,相繼出現(xiàn)了輕量級(jí)鎖,偏向鎖,自旋鎖,自適應(yīng)自旋鎖,鎖粗化及鎖消除等策略。這里僅做簡(jiǎn)單介紹,不進(jìn)行展開(kāi)。
如圖所示,展示了這幾種鎖的關(guān)系:
image
輕量級(jí)鎖,如上圖所示,是當(dāng)某個(gè)資源在沒(méi)有競(jìng)爭(zhēng)或極少競(jìng)爭(zhēng)的情況下,JVM會(huì)優(yōu)先使用CAS操作,讓線程在用戶態(tài)去嘗試修改對(duì)象頭上的鎖標(biāo)記位,從而避免進(jìn)入內(nèi)核態(tài)。這里CAS嘗試修改鎖標(biāo)記是指嘗試對(duì)指向當(dāng)前棧中保存的lock record的線程指針的修改,即對(duì)biased_lock標(biāo)記做CAS修改操作。如果發(fā)現(xiàn)存在多個(gè)線程競(jìng)爭(zhēng)(表現(xiàn)為CAS多次失敗),則膨脹為重量級(jí)鎖,修改對(duì)應(yīng)的lock標(biāo)記位并進(jìn)入內(nèi)核態(tài)執(zhí)行鎖操作。注意,這種膨脹并非屬于性能的惡化,相反,如果競(jìng)爭(zhēng)較多時(shí),CAS方式的弊端就很明顯,因?yàn)樗鼤?huì)占用較長(zhǎng)的CPU時(shí)間做無(wú)謂的操作。此時(shí)重量級(jí)鎖的優(yōu)勢(shì)更明顯。
偏向鎖,是針對(duì)只會(huì)有一個(gè)線程執(zhí)行同步代碼塊時(shí)的優(yōu)化,如果一個(gè)同步塊只會(huì)被一個(gè)線程訪問(wèn),則偏向鎖標(biāo)記會(huì)記錄該線程id,當(dāng)該線程進(jìn)入時(shí),只用check 線程id是否一致,而無(wú)須進(jìn)行同步。鎖偏向后,會(huì)依據(jù)epoch(偏向時(shí)間戳)及設(shè)定的最大epoch判斷是否撤銷鎖偏向。
自旋鎖大意是指線程不進(jìn)入阻塞等待,而只是做自旋等待前一個(gè)線程釋放鎖。不在對(duì)象頭討論范圍之列,這里不做討論。
實(shí)例數(shù)據(jù)
實(shí)例數(shù)據(jù)instance Data是占用堆內(nèi)存的主要部分,它們都是對(duì)象的實(shí)例字段。那么計(jì)算這些字段的大小,主要思路就是根據(jù)這些字段的類型大小進(jìn)行求和的。字段類型的標(biāo)準(zhǔn)大小,如Java對(duì)象格式概述中表格描述的,除了引用類型會(huì)受CPU架構(gòu)及是否開(kāi)啟指針壓縮影響外,其它都是固定的。因此計(jì)算起來(lái)比較簡(jiǎn)單。但實(shí)際情其實(shí)并不這么簡(jiǎn)單,例如如下對(duì)象:
class People{int age = 20;String name = "Xiaoming"; } class Person extends People{boolean married = false;long birthday = 128902093242L;char tag = 'c';double sallary = 1200.00d; }Person對(duì)象實(shí)例數(shù)據(jù)的大小應(yīng)該是多少呢?這里假設(shè)使用64位機(jī)器,采用指針壓縮,則對(duì)象頭的大小為:8(_mark)+4(_klass) = 12
然后實(shí)例數(shù)據(jù)的大小為: 4(age)+4(name) + 8(birthday) + 8(sallary) + 2(tag) + 1(married) = 27
因此最終的對(duì)象本身大小為:12+27+1(padding) = 40字節(jié)
注意,為了盡量減少內(nèi)存空間的占用,這里在計(jì)算的過(guò)程中需要遵循以下幾個(gè)規(guī)則:
> <pre style="box-sizing: border-box; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 13px; display: block; padding: 0px; margin: 0px; line-height: 1.42857; color: rgb(51, 51, 51); word-break: break-all; word-wrap: break-word; background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 4px;">/** > > * 1: 除了對(duì)象整體需要按8字節(jié)對(duì)齊外,每個(gè)成員變量都盡量使本身的大小在內(nèi)存中盡量對(duì)齊。比如 int 按 4 位對(duì)齊,long 按 8 位對(duì)齊。 > > * 2:類屬性按照如下優(yōu)先級(jí)進(jìn)行排列:長(zhǎng)整型和雙精度類型;整型和浮點(diǎn)型;字符和短整型;字節(jié)類型和布爾類型,最后是引用類型。這些屬性都按照各自的單位對(duì)齊。 > > * 3:優(yōu)先按照規(guī)則一和二處理父類中的成員,接著才是子類的成員。 > > * 4:當(dāng)父類中最后一個(gè)成員和子類第一個(gè)成員的間隔如果不夠4個(gè)字節(jié)的話,就必須擴(kuò)展到4個(gè)字節(jié)的基本單位。 > > * 5:如果子類第一個(gè)成員是一個(gè)雙精度或者長(zhǎng)整型,并且父類并沒(méi)有用完8個(gè)字節(jié),JVM會(huì)破壞規(guī)則2,按照整形(int),短整型(short),字節(jié)型(byte),引用類型(reference)的順序,向未填滿的空間填充。 > > */</pre>最后計(jì)算引用類型字段的實(shí)際大小:"Xiaoming",按字符串對(duì)象的字段進(jìn)行計(jì)算,對(duì)象頭12字節(jié),hash字段4字節(jié),char[] 4字節(jié),共12+4+4+4(padding) = 24字節(jié),其中char[]又是引用類型,且是數(shù)組類型,其大小為:對(duì)象頭12+4(length) + 9(arrLength) * 2(char) +4(padding) = 40字節(jié)。
所以綜上所述,一個(gè)Person對(duì)象占用內(nèi)存的大小為104字節(jié)。
關(guān)于指針壓縮
一個(gè)比較明顯的問(wèn)題是,在64位機(jī)器上,如果開(kāi)啟了指針壓縮后,則引用只占用4個(gè)字節(jié),4字節(jié)的最大尋址空間為2^32=4GB, 那么如何保證能滿足尋址空間大于4G的需求呢?
開(kāi)啟指針壓縮后,實(shí)際上會(huì)壓縮的對(duì)象包括:每個(gè)Class的屬性指針(靜態(tài)成員變量)及每個(gè)引用類型的字段(包括數(shù)組)指針,而本地變量,堆棧元素,入?yún)?#xff0c;返回值,NULL這些指針不會(huì)被壓縮。在開(kāi)啟指針壓縮后,如前文源碼所述,markWord中的存儲(chǔ)指針將是_compressed_klass,對(duì)應(yīng)的類型是narrowOop,不再是wideKlassOop了,有什么區(qū)別呢?
wideKlassOop和narrowOop都指向InstanceKlass對(duì)象,其中narrowOop指向的是經(jīng)過(guò)壓縮的對(duì)象。簡(jiǎn)單來(lái)說(shuō),wideKlassOop可以達(dá)到整個(gè)尋址空間。而narrowOop雖然達(dá)不到整個(gè)尋址空間,但它面對(duì)也不再是個(gè)單純的byte地址,而是一個(gè)object,也就是說(shuō)使用narrowOop后,壓縮后的這4個(gè)字節(jié)表示的4GB實(shí)際上是4G個(gè)對(duì)象的指針,大概是32GB。JVM會(huì)對(duì)對(duì)應(yīng)的指針對(duì)象進(jìn)行解碼, JDK源碼中,oop.hpp源碼文件中定義了抽象的編解碼方法,用于將narrowOop解碼為一個(gè)正常的引用指針,或?qū)⒁幌抡5囊弥羔樉幋a為narrowOop:
// Decode an oop pointer from a narrowOop if compressed.// These are overloaded for oop and narrowOop as are the other functions// below so that they can be called in template functions.static oop decode_heap_oop_not_null(oop v);static oop decode_heap_oop_not_null(narrowOop v);static oop decode_heap_oop(oop v);static oop decode_heap_oop(narrowOop v);// Encode an oop pointer to a narrow oop. The or_null versions accept// null oop pointer, others do not in order to eliminate the// null checking branches.static narrowOop encode_heap_oop_not_null(oop v);static narrowOop encode_heap_oop(oop v);對(duì)齊填充
對(duì)齊填充是底層CPU數(shù)據(jù)總線讀取內(nèi)存數(shù)據(jù)時(shí)的要求,例如,通常CPU按照字單位讀取,如果一個(gè)完整的數(shù)據(jù)體不需要對(duì)齊,那么在內(nèi)存中存儲(chǔ)時(shí),其地址有極大可能橫跨兩個(gè)字,例如某數(shù)據(jù)塊地址未對(duì)齊,存儲(chǔ)為1-4,而cpu按字讀取,需要把0-3字塊讀取出來(lái),再把4-7字塊讀出來(lái),最后合并舍棄掉多余的部分。這種操作會(huì)很多很多,且很頻繁,但如果進(jìn)行了對(duì)齊,則一次性即可取出目標(biāo)數(shù)據(jù),將會(huì)大大節(jié)省CPU資源。
在hotSpot虛擬機(jī)中,默認(rèn)的對(duì)齊位數(shù)是8,與CPU架構(gòu)無(wú)關(guān),如下代碼中的objectAlignment:
// Try to get the object alignment (the default seems to be 8 on Hotspot, // regardless of the architecture).int objectAlignment = 8;try {final Class<?> beanClazz = Class.forName("com.sun.management.HotSpotDiagnosticMXBean");final Object hotSpotBean = ManagementFactory.newPlatformMXBeanProxy(ManagementFactory.getPlatformMBeanServer(),"com.sun.management:type=HotSpotDiagnostic",beanClazz);final Method getVMOptionMethod = beanClazz.getMethod("getVMOption", String.class);final Object vmOption = getVMOptionMethod.invoke(hotSpotBean, "ObjectAlignmentInBytes");objectAlignment = Integer.parseInt(vmOption.getClass().getMethod("getValue").invoke(vmOption).toString());supportedFeatures.add(JvmFeature.OBJECT_ALIGNMENT);} catch (Exception e) {// Ignore.}NUM_BYTES_OBJECT_ALIGNMENT = objectAlignment;可以看出,通過(guò)HotSpotDiagnosticMXBean.getVMOption("ObjectAlignmentBytes").getValue()方法可以拿到當(dāng)前JVM環(huán)境下的對(duì)齊位數(shù)。
注意,這里的HotSpotDiagnosticMXBean是JVM提供的JMX中一種可被管理的資源,即HotSpot信息資源。
使用SA Hotspot Debuger(HSDB)查看oops結(jié)構(gòu)
前文所述都是源碼+理論,其實(shí)Hotspot為我們提供了一種工具可以方便的用來(lái)查詢運(yùn)行時(shí)對(duì)象的Oops結(jié)構(gòu),即SA Hotspot Debuger,簡(jiǎn)稱HSDB. 其中SA指“Serviceability Agent”,它是一個(gè)JVM服務(wù)工具集的Agent,它原本是sun公司用來(lái)debug Hotspot的工具,現(xiàn)在開(kāi)放給開(kāi)發(fā)者使用,能夠查看Java對(duì)象的oops、查看類信息、線程棧信息、堆信息、方法字節(jié)碼和JIT編譯后的匯編代碼等。SA提供的入口在$JAVA_HOME/lib/sa-jdi.jar中,包含了很多工具,其中最常用的工具就是HSDB。
下面演示一下HSDB的使用——
1. 先準(zhǔn)備如下代碼并運(yùn)行:
public class Obj{private int age;private long height;private boolean married;private String name;private String addr;private String sex;...get/set } package yp.tools;/*** @author yepei* @date 2018/05/14* @description*/ public class HSDBTest {public static void main(String[] args) throws InterruptedException {Obj o = new Obj(20, 175, false, "小明", "浙江杭洲", "男");Thread.sleep(1000 * 3600);System.out.println(o);} }2. 執(zhí)行jps命令,獲取當(dāng)前運(yùn)行的Java進(jìn)程號(hào):
image
3. 啟動(dòng)HSDB,并添加目標(biāo)進(jìn)程:
`sudo java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB`image.png
可以看到當(dāng)前Java進(jìn)程中的線程信息:
image.png
雙擊指定線程,可以查看到當(dāng)前線程對(duì)象的Oop結(jié)構(gòu)信息,可以看到線程對(duì)象頭也是包含_mark和_metadata兩個(gè)協(xié)議字段的:
image.png
點(diǎn)擊上方的棧圖標(biāo),可以查詢當(dāng)前線程的棧內(nèi)存:
image.png
那么如何查看當(dāng)前線程中用戶定義的類結(jié)存儲(chǔ)信息呢?
先到方法區(qū)去看一下類信息吧
Tools——Class Browser,搜索目標(biāo)類
image.png
可以看到該類對(duì)應(yīng)的對(duì)象的各個(gè)字段的偏移量,最大的是36,String類型,意味著該對(duì)象本身的大小就是36+4 = 40字節(jié)。同時(shí),下方可以看到這個(gè)類相關(guān)的函數(shù)表、常量池信息。
要查看對(duì)象信息,從Tools菜單,打開(kāi)Object Histogram
?
image.png
在打開(kāi)的窗口中搜索目標(biāo)類:yp.tools.Obj
image.png
雙擊打開(kāi):
image.png
點(diǎn)擊Inspect查看該對(duì)象的Oop結(jié)構(gòu)信息:
image.png
如上圖所示即是對(duì)象Obj的Oop結(jié)構(gòu),對(duì)象頭包含_mark與代表class指針的_metadata。示例中的類沒(méi)有并發(fā)或鎖的存在,所以mark值是001,代表無(wú)鎖狀態(tài)。
除此之外,HSDB還有其它一些不錯(cuò)的功能,如查看反編譯信息、根據(jù)地址查找對(duì)象、crash分析、死鎖分析等。
總結(jié)
本文圍繞“計(jì)算Java對(duì)象占用內(nèi)存大小”這一話題,簡(jiǎn)要介紹了直接計(jì)算指定對(duì)象在內(nèi)存中大小的三種方法:使用Instrumentation、Unsafe或第三方工具(RamUsageEstimator)的方式,其中Instrumentation和Unsafe計(jì)算精確,但使用起來(lái)不太方便,Instrumentation需要以javaagent代理的方式啟動(dòng),而Unsafe只能計(jì)算指定對(duì)象的每個(gè)字段的地址起始位置偏移量,需要手工遞歸并增加padding才能完整計(jì)算對(duì)象大小,使用RamUsageEstimator可以很方便的計(jì)算對(duì)象本身或?qū)ο笠脴?shù)整體的大小,但其并非直接基于對(duì)象的真實(shí)內(nèi)存地址而計(jì)算的,而是通過(guò)已知JVM規(guī)則和數(shù)據(jù)類型的標(biāo)準(zhǔn)大小推算的,存在計(jì)算誤差的可能性。
為了揭開(kāi)Java對(duì)象在堆內(nèi)存中存儲(chǔ)格式的面紗,結(jié)合OpenJDK源碼,本文著重討論了Java對(duì)象的格式:對(duì)象頭、實(shí)例數(shù)據(jù)及對(duì)齊填充三部分。其中對(duì)象頭最為復(fù)雜,包含_mark、_klass以及_length(僅數(shù)組類型)的協(xié)議字段。其中的mark word字段較為復(fù)雜,甚至涉及了OOP-Klass模型、hash、gc、鎖的原理及指針壓縮等知識(shí)。
最后,從實(shí)踐的方面入手,介紹了JDK自帶的Hotspot Debuger工具——HSDB的使用,透過(guò)它能夠讓我們更直觀的查看運(yùn)行中的java對(duì)象在內(nèi)存中的存在形式和狀態(tài),如對(duì)象的oops、類信息、線程棧信息、堆信息、方法字節(jié)碼和JIT編譯后的匯編代碼等。
本文查詢了一些資料,并參考了OpenJDK源碼。可能會(huì)有些不正確的地方敬請(qǐng)指正,歡迎探討。
作者:阿里云云棲社區(qū)
鏈接:https://www.jianshu.com/p/9d729c9c94c4
來(lái)源:簡(jiǎn)書(shū)
簡(jiǎn)書(shū)著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處。
總結(jié)
以上是生活随笔為你收集整理的如何计算Java对象所占内存的大小的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 脐带血有必要保存吗
- 下一篇: Java应用CPU问题排查