天天看點

如何精确地測量java對象的大小-底層instrument API

關于java對象的大小測量,網上有很多例子,大多數是申請一個對象後開始做gc,後對比前後的大小,不過這樣,雖然說這樣測量對象的大小是可行的,不過未必是完全準确的,因為過程中包含對象本身的開銷,也許你運氣好,正好能碰上,差不多,不過這種測試往往顯得十分的笨重,因為要寫一堆代碼才能測試一點點東西,而且隻能在本地測試玩玩,要真正測試實際的系統的對象大小這樣可就不行了,本文說說java一些比較偏底層的知識,如何測量對象大小,java其實也是有提供方法的。注意:本文的内容僅僅針對于hotspot

vm,如果你以前不知道jvm的對象大小怎麼測量,而又很想知道,跟我一步一步做一遍你就明白了。

首先,我們先寫一段大家可能不怎麼寫或者認為不可能的代碼:一個類中,幾個類型都是private類型,沒有public方法,如何對這些屬性進行讀寫操作,看似不可能哦,為什麼,這違背了面向對象的封裝,其實在必要的時候,留一道後面可以使得語言的生産力更加強大,對象的序列化不會因為沒有public方法就無法儲存成功吧,ok,我們簡單寫段代碼開個頭,逐漸引入到怎麼樣去測試對象的大小,一下代碼非常簡單,相信不用我解釋什麼:

代碼最基本的意思就是:執行個體化一個nodetest1這個類的執行個體,然後取出兩個屬性,分别乘以2,然後再輸出,相信大家會認為這怎麼可能,nodetest1根本沒有public方法,代碼就在這裡,将代碼拷貝回去運作下就ok了,ok,現在不說這些了,運作結果為:

26

42

為什麼可以取到,是每個屬性都留了一道門,主要是為了自己或者外部接入的友善,相信看代碼自己仔細的朋友,應該知道門就在:field.setaccessible(true);,代表這個域的通路被打開,好比是一道後門打開了,呵呵,上面的方法如果不設定這個,就直接報錯。

看似和對象大小沒啥關系,不過這隻是抛磚引玉,因為我們首先要拿到對象的屬性,才能知道對象的大小,對象如果沒有提供public方法我們也要知道它有哪些屬性,是以我們後面多半會用到這段類似的代碼哦!

對象測量大小的方法關鍵為java提供的(1.5過後才有):java.lang.instrument.instrumentation,它提供了豐富的對結構的等各方面的跟蹤和對象大小的測量的api(本文隻闡述對象大小的測量方法),于是乎我心喜了,不過比較惡心的是它是執行個體化類:sun.instrument.intrumentationimpl是sun開頭的,這個鬼東西有點不好搞,翻開源碼構造方法是private類型,沒有任何getinstance的方法,寫這個類幹嘛?看來這個隻能被jvm自己給初始化了,那麼怎麼将它自己初始化的東西取出來用呢,唯一能想到的就是agent代理,那麼我們先抛開代理,首先來寫一個簡單的對象測量方法:

//步驟1(先建立一個用于測試對象大小的處理類):

//步驟2:上面我們寫好了agent的代碼,此時我們要将上面這個類編譯後打包為一個jar檔案,并且在其包内部的meta-inf/manifest.mf檔案中增加一行:premain-class: mysizeof代表執行代理的全名,這裡的類名稱是沒有package的,如果你有package,那麼就寫全名,我們這裡假設打包完的jar包名稱為agent.jar(打包過程這裡簡單闡述,就不細說了),ok,繼續向下走:

//步驟3:編寫測試類,測試類中寫:

下一步準備運作,運作前我們準備初步估算下結果是什麼,目前我是在32bit模式下運作jvm(注意,不同位數的jvm參數設定不一樣,對象大小也不一樣大)。

1、首先看integer對象,在32bit模式下,_class區域占用4byte,_mark區域占用最少4byte,是以最少8byte頭部,integer内部有一個int類型的資料,占4個byte,是以此時為8+4=12,java預設要求按照8byte對象對其,是以對其到16byte,是以我們理論結果第一個應該是16;

2、再看string,長度為1,string對象内部本身有4個非靜态屬性(靜态屬性我們不計算空間,因為所有對象都是共享一塊空間的),4個非靜态屬性中,有offset、count、hash為int類型,分别占用4個byte,char value[]為一個指針,指針的大小在bit模式下或64bit開啟指針壓縮下預設為4byte,是以屬性占用了16byte,string本身有8直接頭部,是以占用了24byte;其次,一個string包含了子對象char數組,數組對象和普通對象的差別是需要用一個字段來儲存數組的長度,是以頭部變成12位元組,java中一個char采用utf-16編碼,占用2個byte,是以是14byte,對其到16byte,24+16=40byte;

3、第三個在第二個基礎上已經分析,就是16byte大小

也就是理論結果是:16、40、16;

//步驟3:現在開始運作代碼:

運作代碼前需要保證classpath把剛才的agent.jar包含進去:

d:\>javac testsize.java

d:\>java -javaagent:agent.jar testsize

16

24

第一個和第三個結果一緻了,不過奇怪了,第二個怎麼是24,不是40,怎麼和理論結果偏差這麼大,再回到理論結果中,有一個24曾經出現過,24是指string而不包含char數組的空間大小,那麼這麼算還真是對的,可見,java預設提供的方法隻能測量對象目前的大小,如果要測量這個對象實際的大小(也就是包含了子對象,那麼就需要自己寫算法來計算了,最簡單的方法就是遞歸,不過遞歸一項是我不喜歡用的,無意中在一個地方看到有人用棧寫了一個代碼寫得還不錯,自己稍微改了下,就是下面這種了)。

ok,通過上面已經可以看出,保持了原有方法,因為深度遞歸畢竟比較慢,我們有些時候可以選擇到底用那一種:

回到步驟重新做一次:

1、編譯agent

2、打包class,并修改meta-inf/manifest.mf檔案中增加一行:premain-class: mysizeof

3、修改測試類:

4、設定環境變量開始運作(如果已經設定好了就無需重複設定):

40

這個結果是我們想要的了,看來這個測試是靠譜的,面對理論和測試結果,以及上面所謂的對齊方法,大家可以自己編寫一些類的對象來測試大小看時候和實際的保持一緻;

最後,文章補充一些:

1、對象采用8位元組對齊的方式是不論32bit還是64bit都是一樣的

2、java在64bit模式下開啟指針壓縮,比32bit模式下,頭部會大4byte(_mark區域變成8byte,_class區域被壓縮),如果沒有開啟指針壓縮,頭部會大8byte(_mark和_class都會變成8byte),jdk1.6推出參數-xx:+usecompressedoops,在32g記憶體一下預設會自動打開這個參數,如下:

簡單計算一個,在指針壓縮的情況下,一個new string("a");這個對象的空間大小為:12位元組頭部+4*4 = 28位元組對齊到32位元組,然後c所指向的char數組頭部比普通對象多4個byte來存放長度,12+4+2byte的字元=16,也就是48個byte,其實即使你new string()也會占這麼大的空間,因為有對齊,如果字元的長度是8個,那麼就是12+4+16=32,也就是有64byte;

如果不開啟指針壓縮再算算:頭部變成16byte + 4*3個int資料 + 8(1個指針) = 36對齊到40byte,對應的char數組的頭部變成16+4 + 2 = 22對齊到24byte,40+24=64,也就是隻有一個字元或者0個字元都會對齊到64byte,是以,你懂的,參數該怎麼調,代碼該怎麼寫,如果長度為8個字元的那麼後面部分就會變成16+4+16=36對齊到40byte,40+40=80byte,也就是說,抛開其他的引用空間(比如通過數組或集合類引用),如果你有10來個string,每個大小就裝8個字元,就會有1k的大小,你的代碼裡頭有多少?呵呵!

這些不是我說的,這些是一種計算方法,而且這個計算結果隻會少不會多,因為代碼運作過程中,一些對象的頭部會伸展,_mark區域裝不下會用外部的空間來存放,是以官方給出的說明也是,最少會占用多少位元組,絕對不會說隻占用多少位元組。

ok,說得挺吓人的,不過寫代碼還是不要怕,不過就這些而言,隻是說明java是如何浪費空間的,不要一味使用一些進階的東西,在必要的時候,考慮性能還是有很大的空間,類似集合類以及多元數組,前面的引用其實和資料一點關系都沒有,但是占用的空間比資料本身都要大很多。

本文隻是通過一種方式讓大家知道如何去測量對象大小,同時知道一個java對象如何開銷記憶體,開銷而且很大,是以回過頭來說,即使java并不看重性能和空間,不過如果你的代碼寫得好同樣會跑得更加快。