Java對象在內存中的佈局是如何實現的?

1、-XX:FieldsAllocationStyle

對象在內存中的佈局首要相關配置就是FieldsAllocationStyle,這個配置有3個可選值,即0、1、2。當值為2的時候,會經過一些邏輯判斷最終轉化為0或者1.

  • -XX:FieldsAllocationStyle=0 表示先分配對象,然後再按照double/long、ints、chars/shorts、bytes/booleans的順序分配其他字段,也就是類中聲明的相同寬度的字段總是會被分配在一起,而相同寬度字段的順序則是它們在class文件中聲明的順序。-
  • XX:FieldsAllocationStyle=1表示先按照double/long、ints、chars/shorts、bytes/booleans的順序分配屬性,然後再分配對象,分配過程中的其他原則上面為0時是保持一致的,同時這也是JVM默認的分配策略。

當然,上面這2種分配策略隻是針對大部分正常情況而言,有以下幾種情況是會有所區別的(隻是有部分區別,大致是沒有問題的)

  • 如果是特定的類、例如基本類型的包裝類、String、Class、ClassLoader、軟引用等類,會先分配對象,然後再按照double/long、ints、chars/shorts、bytes/booleans的順序分配,同時-XX:+CompactFields和-XX:FieldsAllocationStyle=1都不會生效。
  • 如果配置-XX:+CompactFields,會將ints、shorts/chars、bytes/booleans、oops的順序將字段填充到對象頭信息與字段起始偏移位置的間隙中去
  • 如果當前類或者類中使用瞭註解@sun.misc.Contended, 也會打亂上述佈局

其他:

由於在計算對象字段的佈局(字段基於對象起始位置的偏移量)時,當前類上述各種類型變量的個數是已知的,所以每種類型的起始偏移量就可以通過計算得到,如下:

next_nonstatic_word_offset  = next_nonstatic_double_offset +
                                (nonstatic_double_count * BytesPerLong);
next_nonstatic_short_offset = next_nonstatic_word_offset +
  (nonstatic_word_count * BytesPerInt);
next_nonstatic_byte_offset  = next_nonstatic_short_offset +
  (nonstatic_short_count * BytesPerShort);
next_nonstatic_padded_offset = next_nonstatic_byte_offset +
  nonstatic_byte_count;

而對於oops對象的偏移量處理會比較特殊,如果-XX:FieldsAllocationStyle=0, 那麼oops的偏移量起始位置就為對象頭之後,如果-XX:FieldsAllocationStyle=1, 則會進行下列處理,使得next_nonstatic_padded_offset與heapOopSize是對齊的。如下:

// let oops jump before padding with this allocation style
if( allocation_style == 1 ) {
  next_nonstatic_oop_offset = next_nonstatic_padded_offset;
  if( nonstatic_oop_count > 0 ) {
    next_nonstatic_oop_offset = align_size_up(next_nonstatic_oop_offset, heapOopSize);
  }
  next_nonstatic_padded_offset = next_nonstatic_oop_offset + (nonstatic_oop_count * heapOopSize);
}

同時由於這個oops補齊操作以及計算完所有字段的偏移量之後,會再進行補齊操作,與heapOopSize進行對齊,heapOopSize在開啟和關閉壓縮指針的情況下,值分表為4和8。

2、-XX:CompactFields

-XX:CompactFields表示是否將對象中較窄的數據插入到間隙中,-XX:+CompactFields表示插入,-XX:-CompactFields則是不插入。默認JVM是開啟插入的。

那麼這兒就要討論一下為什麼會插入,以及怎麼插入?

首先需要瞭解Java對象的大致內存佈局,最開始的一塊區域存放對象標記以及元數據指針,然後才是實例數據,如下圖所示:

在這裡插入圖片描述

它們分別對應普通對象與數組對象在內存中的佈局。由於對象字段佈局是在Class文件解析的時候計算的,而數組類沒有對應的Class文件,所以數組對象的佈局這兒不做討論。

