基本功:初探對象的內存佈局

java中我們隨處可見的都是對象,而對象成爲我們與計算機內核交換的主要載體,使用起來也非常簡單,然而一個對象是如何被JVM創建的卻是極其的複雜,它要經歷類加載機制、分片內存以及設置對象頭的內存佈局。

下面講介紹下Hotspot JVM下新建對象需要基本過程。

前言-字節碼

new語句最會被編譯而成的字節碼,而它將包含用來請求內存指令。

public class ObjectSizeMain{
	public static void main(String []args){
		ObjectSizeMain obj=new ObjectSizeMain();
	}
	
}

C:\Users\Administrator\Desktop>javap -v ObjectSizeMain.class
Classfile /C:/Users/Administrator/Desktop/ObjectSizeMain.class
  Last modified 2019-6-1; size 290 bytes
  MD5 checksum 1635999ab99de05314d8568fbaaf6900
  Compiled from "ObjectSizeMain.java"
public class ObjectSizeMain
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#13         // java/lang/Object."<init>":()V
   #2 = Class              #14            // ObjectSizeMain
   #3 = Methodref          #2.#13         // ObjectSizeMain."<init>":()V
   #4 = Class              #15            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               SourceFile
  #12 = Utf8               ObjectSizeMain.java
  #13 = NameAndType        #5:#6          // "<init>":()V
  #14 = Utf8               ObjectSizeMain
  #15 = Utf8               java/lang/Object
{
  public ObjectSizeMain();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class ObjectSizeMain
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8

}
SourceFile: "ObjectSizeMain.java"

重點關注下紅色部分內容,它透露了一些信息:

  1. new 指令,用來請求內存及用來調用構造器的 invokespecial 指令
  2. invokespecial指令,用於調用私有實例方法、構造器,以及使用super關鍵字調用父類的實例方法或構造器,和所實現接口的默認方法

關於字節碼可以參閱《實戰JAVA虛擬機  JVM故障診斷與性能優化》中class文件結構的章節

1. 類加載機制

JVM首先檢查一個new指令的參數是否能在常量池中定位到一個符號引用,並且檢查該符號引用代表的類是否已被加載、解析和初始化過(實際上就是在檢查new的對象所屬的類是否已經執行過類加載機制)。如果沒有,先進行類加載機制加載類。可參閱你忽略的ClassLoader

2. 分配堆內存

簡單理解爲:確定對象內存大小並從Java堆中劃分出來

  • 在類加載完成後,一個對象所需的內存大小就可以完全確定了
  • 爲對象分配空間,即把一塊兒確定大小的內存從Java堆中劃分出來

內存分配兩種方式

Java堆內存是否規整,取決於GC收集器的算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮"),值得注意的是,複製算法內存也是規整的,然後選擇對應的方式。

指針碰撞

適用場合:堆內存規整(即沒有內存碎片)的情況下

原理:用過的內存全部整合到一邊,沒有用過的內存放在另一邊,中間有一個分界值指針,只需要向着沒用過的內存方向將該指針移動對象內存大小位置即可

GC收集器:Serial、ParNew

空閒列表

適用場合:堆內存不規整的情況下

原理:虛擬機會維護一個列表,該列表中會記錄哪些內存塊是可用的,在分配的時候,找一塊兒足夠大的內存塊兒來劃分給對象實例(這一塊兒可以類比memcached的slab模型),最後更新列表記錄。

GC收集器:CMS

內存分配併發問題

堆內存是各個線程的共享區域,所以在操作堆內存的時候,需要處理併發問題。處理的方式有兩種:

CAS+失敗重試

做法與AtomicInteger的getAndSet(int newValue)方法的實現方式類似

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAndSet(int newValue) {
    for (;;) {
        int current = get();// 獲取當前值(舊值)
        if (compareAndSet(current, newValue))// CAS新值替代舊值
            return current;// 返回舊值
    }
}

TLAB

(Thread Local Allocation Buffer)

原理:爲每一個線程預先在Eden區分配一塊兒內存,JVM在給線程中的對象分配內存時,首先在TLAB分配,當對象大於TLAB中的剩餘內存或TLAB的內存已用盡時,再採用上述的CAS進行內存分配

-XX:+/-UseTLAB:是否使用TLAB

-XX:TLABWasteTargetPercent:設置TLAB可佔用的Eden區的比率,默認爲1%

-XX:PrintTLAB:查看TLAB的使用情況

JVM會根據以下三個條件來給每個線程分配合適大小的TLAB:

1.-XX:TLABWasteTargetPercent

2.線程數量

3.線程是否頻繁分配對象

