Java 对象的内存布局,在 Java 对象的布局中,主要有着三部分,对象头,实例数据,对齐填充。

对象头

主要包括两部分

  • 标记字段
  • 标记字段主要记录了对象在运行时候的信息,包括 哈希码,锁信息,GC 等
  • 类型指针
  • 包含了指向对象的类信息的指针。

在 64 位虚拟机中,标记字段和类型指针都占了个 64 为,总共 16 个字节,在这里对于 Integer 类型的对象来说,它的示例数据字段 int 也才四个字节,翻了 4 倍。这也是引入基本类型的原因之一。

为了解决上述问题,引入了压缩指针

压缩指针

对标记字段进行压缩指针,可以把 64 位虚拟机下的 8 字节,压缩成 4 个字节。

这样就造成了,对于内存中的寻址操作,要按照 4 或 8 的倍数来寻址,所以,虚拟机要求对象的起始内存地址,对齐至 8 的倍数(对应虚拟机的参数 -XX:ObjectAlignmentlbBytes ,默认位 8),如果没有达到 8 倍数的字节,那么空间就被浪费掉。

注意,即使没有开启压缩指针,虚拟机依然会内存对齐。

内存对齐

默认情况下,堆中对象的起始地址需要时 8 的整数倍。如果一个对象的空间占用不到 8 的整数倍,那么将浪费掉剩余的空间,这也叫做对象间的填充。

对于内存对齐,不仅在对象之间,也在对象中字段之间。

为了使得字段内存对齐,主要有以下原因。

字段对齐

  • 让字段同时存在在同一行 CPU 缓存中,引入如果字段不对齐,导致对象在内存中的存储是跨 CPU 缓存的,在读取、写入的过程都要同时污染两个缓存行,那么对于程序的指向效率是很不利的,对 CPU 访问不友好
  • 虚拟机要求 long 字段、double 字段、以及非压缩指针的状态下,引用字段的地址必须为 8 的倍数

针对字段对齐,虚拟机采用了字段重排列的优化方法,来使得内存对齐。

字段重排列

重新分配字段的前后顺序,来达到内存对齐的目的。

Java 中有三种排列方式(对于 Java 虚拟机选项 -XX:FieldAllocationStyle,默认1),有以下两个规则。

规则

  • 如果一个字段占用 C 字节,那么相对于对象的起始地址,它要被偏移到 NC 字节处。
  • 子类继承字段的偏移量,要和父类的对应字段偏移量保持一致。
  • 虚拟机还会对齐子类字段的起始位置。
  • 在开启压缩指针的 64 位虚拟机,子类第一个字段需要对齐至 4N。
  • 在关闭压缩指针的 64 位虚拟机,子类第一个字段需要对齐至 8N。

虚共享

当两个线程访问同一个对象,不同的 volatile 字段(强制变量的赋值会同步刷新回主内存,强制变量的读取从主内存中重新加载。保证变量看起来始终是最新指。),直接看起来没有共享内容,所以不需要同步。但是,如果它们存在同一个缓存行中,对于这些字段的写入读取都造成了实质的共享。

Java 8 引入了 @Contended ,就是为了解决这一问题,但是会影响到字段的排列。

@Contended 会让字段处于独立的缓存行中,会造成空间浪费,这里不过多赘述。可以通过 -XX:-RestrictContended 虚拟机选项查看 Contentded 字段的内存布局。

实例数据

不多解释,也就是存放实际存储数据的位置。

对齐填充

并不是必然存在,仅仅是起着占位符的作用。由于 HotSpot VM 内存管理系统要求对象起始地址是 8 字节的整数倍,所以对象大小必须是 8 字节整数倍,所以需要填充为满 8 字节的地方。