繼續回到剛剛的話題,將對象中較窄數據的插入間隙,可以細分為2種情況

  • 當前類沒有父類或者是父類中沒有實例數據,此時會將實例數據前的對象標記和對象元數據指針按照8字節對齊,如上圖所示,在開啟壓縮指針的情況下,對齊前占用12個字節,對齊後到16字節,此時存在4個字節的間隙,那麼會將類中存在的字段按照 ints、chars/shorts、bytes/booleans、oops的順序進行填充,直到將間隙填充完畢,由於對齊之後的間隙要麼是0,要麼是4,所以填充間隙最多1個ints、2個chars/shorts、4個bytes/booleans、1個oops。
  • 當前類存在父類,並且父類中存在實例數據,此時會將實例數據前的對象標記和對象元數據指針 + 父類的實例數據大小按照8字節對齊,然後再進行填充,由於整個類在計算完所有字段偏移之後,會再與heapOopSize進行對齊,所以父類的實例數據大小肯定是heapOopSize的倍數,也就是與第一種情況類似,不同的是,子類中的字段屬性需要在父類字段之後進行分配。

最終可以得到如下圖所示:

在這裡插入圖片描述

間隙插入受-XX:CompactFields影響外,還受到配置-XX:-UseCompressedOops的影響,回到上面的對齊,在開啟壓縮指針的情況下,元數據指針占8個字節,這時候按照上面的細分情況1,也就不存在對齊瞭,而細分的情況二,由於父類在計算完字段偏移量之後會與heapOopSize對齊,heapOopSize在開啟壓縮指針的情況下為jintSize, 關閉的情況下為oopSize,分別對應4和8, 也就是關閉壓縮指針的情況下,無論如何都不會發生間隙插入。

3、@sun.misc.Contended

@sun.misc.Contended也會影響對象在內存中的佈局,這個註解是為瞭解決偽共享(False Sharing)的問題,關於偽共享的問題這兒就不講解瞭。

@sun.misc.Contended 可以用於修飾類、也可以用於修飾字段。

對於在類上的修飾來講,會在2個地方增加ContendedPaddingWidth,這個變量值為128。

一個地方是對象標記和元數據指針 + 父類實例數據(當前可能沒有父類實例數據)之後 + ContendedPaddingWidth,然後再與8位進行對齊,另一個地方是,所有的非Contended實例字段偏移量計算完畢後,再加上ContendedPaddingWidth。

處理完類,接下來是字段,這兒的字段偏移量計算跟上面不一樣,並沒有按照double/long、ints、chars/shorts、bytes/booleans的順序來,而是按照@sun.misc.Contended對應的group來進行計算,相同group的字段會放在一起,不同group的字段之間會以ContendedPaddingWidth來隔開,這兒比較特殊的情況是默認分組,默認分組為0,這個分組對應的每個字段在計算完偏移量之後都會加上ContendedPaddingWidth。所以@sun.misc.Contended修飾的字段佈局如下圖所示:

在這裡插入圖片描述

同時在計算每個字段偏移前,會使當前的偏移量與當前字段類型所對應的字節數對齊,例如int,當前偏移量會以4字節進行對齊,對齊之後的偏移量為當前int字段的偏移量。

4、靜態字段的偏移量計算

靜態字段的偏移量計算不受-XX:FieldsAllocationStyle和-XX:CompactFields的影響,會直接按照

oops、double/long、ints、chars/shorts、bytes/booleans的順序進行偏移量的計算。

同時給靜態字段 加上@sun.misc.Contended不會起到任何作用。

5、示例

5.1、-XX:FieldsAllocationStyle

測試代碼:

final class NoChild {
    private Boolean value = Boolean.TRUE;
    private byte b;
    private int i;
}

@Test
public void test() {
  declaredFields = NoChildContended.class.getDeclaredFields();
  for (Field field : declaredFields) {
    if (Modifier.isStatic(field.getModifiers())) {
      long offset = unsafe.staticFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " static field " + field.getName() + 					" offset is " + offset);
    } else {
      long offset = unsafe.objectFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " field " + field.getName() + " 						offset is " + offset);
    }
  }
}

運行結果:

-XX:FieldsAllocationStyle=0 -XX:-UseCompressedOops
//可以看到對象實例數據順序為value、int、byte,這兒由於沒有開啟指針壓縮,所以對象引用占瞭8個字節。
org.yamikaze.NoChild field value offset is 16
org.yamikaze.NoChild field b offset is 28
org.yamikaze.NoChild field i offset is 24
-XX:FieldsAllocationStyle=1 -XX:-UseCompressedOops
//可以看到先分配 int變量 i,其次byte變量 b,最後才是對象 value
//這兒的byte變量b偏移是20,占用大小1字節,而經過對齊之後,會產生3個字節的align
org.yamikaze.NoChild field value offset is 24
org.yamikaze.NoChild field b offset is 20
org.yamikaze.NoChild field i offset is 16

