java對象的對齊規則

零、註記

本文是一次討論的流水賬,旨在講明原理就行了,行文大家不要抱太大的希望。

另外,特別重要的是,本文是基於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 sampleshttp://hg.openjdk.java.net/code-tools/jol/file/tip/jol-samples/src/main/java/org/openjdk/jol/samples/

jol sourcecodehttp://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呢,下面會細說)
  • 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的對齊規則

  1. 對象內的對齊規則:對象頭、field和padding和#program pack第一條規則一致;
    1. 注意:對象頭默認是沒有padding的,是否需要padding,要看是32bit機器還是64bit機器,以及對象頭後面跟的field size,這是#program pack的對齊規則導致的padding,而不是對象頭導致padding,這一點看過很多人討論錯了。
  2. 對象間的對齊規則:不一樣,java的對象間對齊如果是按照8byte,那就是8byte,不會再像#program pack中還需要和對象內最大size的屬性比較;
    1. 解釋: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的對齊規則。可能有遺漏,甚至有錯誤,歡迎指正,歡迎留言討論!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章