3. 初始化零值並設置對象頭

  • 對象的實例字段不需要賦初始值也可以直接使用其默認零值
  • 每一種類型的對象都有不同的默認零值

對象在內存中存儲的佈局分爲三塊

對象頭

存儲對象自身的運行時數據:Mark Word(在32bit和64bit虛擬機上長度分別爲32bit和64bit),包含如下信息:

  • 對象hashCode
  • 對象GC分代年齡,年齡大導致進入老生代
  • 鎖狀態標誌(無鎖、偏向鎖、輕量級鎖、重量級鎖)
  • 線程持有的鎖(無鎖、偏向鎖、輕量級鎖、重量級鎖)
  • oops-類型指針,ordinary object pointers,普通對象指向類元數據的指針(32bit-->32bit,64bit-->64bit(未開啓壓縮指針),32bit(開啓壓縮指針))。JVM通過這個指針來確定這個對象是哪個類的實例(根據對象確定其Class的指針)

-XX:+UseCompressedOops:JDK 8下默認爲啓用,啓用對象的指針壓縮,節約內存佔用的大小

-XX:+CompactFields:JDK 8下默認爲啓用,分配一個非static的字段在前面字段縫隙中,提高內存的利用率

-XX:FieldsAllocationStyle=1:JDK 8下默認值爲‘1’,實例對象中有效信息的存儲順序

  • 0:先放入oops,然後在放入基本變量類型(順序:longs/doubles、ints/floats、shorts/chars、bytes/booleans),如果longs/doubles都有按定義的順序
  • 1:先放入基本變量類型(順序:longs/doubles、ints/floats、shorts/chars、bytes/booleans),然後放入oops
  • 2:oops和基本變量類型交叉存儲

注:Mark Word具有非固定的數據結構,以便在極小的空間內存儲儘量多的信息。如果對象是一個數組,對象頭必須有一塊兒用於記錄數組長度的數據。JVM可以通過Java對象的元數據確定對象長度,但是對於數組不行。

一般對象頭大小
                    32 位系統   64 位系統(+UseCompressedOops)    64 位系統(-UseCompressedOops)
Mark Word       4 bytes        8 bytes                                  8 bytes
Class Pointer   4 bytes        4 bytes                                  8 bytes
總計                 8 bytes        12 bytes                               16 bytes

數組對象對象頭大小
                32 位系統        64 位系統(+UseCompressedOops)    64 位系統(-UseCompressedOops)
Mark Word      4 bytes     8 bytes                            8 bytes
Class Pointer  4 bytes     4 bytes                            8 bytes
Length            4 bytes     4 bytes                             2 bytes
總計                12 bytes   16 bytes                          20 bytes

基本數據類型(32/64位,不區分壓縮)
double    8 bytes
long        8 bytes
float        4 bytes
int           4 bytes
char        2 bytes
short       2 bytes
byte        1 bytes
boolean  1 bytes
oops       4 bytes
包裝類型
                              +useCompressedOops    -useCompressedOops
Byte, Boolean        16 bytes                           24 bytes
Short, Character     16 bytes                          24 bytes
Integer, Float          16 bytes                          24 bytes
Long, Double           24 bytes                         24 bytes

在java中對象的每個成員屬性都有一個offset,通過UnsafeUtils.unsafe().objectFieldOffset可以獲得。但分配內存是有些差別,64位系統中CPU一次讀操作可讀取64bit(8 bytes)的數據,讀取屬性long不能讀取2次(如果從對象頭開始就會發生),因此會打破存儲順序(FieldsAllocationStyle)。字段重排列技術指的是重新分配字段的先後順序,以達到內存對齊的目的。

實例數據 對象真正存儲的有效信息
對齊填充 HotSpot VM的自動內存管理系統要求對象大小必須是8字節的整數倍,對齊填充沒有特別的含義,它僅僅起着佔位符的作用

4. 執行init

爲對象的字段賦值(這裏會根據所寫程序給實例賦值)

小刀牛試

