轉載自:https://blog.csdn.net/zqz_zqz/article/details/70246212
對象結構
在HotSpot虛擬機中,對象在內存中存儲的布局可以分為3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。下圖是普通對象實例與數組對象實例的數據結構:
對象頭
HotSpot虛擬機的對象頭包括兩部分信息:
- markword
第一部分markword,用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啟壓縮指針)中分別為32bit和64bit,官方稱它為“MarkWord”。 - klass
對象頭的另外一部分是klass類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例. - 數組長度(只有數組對象有)
如果對象是一個數組, 那在對象頭中還必須有一塊數據用於記錄數組長度.
實例數據
實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。
對齊填充
第三部分對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起着占位符的作用。由於HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或者2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。
對象大小計算
要點
1. 在32位系統下,存放Class指針的空間大小是4字節,MarkWord是4字節,對象頭為8字節。
2. 在64位系統下,存放Class指針的空間大小是8字節,MarkWord是8字節,對象頭為16字節。
3. 64位開啟指針壓縮的情況下,存放Class指針的空間大小是4字節,MarkWord是8字節,對象頭為12字節。 數組長度4字節+數組對象頭8字節(對象引用4字節(未開啟指針壓縮的64位為8字節)+數組markword為4字節(64位未開啟指針壓縮的為8字節))+對齊4=16字節。
4. 靜態屬性不算在對象大小內。
以上內容轉載自http://blog.csdn.net/lihuifeng/article/details/51681146
補充:
HotSpot對象模型
HotSpot中采用了OOP-Klass模型,它是描述Java對象實例的模型,它分為兩部分:
- 類被加載到內存時,就被封裝成了klass,klass包含類的元數據信息,像類的方法、常量池這些信息都是存在klass里的,你可以認為它是java里面的java.lang.Class對象,記錄了類的全部信息;
- OOP(Ordinary Object Pointer)指的是普通對象指針,它包含MarkWord 和元數據指針,MarkWord用來存儲當前指針指向的對象運行時的一些狀態數據;元數據指針則指向klass,用來告訴你當前指針指向的對象是什么類型,也就是使用哪個類來創建出來的;
那么為何要設計這樣一個一分為二的對象模型呢?這是因為HotSopt JVM的設計者不想讓每個對象中都含有一個vtable(虛函數表),所以就把對象模型拆成klass和oop,其中oop中不含有任何虛函數,而klass就含有虛函數表,可以進行method dispatch。
HotSpot中,OOP-Klass實現的代碼都在/hotspot/src/share/vm/oops/路徑下,oop的實現為instanceOop 和 arrayOop,他們來描述對象頭,其中arrayOop對象用於描述數組類型。
以下就是oop.hhp文件中oopDesc的源碼,可以看到兩個變量_mark就是MarkWord,_metadata就是元數據指針,指向klass對象,這個指針壓縮的是32位,未壓縮的是64位;
volatile markOop _mark; //標識運行時數據 union _metadata { Klass* _klass; narrowKlass _compressed_klass; } _metadata; //klass指針
一個Java對象在內存中的布局可以連續分成兩部分:instanceOop(繼承自oop.hpp)和實例數據;
上圖可以看到,通過棧幀中的對象引用reference找到Java堆中的對象,再通過對象的instanceOop中的元數據指針klass來找到方法區中的instanceKlass,從而確定該對象的類型。
下面來分析一下,執行new A()的時候,JVM 做了什么工作。首先,如果這個類沒有被加載過,JVM就會進行類的加載,並在JVM內部創建一個instanceKlass對象表示這個類的運行時元數據(相當於Java層的Class對象)。初始化對象的時候(執行invokespecial A::),JVM就會創建一個instanceOopDesc對象表示這個對象的實例,然后進行Mark Word的填充,將元數據指針指向Klass對象,並填充實例變量。
元數據—— instanceKlass 對象會存在元空間(方法區),而對象實例—— instanceOopDesc 會存在Java堆。Java虛擬機棧中會存有這個對象實例的引用。
成員變量重排序
為了提高性能,每個對象的起始地址都對齊於8字節,當封裝對象的時候為了高效率,對象字段聲明的順序會被重排序成下列基於字節大小的順序:
- double (8字節) 和 long (8字節)
- int (4字節) 和 float (4字節)
- short (2字節) 和 char (2字節):char在java中是2個字節。java采用unicode,2個字節(16位)來表示一個字符。
- boolean (1字節) 和 byte (1字節)
- reference引用 (4/8 字節)
子類字段重復上述順序。
我們可以測試一下java對不同類型的重排序,使用jdk1.8,采用反射的方式先獲取到unsafe類,然后獲取到每個field在類里面的偏移地址,就能看出來了
測試代碼如下:
import java.lang.reflect.Field; import sun.misc.Contended; import sun.misc.Unsafe; public class TypeSequence { @Contended private boolean contended_boolean; private volatile byte a; private volatile boolean b; @Contended private int contended_short; private volatile char d; private volatile short c; private volatile int e; private volatile float f; @Contended private int contended_int; @Contended private double contended_double; private volatile double g; private volatile long h; public static Unsafe UNSAFE; static { try { @SuppressWarnings("ALL") Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); UNSAFE = (Unsafe) theUnsafe.get(null); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) throws NoSuchFieldException, SecurityException{ System.out.println("e:int \t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("e"))); System.out.println("g:double \t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("g"))); System.out.println("h:long \t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("h"))); System.out.println("f:float \t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("f"))); System.out.println("c:short \t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("c"))); System.out.println("d:char \t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("d"))); System.out.println("a:byte \t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("a"))); System.out.println("b:boolean\t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("b"))); System.out.println("contended_boolean:boolean\t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("contended_boolean"))); System.out.println("contended_short:short\t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("contended_short"))); System.out.println("contended_int:int\t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("contended_int"))); System.out.println("contended_double:double\t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("contended_double"))); } }
以上代碼運行結果如下
e:int 12 g:double 16 h:long 24 f:float 32 c:short 38 d:char 36 a:byte 40 b:boolean 41 contended_boolean:boolean 170 contended_short:short 300 contended_int:int 432 contended_double:double 568
除了int字段跑到了前面來了,還有兩個添加了contended注解的字段外,其它字段都是按照重排序的順序,類型由最長到最短的順序排序的;
對象頭對成員變量排序的影響
有的童鞋疑惑了,為啥int跑到前面來了呢?這是因為int字段被提升到前面填充對象頭了,對象頭有12個字節,會優先在字段中選擇一個或多個能夠將對象頭填充為16個字節的field放到前面,如果填充不滿,就加上padding,上面的例子加上一個4字節的int,正好是16字節,地址按8字節對齊;
擴展contended對成員變量排序的影響
那么contended注解呢?這個注解是為了解決cpu緩存行偽共享問題的,cpu緩存偽共享是並發編程性能殺手,不知道什么是偽共享的可以查看我前面寫的LongAdder類的源碼解讀 或者《java 中的鎖 – 偏向鎖、輕量級鎖、自旋鎖、重量級鎖》這篇文章都有講到,加了contended注解的字段會按照聲明的順序放到末尾,contended注解如果是用在類的field上會在該field前面插入128字節的padding,如果是用在類上則會在類所有field的前后都加上128字節的padding。