Java对象的内存布局
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虚拟机重新分配源代码中声明的字段先后顺序, 以达到内存对齐的目的.
规则
- 如果字段占据C个字节, 那么该字段的偏移量需要对齐至NC. 这里偏移量指的是字段起始地址与对象的起始地址的差值
例如:
Long类型有一个long字段, 在使用了压缩指针的64位虚拟机中
1 | long字段的地址 = n*16 + 对象起始地址 (n=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
9OFFSET 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
11OFFSET 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)