零、註記
本文是一次討論的流水賬,旨在講明原理就行了,行文大家不要抱太大的希望。
另外,特別重要的是,本文是基於hotspot來討論的,不同的java虛擬機是有不同的,這一點,一定要注意。
一、什麼是對象的內存佈局
簡單一句話:對象實例在jvm堆內存中存放的結構。就是隨便實例化一個對象new Object(),他在堆內存裏面是怎麼放置的。
看下面這個jol工具給出的java.math.BigInteger內存佈局的例子:一個對象的內存佈局包含了對象頭object header、實例數據域和對齊填充alignment padding(可能有,可能沒有,下面再細說)。
***** 64-bit VM, compressed references enabled: ***************************
java.math.BigInteger object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int BigInteger.signum N/A
16 4 int[] BigInteger.mag N/A
20 4 int BigInteger.bitCount N/A
24 4 int BigInteger.bitLength N/A
28 4 int BigInteger.lowestSetBit N/A
32 4 int BigInteger.firstNonzeroIntNum N/A
36 4 (loss due to the next object alignment)
Instance size: 40 bytes (estimated, the sample instance is not available)
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
二、查看對象內存佈局的工具
1. openjdk jol
openjdk官網給了一個查看對象內存佈局的工具,jol(java object layout):http://openjdk.java.net/projects/code-tools/jol/
怎麼拿呢?openjdk給了maven的依賴:
Use as Library Dependency
OpenJDK Community semi-regularly pushes the releases to Maven Central. Therefore, you can use it right away by setting up the Maven dependency:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>put-the-version-here</version>
</dependency>
It is a good idea to review JOL Samples and CLI tools source before using the tool at its full capacity as the library.
怎麼用呢?上面給的jol的鏈接頁面,最下面官方給了jol samples的鏈接,使用極其簡單,就是一個ClassLayout就沒了。示例就懶得給了,看samples吧。
jol samples:http://hg.openjdk.java.net/code-tools/jol/file/tip/jol-samples/src/main/java/org/openjdk/jol/samples/
jol sourcecode:http://central.maven.org/maven2/org/openjdk/jol/
如果不想看samples呢?這篇參考文章給了用例和講解《JDK之JVM中Java對象的頭部佔多少byte》:https://my.oschina.net/u/2518341/blog/1838006
那如果不想用jol工具怎麼辦呢?臥槽,我好難啊。。。
2. sun.misc.Unsafe
- sun.misc.Unsafe.objectFieldOffset方法獲取第一個field的偏移地址(弊端:當對象頭後面有padding的時候,你看不出來,什麼時候有padding呢,下面會細說)
- unsafe怎麼用呢?《Understanding sun.misc.Unsafe》:https://dzone.com/articles/understanding-sunmiscunsafe
- JDK8及之前,是用的sun.misc.Unsafe
- JDK9有兩個Unsafe,除了sun.misc.Unsafe還提供了jdk.internal.misc.Unsafe,但是jdk.internal.misc.Unsafe不像sun.misc.Unsafe是可以通過反射使用的,實際上目前在JDK9以後的版本中,sun.misc.Unsafe中組合了jdk.internal.misc.Unsafe的實例,實際上sun.misc.Unsafe是一個簡單包裝,你可以自己翻翻源碼。
三、#program pack(n)
C、C++裏面的對齊規則,默認32bit機器是4byte對齊,64比特機器是8byte對齊。那如果想修改默認對齊規則呢?在源碼開頭寫上#program pack(n)聲明就行了。
#program pack(n),n必須是2的次方,這個聲明的作用就是告訴編譯器使用的對齊方式是n(不管對齊方式n是1byte、2byte、4byte、8byte、16byte還是多少,對齊規則不變,都如下所示),就不再使用默認的對齊方式。
在C、C++裏面的對齊規則如下(鏈接:https://baike.baidu.com/item/%E5%86%85%E5%AD%98%E5%AF%B9%E9%BD%90/9537460?fr=aladdin):
規則:
1、數據成員對齊規則:結構(struct)(或聯合(union))的數據成員,第一個數據成員放在offset爲0的地方,以後每個數據成員的對齊按照#pragma pack指定的數值和這個數據成員自身長度中,比較小的那個進行。
2、結構(或聯合)的整體對齊規則:在數據成員完成各自對齊之後,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大數據成員長度中,比較小的那個進行。
3、結合1、2可推斷:當#pragma pack的n值等於或超過所有數據成員長度的時候,這個n值的大小將不產生任何效果。
衆所周知,jvm是C、C++寫的,那java默認的8byte對齊規則,和這個一樣嗎?
結果是有相同的部分,也有不同的部分:
- 對象內的對齊規則:對象頭、field和padding和第一條規則一致;
- 對象間的對齊規則:不一樣,java默認就是對象間8byte對齊,不管對象頭的size、fields中最大的field的size是否小於8byte,如果是16byte,那對象之間就是按照16byte對齊,一樣的也不管對象頭的size、fields中最大的field的size是否小於16byte。
第五部分,通過jol會給出兩條規則的示例,更多的下面部分再細說。
補充:
爲什麼要對齊?1. 效率;2. 有些OS平臺有要求。
參考鏈接:《Data alignment: Straighten up and fly right》https://developer.ibm.com/articles/pa-dalign/
四、java對象的內存佈局
java對象的內存佈局,在周志明的《深入理解java虛擬機》第二章有講解,三個結構:
- 對象頭:mark word和元數據指針,注意如果是數組對象,其對象頭除了mark word和元數據指針之外,還有個4byte int類型的length,本文未討論數組類型,對象頭加4byte,其他規則不變。
- 實例數據域
- 對齊填充padding
1. 對象頭
如果你理解hotspot的oop-klass二分模型,那這裏你一定了解過。jvm中對象的對象頭分爲兩部分,mark work和元數據指針。
在hotspot的oop.hpp文件中class oopDesc描述了對象頭,鏈接和源碼如下:
src/share/vm/oops/oop.hpp:http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/oops/oop.hpp
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
其中,markOop _mark官方文檔叫做mark word,union _metadata中的Klass* _klass是元數據指針,指向持久代或者metaspace中每個類的元數據,也就是java.lang.Class類實例訪問的jvm中該類的數據結構。
mark word的內存結構及源碼如下,其中在32bit機器上是佔4byte,在64bit機器上是8byte,不管是否開啓壓縮指針-XUseCompressOops。是否開啓壓縮指針,影響的是元數據指針_klass的size。
src/share/vm/oops/markOop.hpp:http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/oops/markOop.hpp
// 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)
//
// - hash contains the identity hash value: largest value is
// 31 bits, see os::random(). Also, 64-bit vm's require
// a hash value no bigger than 32 bits because they will not
// properly generate a mask larger than that: see library_call.cpp
// and c1_CodePatterns_sparc.cpp.
//
// - the biased lock pattern is used to bias a lock toward a given
// thread. When this pattern is set in the low three bits, the lock
// is either biased toward a given thread or "anonymously" biased,
// indicating that it is possible for it to be biased. When the
// lock is biased toward a given thread, locking and unlocking can
// be performed by that thread without using atomic operations.
// When a lock's bias is revoked, it reverts back to the normal
// locking scheme described below.
//
// Note that we are overloading the meaning of the "unlocked" state
// of the header. Because we steal a bit from the age we can
// guarantee that the bias pattern will never be seen for a truly
// unlocked object.
//
// Note also that the biased state contains the age bits normally
// contained in the object header. Large increases in scavenge
// times were seen when these bits were absent and an arbitrary age
// assigned to all biased objects, because they tended to consume a
// significant fraction of the eden semispaces and were not
// promoted promptly, causing an increase in the amount of copying
// performed. The runtime system aligns all JavaThread* pointers to
// a very large value (currently 128 bytes (32bVM) or 256 bytes (64bVM))
// to make room for the age bits & the epoch bits (used in support of
// biased locking), and for the CMS "freeness" bit in the 64bVM (+COOPs).
//
// [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread
// [0 | epoch | age | 1 | 01] lock is anonymously biased
//
// - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
// [ptr | 00] locked ptr points to real header on stack
// [header | 0 | 01] unlocked regular object header
// [ptr | 10] monitor inflated lock (header is wapped out)
// [ptr | 11] marked used by markSweep to mark an object
// not valid at any other time
//
// We assume that stack/thread pointers have the lowest two bits cleared.
在64bit機器上,元數據指針的大小是會受壓縮指針是否開啓的影響的。32bit機器,元數據指針大小4byte,在64byte機器上,默認是開啓壓縮指針的(-XUseCompressOops),開啓之後,元數據指針也是4byte,關閉則佔8byte。
補充:
在Scott oaks寫的《java性能權威指南》第八章8.22節提到了當heap size堆內存大於32GB是用不了壓縮指針的,對象引用會額外佔用20%左右的堆空間,也就意味着要38GB的內存才相當於開啓了指針壓縮的32GB堆空間。
這是爲什麼呢?看下面引用中的紅字(來自openjdk wiki:https://wiki.openjdk.java.net/display/HotSpot/CompressedOops)。32bit最大尋址空間是4GB,開啓了壓縮指針之後呢,一個地址尋址不再是1byte,而是8byte,因爲不管是32bit的機器還是64bit的機器,java對象都是8byte對齊的,而類是java中的基本單位,對應的堆內存中都是一個一個的對象。
Compressed oops represent managed pointers (in many but not all places in the JVM) as 32-bit values which must be scaled by a factor of 8 and added to a 64-bit base address to find the object they refer to. This allows applications to address up to four billion objects (not bytes), or a heap size of up to about 32Gb. At the same time, data structure compactness is competitive with ILP32 mode.
爲什麼堅持8byte對齊呢?Scott oaks在書上給了理由:
2. 實例數據域
實例數據域緊跟在對象頭之後。一個類沒有field,就不需要實例數據域,有那就按照第三部分#program pack對齊規則的第一條放在堆內存中。這部分在第五部分,給出jol的示例詳細討論其規則。
3. padding
對象內可以有padding也可以沒有;對象間默認按照8byte對齊,對齊則不需要padding,否則需要padding補充8byte對齊,也在第五部分根據jol的示例詳細討論其規則。
五、java的對齊規則
- 對象內的對齊規則:對象頭、field和padding和#program pack第一條規則一致;
- 注意:對象頭默認是沒有padding的,是否需要padding,要看是32bit機器還是64bit機器,以及對象頭後面跟的field size,這是#program pack的對齊規則導致的padding,而不是對象頭導致padding,這一點看過很多人討論錯了。
- 對象間的對齊規則:不一樣,java的對象間對齊如果是按照8byte,那就是8byte,不會再像#program pack中還需要和對象內最大size的屬性比較;
- 解釋:java默認就是對象間8byte對齊,不管對象頭的size、fields中最大的field的size是否小於8byte;如果是16byte,那對象之間就是按照16byte對齊,一樣的也不管對象頭的size、fields中最大的field的size是否小於16byte。
補充:前四個例子均來源於jol官網示例,我感覺足夠說明了,就懶得動手了,第五個需要動手搞一下,但是我也沒動手 ,直接用了討論時別人貼過來問我的例子,哈哈哈哈
例一、32bit機器的對象內存佈局(默認對象間8byte對齊)
***** 32-bit VM: **********************************************************
$ java -jar jol-cli/target/jol-cli.jar estimates java.math.BigInteger
java.math.BigInteger object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 8 (object header) N/A
8 4 int BigInteger.signum N/A
12 4 int[] BigInteger.mag N/A
16 4 int BigInteger.bitCount N/A
20 4 int BigInteger.bitLength N/A
24 4 int BigInteger.lowestSetBit N/A
28 4 int BigInteger.firstNonzeroIntNum N/A
Instance size: 32 bytes (estimated, the sample instance is not available)
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
32bit機器上,對象頭8byte,其中,mark word 4byte,元數據指針4byte。
java.math.BigInteger有6個成員屬性field,都是引用類型每個4byte,按照4byte對齊,對象頭0~7byte,後面的每個field的起始地址都是4byte的整倍數,不需要額外的padding來對齊。
對象大小:object header(mark word+metadata klass)+6*field = 8byte+4*6byte =32byte。
因爲32byte % 8byt e= 0,所以對象間不需要額外的padding來幫助對齊。
例二、64bit機器的對象內存佈局(默認對象間8byte對齊,沒有開啓指針壓縮)
***** 64-bit VM: **********************************************************
java.math.BigInteger object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 16 (object header) N/A
16 8 int[] BigInteger.mag N/A
24 4 int BigInteger.signum N/A
28 4 int BigInteger.bitCount N/A
32 4 int BigInteger.bitLength N/A
36 4 int BigInteger.lowestSetBit N/A
40 4 int BigInteger.firstNonzeroIntNum N/A
44 4 (loss due to the next object alignment)
Instance size: 48 bytes (estimated, the sample instance is not available)
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
64bit機器,未開啓指針壓縮,對象頭16byte,其中mark word 8byte,元數據指針8byte。
java.math.BigInteger有6個成員屬性,其中int[] BigInteger.mag是引用類型,未開啓指針壓縮佔8byte,剩下5個int各佔4byte,對象頭0~15byte,mag 8byte對齊,佔16~23,剩下的5個4byte對齊,對象內不需要額外的padding對齊。
對象大小:16byte+8byte+5*4byte = 44byte。
因爲44byte%8byte = 4byte,所以按照8byte對齊,對象間還需要額外的4byte來幫助對齊。
所以,對象真正佔用的內存是44byte + 4byte = 48byte。
例三、64bit機器並開啓壓縮指針的對象內存佈局(默認對象間8byte對齊)
***** 64-bit VM, compressed references enabled: ***************************
java.math.BigInteger object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int BigInteger.signum N/A
16 4 int[] BigInteger.mag N/A
20 4 int BigInteger.bitCount N/A
24 4 int BigInteger.bitLength N/A
28 4 int BigInteger.lowestSetBit N/A
32 4 int BigInteger.firstNonzeroIntNum N/A
36 4 (loss due to the next object alignment)
Instance size: 40 bytes (estimated, the sample instance is not available)
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
64bit機器開啓指針壓縮,對象頭12byte,其中mark word 8byte,元數據指針4byte。
java.math.BigInteger 6個成員屬性,因爲開啓了指針壓縮,所以例二中的int[] BigInteger.mag不再佔用8byte,而是4byte,其他5個成員屬性int各佔4byte。對象頭12byte,每個成員屬性按照4byte對齊,對象內不需要額外的padding來幫助對齊。
對象大小:12byte + 6*4byte = 36byte。
因爲36byte % 8byte = 4byte,按照8byte對齊,所以對象間還需要額外的4byte來幫助對齊。
所以對象真正佔用的內存大小:36byte + 4byte = 40byte。
例四、64bit機器並開啓壓縮指針、修改按照16byte對齊
***** 64-bit VM, compressed references enabled, 16-byte align: ************
java.math.BigInteger object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int BigInteger.signum N/A
16 4 int[] BigInteger.mag N/A
20 4 int BigInteger.bitCount N/A
24 4 int BigInteger.bitLength N/A
28 4 int BigInteger.lowestSetBit N/A
32 4 int BigInteger.firstNonzeroIntNum N/A
36 12 (loss due to the next object alignment)
Instance size: 48 bytes (estimated, the sample instance is not available)
Space losses: 0 bytes internal + 12 bytes external = 12 bytes total
對象按16byte對齊,也就意味着對象放到堆中的時候,其起始地址模16必須爲0,即address % 16byte = 0。
對象頭和例三一樣,12byte。
開啓指針壓縮,對象內6個屬性也都是4byte,對象內各個屬性按照4byte對齊,不需要額外的padding。
對象大小:12byte + 6*4byte = 36byte。
因爲36byte % 16byte = 4byte。
所以爲了模16byte爲0,對象間還需要額外的12byte來幫助對齊(12byte + 4byte = 16byte)。
例五、對象頭是否需要padding
這個對比例子中,java.lang.Integer對比java.lang.Long。Integer中只有一個private final int value的對象,Long中只有一個private final long value屬性(就是JDK的源碼)。
環境:64bit機器開啓壓縮指針,默認按照8byte對齊。
Integer的例子中,對象頭12byte,屬性int value 4byte並按照4byte對齊,所以最終對象大小16byte。對象內的屬性int value起始地址12,所以對象內不需要額外的padding,對象大小16byte,是8byte的整倍數,所以對象間也不需要額外的padding來對其。
Long的例子中,對象頭12byte,long value是8byte並按照8byte對齊,而對象頭12byte從0byte~11byte,所以Long的實際存放地址是16~23byte,在long value和對象之間需要4byte的padding(但這個padding不是對象頭的,是後面的long value根據第一條對齊規則導致的,Integer例子中int value是4byte對齊,就不需要額外的padding)。填充之後,對象大小爲24byte,是默認對齊8byte的整倍數,對象間不需要額外的padding。
六、附註
以上就是根據表象推斷出來的java的對齊規則。可能有遺漏,甚至有錯誤,歡迎指正,歡迎留言討論!