public class ObjectSize_test {
    /**
     * 啓動參數中添加: -javaagent:D:\Program\repository\classmexer\classmexer\0.03\classmexer-0.03.jar
     */
    @Test
    public void showObjSize() {
        LongInstace v = new LongInstace();
        //24=12+8+2+2=對象頭+long+byte+padding(2),不壓縮的話就是16+8+2...
        System.out.printf("shallow size:    %s.byte\n", MemoryUtil.memoryUsageOf(v));
        System.out.printf("retained size:   %s.byte\n", MemoryUtil.deepMemoryUsageOf(v));

        LongInstace2 v2 = new LongInstace2();
        //16=12+4=對象頭+oops
        System.out.printf("shallow size:    %s.byte\n", MemoryUtil.memoryUsageOf(v2));
        //40=16+24=LongInstace2+Long
        System.out.printf("retained size:   %s.byte\n", MemoryUtil.deepMemoryUsageOf(v2));

        IntegerInstace1 v3 = new IntegerInstace1();
        //16=12+4=對象頭+oops
        System.out.printf("shallow size:    %s.byte\n", MemoryUtil.memoryUsageOf(v3));
        //32=16+16=LongInstace3+Integer,有壓縮
        System.out.printf("retained size:   %s.byte\n", MemoryUtil.deepMemoryUsageOf(v3));

        LongInstace[]vs=new LongInstace[2];
        vs[0]=v;
        System.out.printf("shallow size:    %s.byte\n", MemoryUtil.memoryUsageOf(vs));
        System.out.printf("retained size:   %s.byte\n", MemoryUtil.deepMemoryUsageOf(vs));

        System.out.printf("first item offset : %s,per item size: %s \n",UnsafeUtils.unsafe().arrayBaseOffset(vs.getClass()),UnsafeUtils.unsafe().arrayIndexScale(LongInstace[].class));

        ComplexInstance obj = new ComplexInstance();
        //48=12+1+2+2+4+4+8+8+padding(7)
        System.out.println("ComplexInstance:" + MemoryUtil.memoryUsageOf(obj));
    }

    @Test
    public void showLayout() throws NoSuchFieldException {
        /**
         * 無填充
         * 內存佈局:對象頭(12) + oops(4)
         */
        long offset = UnsafeUtils.unsafe().objectFieldOffset(IntegerInstace1.class.getDeclaredField("value"));
        //12=對象頭(12) 開始
        System.out.printf("IntegerInstace1.value(Integer):  %s \n", offset);
        /**
         * 有填充
         * 規則:64位系統中CPU一次讀操作可讀取64bit(8 bytes)的數據,讀取屬性long不能讀取2次(如果從對象頭開始就會發生)
         */
        offset = UnsafeUtils.unsafe().objectFieldOffset(LongInstace1.class.getDeclaredField("value"));
        //16=對象頭(12)+padding(4)
        System.out.printf("LongInstace1.value(long):    %s \n", offset);
        offset = UnsafeUtils.unsafe().objectFieldOffset(LongInstace.class.getDeclaredField("value2"));
        //12=對象頭(12)    -XX:+CompactFields生效
        System.out.printf("LongInstace.value2(byte):    %s \n", offset);
        offset = UnsafeUtils.unsafe().objectFieldOffset(LongInstace.class.getDeclaredField("value3"));
        //13=對象頭(12)+value2     -XX:+CompactFields生效
        System.out.printf("LongInstace.value3(char):    %s \n", offset);
        /**
         * 內部有多個成員變量時,佈局按-XX:FieldsAllocationStyle=1(longs/doubles、ints/floats、shorts/chars、bytes/booleans)
         * double=24=對象頭(12)+int(4)+long(8),double和long是有變量定義的順序決定的,不行你可以試試
         * 48=對象頭(12)+int(4)+long(8)+double(8)+float(4)+char(2)+short(2)+boolean(1)+padding(7)
         */
        offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("bool"));
        System.out.printf("ComplexInstance.bool(boolean):   %s \n", offset);//40
        offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("cha"));
        System.out.printf("ComplexInstance.cha(char):   %s \n", offset);//36
        offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("sho"));
        System.out.printf("ComplexInstance.sho(short):   %s \n", offset);//38
        offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("in"));
        System.out.printf("ComplexInstance.in(int):   %s \n", offset);//12
        offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("flo"));
        System.out.printf("ComplexInstance.flo(float):   %s \n", offset);//32
        offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("lon"));
        System.out.printf("ComplexInstance.lon(long):   %s \n", offset);//16
        offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("dou"));
        System.out.printf("ComplexInstance.dou(double):   %s \n", offset);//24
    }

    public final static class LongInstace {
        protected long value = 0L;
        byte value2;
        boolean value3;
    }

    public final static class LongInstace1 {
        protected long value = 0L;
    }

    public final static class LongInstace2 {
        Long value = 0L;
    }

    public final static class IntegerInstace1 {
        Integer value = 0;
    }

    public final static class ComplexInstance {
        boolean bool;

        char cha;
        short sho;

        int in;
        float flo;

        long lon;
        double dou;
    }
}

 

參閱資料

  • 《HotSpot實戰》
  • 《深入理解Java虛擬機:JVM高級特性與最佳實踐(最新第二版)》
  • 《實戰JAVA虛擬機  JVM故障診斷與性能優化》
  • 《分佈式Java應用:基礎與實踐》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章