5.2、-XX:CompactFields

測試代碼:

class Parent {
    private long value;
    private int j;
    private byte b;
}

class Child2 extends Parent {
    private byte d;
    private long a;
    private int f;
}

@Test
public void test() {
  
  Child2 c = new Child2();
  Unsafe unsafe = UnSafeUtils.getUnsafe();
  Field[] declaredFields = c.getClass().getSuperclass().getDeclaredFields();
  for (Field field : declaredFields) {

    if (Modifier.isStatic(field.getModifiers())) {
      long offset = unsafe.staticFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " static field " + field.getName() + 				" offset is " + offset);
    } else {
      long offset = unsafe.objectFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " field " + field.getName() + " 						offset is " + offset);
    }
  }

  declaredFields = c.getClass().getDeclaredFields();
  for (Field field : declaredFields) {
    if (Modifier.isStatic(field.getModifiers())) {
      long offset = unsafe.staticFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " static field " + field.getName() + 				" offset is " + offset);

    } else {
      long offset = unsafe.objectFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " field " + field.getName() + " 					offset is " + offset);
    }
  }
}

測試結果:

-XX:+CompactFields -XX:+UseCompressedOops
//可以看到子類Child2的變量f並沒有按照double/long、ints、shorts/chars、bytes/booleans的順序計算偏移量,
//而是插入到瞭間隙裡面
org.yamikaze.Parent field value offset is 16
org.yamikaze.Parent field j offset is 12
org.yamikaze.Parent field b offset is 24
org.yamikaze.Child2 field d offset is 40
org.yamikaze.Child2 field a offset is 32
org.yamikaze.Child2 field f offset is 28
-XX:-CompactFields -XX:+UseCompressedOops
//由於關閉瞭CompactFields,所以變量f的按照上面的順序進行偏移量計算
org.yamikaze.Parent field value offset is 16
org.yamikaze.Parent field j offset is 24
org.yamikaze.Parent field b offset is 28
org.yamikaze.Child2 field d offset is 44
org.yamikaze.Child2 field a offset is 32
org.yamikaze.Child2 field f offset is 40

5.3、Contended

測試代碼:

@Contended
final class NoChildContended {

    private byte b;

    @Contended("aaa")
    private double value;

    @Contended("bbb")
    private int value1;
}

@Test
public void test() {
  declaredFields = NoChildContended.class.getDeclaredFields();
  for (Field field : declaredFields) {

    if (Modifier.isStatic(field.getModifiers())) {
      long offset = unsafe.staticFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " static field " + field.getName() + " offset is " + offset);

    } else {
      long offset = unsafe.objectFieldOffset(field);
      System.out.println(field.getDeclaringClass().getName() + " field " + field.getName() + " offset is " + offset);
    }
  }
}

測試結果:

-XX:-RestrictContended
同分組時:
org.yamikaze.NoChildContended field b offset is 140
org.yamikaze.NoChildContended field value offset is 272
org.yamikaze.NoChildContended field value1 offset is 280
不同分組(默認分組):
org.yamikaze.NoChildContended field b offset is 140
org.yamikaze.NoChildContended field value offset is 272
org.yamikaze.NoChildContended field value1 offset is 408

可以看到,由於Class上有@sun.misc.Contended註解修飾,導致byte變量的偏移量很大(12 + 128) 同樣byte變量之後的value,偏移量再次增加瞭128,達到272(141 + 128 = 269然後與4字節對齊得到272),然後相同分組的value1緊跟著value,而在不同分組的情況下,value1和value之間又隔瞭128。

6、其他

6.1、通過Unsafe獲取實例字段和靜態字段的偏移量

//實例字段
unsafe.objectFieldOffset(field);
//靜態字段
unsafe.staticFieldOffset(field);

6.2、Unsafe是如何進行實例字段和靜態字段偏移量的獲取,以及如何通過CAS操作改變值

回到上文的偏移量計算,在經過計算後,每個字段相對於對象頭的偏移量都是已知的,這個偏移量會保存到字段信息裡面去,那麼獲取字段偏移量也很簡單,直接拿到字段相關信息取得offset即可,而通過CAS操作改變字段的值也很簡單,當前對象指針加上字段偏移量就是當前字段在內存中的地址,直接通過指針更字段值即可。

到此這篇關於Java對象在內存中的佈局是如何實現的?的文章就介紹到這瞭,更多相關Java對象在內存中的佈局內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: