java对象的额外内存开销(对象头)

运行数据 (8字节)

包括

  • 哈希码
  • GC信息
  • 所信息
对象类型指针 (8字节)

指向对象对应的类

压缩指针

通过 jvm命令选项开启 -XX:+UseCompressedOops
这里针对对象类型指针, 实际上对所有引用指针起作用

对于64位java虚拟机 64位 -> 32位
因此java对象的额外内存开销为12字节

内存对齐

通过jvm选项 -XX:ObjectAlignmentlnBytes开启
进一步提升了寻址范围. 同时也有可能增加对象间内存填充, 倒置压缩指针没有到达原本节省空间的效果.

对象内存对齐
对象内字段内存对齐

java虚拟机要求long字段,double字段以及非压缩指针状态下的引用字段地址为8的倍数

  • do for what?

让字段支出现在同一CPU的缓存行种. 如果字段不是对齐的, 那么可能出现跨缓存行的字段. 该对象的读取可能需要替换两个缓存行, 该字段的存储也会同时污染来那个缓存行. 对程序的执行效率不利.

字段重排列

通过jvm选项 -XX:FieldsAllocationStyle(默认为1)开启
java虚拟机重新分配源代码中声明的字段先后顺序, 以达到内存对齐的目的.

规则
  1. 如果字段占据C个字节, 那么该字段的偏移量需要对齐至NC. 这里偏移量指的是字段起始地址与对象的起始地址的差值

例如:
Long类型有一个long字段, 在使用了压缩指针的64位虚拟机中

1
long字段的地址 = n*16 + 对象起始地址 (n=1)
  1. 子类所继承的偏移量, 需要与父类的对应字段一致
    在具体实现中,Java 虚拟机还会对齐子类字段的起始位置。对于使用了压缩指针的 64 位虚拟机,子类第一个字段需要对齐至 4N;而对于关闭了压缩指针的 64 位虚拟机,子类第一个字段则需要对齐至 8N。

class A {
long l;
int i;
}

class B extends A {
long l;
int i;
}
我在文中贴了一段代码,里边定义了两个类 A 和 B,其中 B 继承 A。A 和 B 各自定义了一个 long 类型的实例字段和一个 int 类型的实例字段。下面我分别打印了 B 类在启用压缩指针和未启用压缩指针时,各个字段的偏移量。

  • 启用压缩指针时,B 类的字段分布
    B object internals:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    OFFSET  SIZE   TYPE DESCRIPTION
    0 4 (object header)
    4 4 (object header)
    8 4 (object header)
    12 4 int A.i 0
    16 8 long A.l 0
    24 8 long B.l 0
    32 4 int B.i 0
    36 4 (loss due to the next object alignment)

    当启用压缩指针时,可以看到 Java 虚拟机将 A 类的 int 字段放置于 long 字段之前,以填充因为 long 字段对齐造成的 4 字节缺口。由于对象整体大小需要对齐至 8N,因此对象的最后会有 4 字节的空白填充。

  • 关闭压缩指针时,B 类的字段分布
    B object internals:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    OFFSET  SIZE   TYPE DESCRIPTION
    0 4 (object header)
    4 4 (object header)
    8 4 (object header)
    12 4 (object header)
    16 8 long A.l
    24 4 int A.i
    28 4 (alignment/padding gap)
    32 8 long B.l
    40 4 int B.i
    44 4 (loss due to the next object alignment)

当关闭压缩指针时,B 类字段的起始位置需对齐至 8N。这么一来,B 类字段的前后各有 4 字节的空白。那么我们可不可以将 B 类的 int 字段移至前面的空白中,从而节省这 8 字节呢?

是可以节省的, 这可能是一个历史遗留问题

虚共享问题

两个线程分别访问同一个对象中不同的volatile字段, 逻辑上它们并没有共享内容, 因此不需要同步.
然而, 如果这两个字段恰好在同一个缓存行中, 那么对这些字段的写操作会导致缓存行的写会, 造成了实质上的共享.

  • java虚拟机会让不同的@Contended字段之间处于独立的缓存行, 因此造成大量空间浪费, 具体的分布算法属于实现细节.
  • 通过jvm选项-XX:-RestrictContended查阅Contended字段的内存布局(if java version > 9 编译时需要–add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAME)