Java對(duì)象在內(nèi)存中的布局是如何實(shí)現(xiàn)的?
1、-XX:FieldsAllocationStyle
對(duì)象在內(nèi)存中的布局首要相關(guān)配置就是FieldsAllocationStyle,這個(gè)配置有3個(gè)可選值,即0、1、2。當(dāng)值為2的時(shí)候,會(huì)經(jīng)過(guò)一些邏輯判斷最終轉(zhuǎn)化為0或者1.
- -XX:FieldsAllocationStyle=0 表示先分配對(duì)象,然后再按照double/long、ints、chars/shorts、bytes/booleans的順序分配其他字段,也就是類(lèi)中聲明的相同寬度的字段總是會(huì)被分配在一起,而相同寬度字段的順序則是它們?cè)赾lass文件中聲明的順序。-
- XX:FieldsAllocationStyle=1表示先按照double/long、ints、chars/shorts、bytes/booleans的順序分配屬性,然后再分配對(duì)象,分配過(guò)程中的其他原則上面為0時(shí)是保持一致的,同時(shí)這也是JVM默認(rèn)的分配策略。
當(dāng)然,上面這2種分配策略只是針對(duì)大部分正常情況而言,有以下幾種情況是會(huì)有所區(qū)別的(只是有部分區(qū)別,大致是沒(méi)有問(wèn)題的)
- 如果是特定的類(lèi)、例如基本類(lèi)型的包裝類(lèi)、String、Class、ClassLoader、軟引用等類(lèi),會(huì)先分配對(duì)象,然后再按照double/long、ints、chars/shorts、bytes/booleans的順序分配,同時(shí)-XX:+CompactFields和-XX:FieldsAllocationStyle=1都不會(huì)生效。
- 如果配置-XX:+CompactFields,會(huì)將ints、shorts/chars、bytes/booleans、oops的順序?qū)⒆侄翁畛涞綄?duì)象頭信息與字段起始偏移位置的間隙中去
- 如果當(dāng)前類(lèi)或者類(lèi)中使用了注解@sun.misc.Contended, 也會(huì)打亂上述布局
其他:
由于在計(jì)算對(duì)象字段的布局(字段基于對(duì)象起始位置的偏移量)時(shí),當(dāng)前類(lèi)上述各種類(lèi)型變量的個(gè)數(shù)是已知的,所以每種類(lèi)型的起始偏移量就可以通過(guò)計(jì)算得到,如下:
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;
而對(duì)于oops對(duì)象的偏移量處理會(huì)比較特殊,如果-XX:FieldsAllocationStyle=0, 那么oops的偏移量起始位置就為對(duì)象頭之后,如果-XX:FieldsAllocationStyle=1, 則會(huì)進(jìn)行下列處理,使得next_nonstatic_padded_offset與heapOopSize是對(duì)齊的。如下:
// 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);
}
同時(shí)由于這個(gè)oops補(bǔ)齊操作以及計(jì)算完所有字段的偏移量之后,會(huì)再進(jìn)行補(bǔ)齊操作,與heapOopSize進(jìn)行對(duì)齊,heapOopSize在開(kāi)啟和關(guān)閉壓縮指針的情況下,值分表為4和8。
2、-XX:CompactFields
-XX:CompactFields表示是否將對(duì)象中較窄的數(shù)據(jù)插入到間隙中,-XX:+CompactFields表示插入,-XX:-CompactFields則是不插入。默認(rèn)JVM是開(kāi)啟插入的。
那么這兒就要討論一下為什么會(huì)插入,以及怎么插入?
首先需要了解Java對(duì)象的大致內(nèi)存布局,最開(kāi)始的一塊區(qū)域存放對(duì)象標(biāo)記以及元數(shù)據(jù)指針,然后才是實(shí)例數(shù)據(jù),如下圖所示:

它們分別對(duì)應(yīng)普通對(duì)象與數(shù)組對(duì)象在內(nèi)存中的布局。由于對(duì)象字段布局是在Class文件解析的時(shí)候計(jì)算的,而數(shù)組類(lèi)沒(méi)有對(duì)應(yīng)的Class文件,所以數(shù)組對(duì)象的布局這兒不做討論。
繼續(xù)回到剛剛的話題,將對(duì)象中較窄數(shù)據(jù)的插入間隙,可以細(xì)分為2種情況
- 當(dāng)前類(lèi)沒(méi)有父類(lèi)或者是父類(lèi)中沒(méi)有實(shí)例數(shù)據(jù),此時(shí)會(huì)將實(shí)例數(shù)據(jù)前的對(duì)象標(biāo)記和對(duì)象元數(shù)據(jù)指針按照8字節(jié)對(duì)齊,如上圖所示,在開(kāi)啟壓縮指針的情況下,對(duì)齊前占用12個(gè)字節(jié),對(duì)齊后到16字節(jié),此時(shí)存在4個(gè)字節(jié)的間隙,那么會(huì)將類(lèi)中存在的字段按照 ints、chars/shorts、bytes/booleans、oops的順序進(jìn)行填充,直到將間隙填充完畢,由于對(duì)齊之后的間隙要么是0,要么是4,所以填充間隙最多1個(gè)ints、2個(gè)chars/shorts、4個(gè)bytes/booleans、1個(gè)oops。
- 當(dāng)前類(lèi)存在父類(lèi),并且父類(lèi)中存在實(shí)例數(shù)據(jù),此時(shí)會(huì)將實(shí)例數(shù)據(jù)前的對(duì)象標(biāo)記和對(duì)象元數(shù)據(jù)指針 + 父類(lèi)的實(shí)例數(shù)據(jù)大小按照8字節(jié)對(duì)齊,然后再進(jìn)行填充,由于整個(gè)類(lèi)在計(jì)算完所有字段偏移之后,會(huì)再與
heapOopSize進(jìn)行對(duì)齊,所以父類(lèi)的實(shí)例數(shù)據(jù)大小肯定是heapOopSize的倍數(shù),也就是與第一種情況類(lèi)似,不同的是,子類(lèi)中的字段屬性需要在父類(lèi)字段之后進(jìn)行分配。
最終可以得到如下圖所示:

間隙插入受-XX:CompactFields影響外,還受到配置-XX:-UseCompressedOops的影響,回到上面的對(duì)齊,在開(kāi)啟壓縮指針的情況下,元數(shù)據(jù)指針占8個(gè)字節(jié),這時(shí)候按照上面的細(xì)分情況1,也就不存在對(duì)齊了,而細(xì)分的情況二,由于父類(lèi)在計(jì)算完字段偏移量之后會(huì)與heapOopSize對(duì)齊,heapOopSize在開(kāi)啟壓縮指針的情況下為jintSize, 關(guān)閉的情況下為oopSize,分別對(duì)應(yīng)4和8, 也就是關(guān)閉壓縮指針的情況下,無(wú)論如何都不會(huì)發(fā)生間隙插入。
3、@sun.misc.Contended
@sun.misc.Contended也會(huì)影響對(duì)象在內(nèi)存中的布局,這個(gè)注解是為了解決偽共享(False Sharing)的問(wèn)題,關(guān)于偽共享的問(wèn)題這兒就不講解了。
@sun.misc.Contended 可以用于修飾類(lèi)、也可以用于修飾字段。
對(duì)于在類(lèi)上的修飾來(lái)講,會(huì)在2個(gè)地方增加ContendedPaddingWidth,這個(gè)變量值為128。
一個(gè)地方是對(duì)象標(biāo)記和元數(shù)據(jù)指針 + 父類(lèi)實(shí)例數(shù)據(jù)(當(dāng)前可能沒(méi)有父類(lèi)實(shí)例數(shù)據(jù))之后 + ContendedPaddingWidth,然后再與8位進(jìn)行對(duì)齊,另一個(gè)地方是,所有的非Contended實(shí)例字段偏移量計(jì)算完畢后,再加上ContendedPaddingWidth。
處理完類(lèi),接下來(lái)是字段,這兒的字段偏移量計(jì)算跟上面不一樣,并沒(méi)有按照double/long、ints、chars/shorts、bytes/booleans的順序來(lái),而是按照@sun.misc.Contended對(duì)應(yīng)的group來(lái)進(jìn)行計(jì)算,相同group的字段會(huì)放在一起,不同group的字段之間會(huì)以ContendedPaddingWidth來(lái)隔開(kāi),這兒比較特殊的情況是默認(rèn)分組,默認(rèn)分組為0,這個(gè)分組對(duì)應(yīng)的每個(gè)字段在計(jì)算完偏移量之后都會(huì)加上ContendedPaddingWidth。所以@sun.misc.Contended修飾的字段布局如下圖所示:

同時(shí)在計(jì)算每個(gè)字段偏移前,會(huì)使當(dāng)前的偏移量與當(dāng)前字段類(lèi)型所對(duì)應(yīng)的字節(jié)數(shù)對(duì)齊,例如int,當(dāng)前偏移量會(huì)以4字節(jié)進(jìn)行對(duì)齊,對(duì)齊之后的偏移量為當(dāng)前int字段的偏移量。
4、靜態(tài)字段的偏移量計(jì)算
靜態(tài)字段的偏移量計(jì)算不受-XX:FieldsAllocationStyle和-XX:CompactFields的影響,會(huì)直接按照
oops、double/long、ints、chars/shorts、bytes/booleans的順序進(jìn)行偏移量的計(jì)算。
同時(shí)給靜態(tài)字段 加上@sun.misc.Contended不會(huì)起到任何作用。
5、示例
5.1、-XX:FieldsAllocationStyle
測(cè)試代碼:
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);
}
}
}
運(yùn)行結(jié)果:
-XX:FieldsAllocationStyle=0 -XX:-UseCompressedOops
//可以看到對(duì)象實(shí)例數(shù)據(jù)順序?yàn)関alue、int、byte,這兒由于沒(méi)有開(kāi)啟指針壓縮,所以對(duì)象引用占了8個(gè)字節(jié)。
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,最后才是對(duì)象 value
//這兒的byte變量b偏移是20,占用大小1字節(jié),而經(jīng)過(guò)對(duì)齊之后,會(huì)產(chǎn)生3個(gè)字節(jié)的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
測(cè)試代碼:
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);
}
}
}
測(cè)試結(jié)果:
-XX:+CompactFields -XX:+UseCompressedOops
//可以看到子類(lèi)Child2的變量f并沒(méi)有按照double/long、ints、shorts/chars、bytes/booleans的順序計(jì)算偏移量,
//而是插入到了間隙里面
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
//由于關(guān)閉了CompactFields,所以變量f的按照上面的順序進(jìn)行偏移量計(jì)算
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
測(cè)試代碼:
@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);
}
}
}
測(cè)試結(jié)果:
-XX:-RestrictContended
同分組時(shí):
org.yamikaze.NoChildContended field b offset is 140
org.yamikaze.NoChildContended field value offset is 272
org.yamikaze.NoChildContended field value1 offset is 280
不同分組(默認(rèn)分組):
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注解修飾,導(dǎo)致byte變量的偏移量很大(12 + 128) 同樣byte變量之后的value,偏移量再次增加了128,達(dá)到272(141 + 128 = 269然后與4字節(jié)對(duì)齊得到272),然后相同分組的value1緊跟著value,而在不同分組的情況下,value1和value之間又隔了128。
6、其他
6.1、通過(guò)Unsafe獲取實(shí)例字段和靜態(tài)字段的偏移量
//實(shí)例字段 unsafe.objectFieldOffset(field); //靜態(tài)字段 unsafe.staticFieldOffset(field);
6.2、Unsafe是如何進(jìn)行實(shí)例字段和靜態(tài)字段偏移量的獲取,以及如何通過(guò)CAS操作改變值
回到上文的偏移量計(jì)算,在經(jīng)過(guò)計(jì)算后,每個(gè)字段相對(duì)于對(duì)象頭的偏移量都是已知的,這個(gè)偏移量會(huì)保存到字段信息里面去,那么獲取字段偏移量也很簡(jiǎn)單,直接拿到字段相關(guān)信息取得offset即可,而通過(guò)CAS操作改變字段的值也很簡(jiǎn)單,當(dāng)前對(duì)象指針加上字段偏移量就是當(dāng)前字段在內(nèi)存中的地址,直接通過(guò)指針更字段值即可。
到此這篇關(guān)于Java對(duì)象在內(nèi)存中的布局是如何實(shí)現(xiàn)的?的文章就介紹到這了,更多相關(guān)Java對(duì)象在內(nèi)存中的布局內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java class文件格式之?dāng)?shù)據(jù)類(lèi)型(二)_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java class文件格式之?dāng)?shù)據(jù)類(lèi)型(二)的相關(guān)資料,需要的朋友可以參考下2017-06-06
Springboot使用jxls實(shí)現(xiàn)excel模板導(dǎo)出excel方式
這篇文章主要介紹了Springboot使用jxls實(shí)現(xiàn)excel模板導(dǎo)出excel方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
Java如何實(shí)現(xiàn)多個(gè)線程之間共享數(shù)據(jù)
這篇文章主要介紹了Java如何實(shí)現(xiàn)多個(gè)線程之間共享數(shù)據(jù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
Java 替換字符串右側(cè)出現(xiàn)的第一個(gè)子串方式
這篇文章主要介紹了Java 替換字符串右側(cè)出現(xiàn)的第一個(gè)子串方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
java中數(shù)組list map三者之間的互轉(zhuǎn)介紹
java中 數(shù)組 list map之間的互轉(zhuǎn)一張圖清晰呈現(xiàn)并附有代碼,不懂的朋友可以參考下2013-10-10
一文帶你掌握J(rèn)ava中Scanner類(lèi)的使用
Scanner類(lèi)是java.util包中的一個(gè)類(lèi),常用于控制臺(tái)的輸入,當(dāng)需要使用控制臺(tái)輸入時(shí)即可調(diào)用這個(gè)類(lèi)。本文將通過(guò)一些簡(jiǎn)單的例子為大家介紹一下Java中Scanner類(lèi)的使用,需要的可以參考一下2023-04-04

