文章目录
Java对象在内存中属于oop-klass二分模型,即对象的实例数据和对象类型的元数据(字段定义、方法、常量池等元数据)是分开存储的,所以这儿的对象内存布局只讨论对象的实例数据(nonstatic field),而对象的实例数据可以划分为以下几类
oops 对象、 double/long、ints、chars/shorts、bytes/booleans,其中ints表示int和float
而由于JVM对对象内相同宽度的字段分配在一起,所以只要指定了字段类型分配的顺序,就可以计算出每种类型字段相对于当前对象的偏移起始位置。
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