每个 Java 对象都有一个对象头(object header),这个由标记字段和类型指针所构成。其中,标记字段用以存储 Java 虚拟机有关该对象的运行数据,如哈希码、GC 信息以及锁信息,而类型指针则指向该对象的类。
在 64 位的 Java 虚拟机中,对象头的标记字段占 64 位,而类型指针又占了 64 位。也就是说,每一个 Java 对象在内存中的额外开销就是 16 个字节。以 Integer 类为例,它仅有一个 int 类型的私有字段,占 4 个字节。因此,每一个 Integer 对象的额外内存开销至少是 400%。这也是为什么 Java 要引入基本类型的原因之一。
为了尽量较少对象的内存使用量,64 位 Java 虚拟机引入了压缩指针,将堆中原本 64 位的 Java 对象指针压缩成 32 位的。
压缩指针
压缩指针的原理是,用一个编号标识多个连续地址,比如0号表示0到1位置上的空间,那么,32 位压缩指针最多可以标记 2 的 32 次方个字段,对应着 2 的 33 次方个位空间。这样子有个限制,就是每个字段都从偶数号放起。这个概念我们称之为内存对齐(对应虚拟机选项 -XX:ObjectAlignmentInBytes,其实这个值的默认值为 8,压缩指针要求 Java 虚拟机堆中对象的起始地址要对齐至 8 的倍数。)。这样子,Java 虚拟机中的 32 位压缩指针可以寻址到 2 的 35 次方个字节,也就是 32GB 的地址空间(超过 32GB 则会关闭压缩指针)。
字段内存对齐的其中一个原因,是让字段只出现在同一 CPU 的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。
字段重排列
Java 虚拟机重新分配字段的先后顺序,以达到内存对齐的目的。
如果一个字段占据 C 个字节,那么该字段的偏移量需要对齐至 NC。这里偏移量指的是字段地址与对象的起始地址差值。
以 long 类为例,它仅有一个 long 类型(8字节)的实例字段。在使用了压缩指针的 64 位虚拟机中,尽管对象头的大小为 12 个字节,该 long 类型字段的偏移量也只能是 16,而中间空着的 4 个字节便会被浪费掉。
子类所继承字段的偏移量,需要与父类对应字段的偏移量保持一致。
Java 虚拟机还会对齐子类字段的起始位置。对于使用了压缩指针的 64 位虚拟机,子类第一个字段需要对齐至 4N;而对于关闭了压缩指针的 64 位虚拟机,子类第一个字段则需要对齐至 8N。
class A {
long l;
int i;
}
class B extends A {
long l;
int i;
}

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

当关闭压缩指针时,B 类字段的起始位置需对齐至 8N。这么一来,B 类字段的前后各有 4 字节的空白。
951

被折叠的 条评论
为什么被折叠?



