java 指针_Java对象内存布局

言简意赅

本文探讨 java 对象在 hotspot 虚拟机下的内存使用细节

  1. 每个 java 对象包含 12B 或 16B 的头部信息,用于支持对象锁、多态、反射等特性
  2. 头部之后的内存区域,存储对象成员变量的值
  3. 内存对齐机制改变变量地址起点,提高内存使用效率,但形成内存浪费
  4. hotspot 通过变量重排等手段减少内存浪费

文章结构

ee3abaf0fb9482fa7a3d5adbaf15711a.png

对象组成

hotspot 虚拟机主体用 C/C++ 语言实现,运行其中的每个 java 对象会映射到一个 C++ 对象,称为 oop。java 对象和 oop 对象是对同一块内存区域的不同表达方式,因此对 java 对象内存开销的讨论,来源于 oop 对象的内存开销。

C++ 对象有自己的内存布局方式,通常是按照成员变量声明顺序依次分配内存空间,存在虚函数的情况下,对象头是虚表指针,用于实现多态调用。

java 对象虽然根植于 C++,但并没有继承 C++ 内存布局风格,我认为有以下原因:

  1. 为实现反射,java 对象需要关联到类信息
  2. 用 C++ 的标准看,java 所有函数都是虚函数,但 java 不需要每个对象都包含一个字的虚表指针
  3. java语言不面向底层,内存布局可以具有更高的灵活性

2b66f0cddf0aeb4f68e2fb7a1ec274ab.png
hotspot 改变了成员变量存储顺序

oop 对象在 openJdk13 中定义如下:

class 

_mark 是一个字长的对象元信息,存着 hash、锁升级信息、分代年龄等。

_metadata 是用 union 定义的指向对象所属类的指针,一个字长的 _klass 或 固定 4Bytes 的压缩指针,用于实现反射、多态等语言特性。

_mark 和 _metadata 构成了 java 对象头,内存中随后的部分是 java 对象实体(非静态成员变量),整个 java 对象在内存中的布局如下图所示:

ff0c3da92dd0ad04fd37f8ce6518d73f.png

内存对齐

进一步了解内存布局前,先要清楚内存对齐的概念。

内存对齐是指在两个变量之间插入空白区域使得变量地址起点满足内存对齐要求。例如:

class 

A对象的实际内存占用为:

30099e15cb88f9922b7e230642fada87.png

x和y之间插入了 3Bytes 的空数据,目的是使得y变量满足内存对齐要求。

不同编译器的内存对齐规则存在差异,但其中一个共同点是要求变量地址起点是变量长度的整数倍,内存对齐机制使得对象实际内存占用比实际需要的更多。

内存对齐牺牲了内存利用率,但提高了内存访问效率。这是因为 cpu 是以固定步长从主存中加载数据,一次可以加载包含多个字节的数据区块。内存对齐可以避免一个变量横跨两个数据区块,从而保证数据操作能在一个指令周期中完成(实际时间开销取决于 cpu 数据获取行为,很多 cpu 以 cacheline 粒度和主存交换数据)。另外内存对齐也能使得一个数据不会占用两个 cacheline(以上内容限定为基础类型数据)。

变量重排

变量重排能提高内存利用率,其核心思想是在内存空白区插入长度较小的变量。例如对上例A类结构做改造,交换y和z的位置:

class 

A对象的实际内存占用为:

369dc042ee32249162f635956bf0d4b0.png

内存开销从 12B 下降到 8B。

hotspot 自动重排成员变量,策略很简单,相同长度的变量连续存储,且按照字段长度有序排列

C++ 通常不具有这种能力,我认为这受限于 C++ 语言特性和面对的问题场景。换而言之,C++程序可能存在对变量偏移地址的依赖,而java没有。例如 C++ 里使用指针能跳过对象直接修改成员变量内容;又或者部分 C++ 对象或结构体需要映射到具体硬件单元上,变量的顺序关系到硬件驱动方式。这些场景要求不同 C++ 编译器的编译风格要固定且统一。

内存布局范例

类 A,B,C 依次形成父子依赖。

public 

输出子类 C 的变量顺序如下(变量偏移获取方式见文末):

16:

从数据结果能够看出:

  1. 变量偏移从 16Bytes 开始,前 16B 是对象头
  2. 同一类内,相同类型的成员变量连续存储,double 在前,short 在后
  3. 父类成员变量位置靠前,子类成员变量位置靠后
  4. 默认启用指针压缩,引用变量 d1 占用 4B 空间
  5. 依然存在内存浪费的情况,short1 和 d1 之间有 4B,而 short1 只使用 2B

压缩指针

有一些参数能影响java对象内存布局,首先是压缩指针。

f6b808094de574a5c849b328d0fecec0.png
openJdk里定义如下

启动参数增加 -XX:-UseCompressedOops 关闭指针压缩,看到引用的内存占用变成 8B。

...

hotspot 压缩指针是在 64bits 运行环境下使用 32bits 存储指针,并能寻址超过 4GB 的空间。压缩指针只对以下三种指针有影响

  1. the klass field of every object
  2. every oop instance field
  3. every element of an oop array (objArray)

开启压缩指针后,每个对象的头信息能节省 4B 空间,对象之间的相互引用也由原来的 8B 变成 4B,在java对象数量很多的场景下,节省的内存空间非常可观。

hotspot 使用 32bits 压缩指针后,依然能提升内存访问范围的方式,是在使用指针时左移三位,此时地址位提升到35bits,寻址空间也放大到32GB。

为什么压缩指针偏移地址是 3,因为压缩指针在JVM内部只影响 klass 对象和 oop 对象的引用,C++在 64bits 环境中分配对象的起始地址基于内存对齐的原则会是8的倍数(Why is dynamically allocated memory always 16 bytes aligned?),因此所有对象地址的低三位一定是0,hotspot在指针存储时就把0省去了。

hotspot在64bits环境下,堆内存小于 32GB 时默认开启压缩指针。

内存布局样式

e12e1c8d3358585032c9d84bf1b24b27.png
openJdk里定义如下

上文提到,java 对变量重排的实现是将相同类型变量连续存储,而 FieldsAllocationStyle 属性决定不同类型变量间的存储顺序,取值有三种:

  • 0:Fields order: oops, longs/doubles, ints, shorts/chars, bytes, padded fields
  • 1:Fields order: longs/doubles, ints, shorts/chars, bytes, oops, padded fields
  • 2:Fields allocation: oops fields in super and sub classes are together

其中 oops 是引用类型的变量。

style = 0 和 1 只是 oops 位置不同,容易理解。style = 2 时父类 oops 在本类的队尾,子类的 oops 在本类的队头,父子类 oops 在内存空间上连续,减少了 oopMap 里的记录数量。存在多层继承时,相邻两层 oops 连续,在不交错父子内存空间的前提下,尽量减少 oopMap记录数。

上文测试用例启动参数增加 -XX:FieldsAllocationStyle=2,输出为:

16:

看到 A 类引用变量和 B 类引用变量首尾相连,内存连续,C 类引用变量还在 C 类内存区域的尾部,如果还有其他类继承 C 类,会和 C 类再形成首尾相连的关系。

变量压缩

0be8f3a1229b2df2bdd11d60937d93bd.png
openJdk里定义如下

CompactFields 能够通过调整参数顺序,尽量填补 8 字节对齐造成的空白区域。

例如开启压缩指针后,klass 只占用 4 Bytes,FieldsAllocationStyle = 1且包含 double long 成员变量时,klass 和变量之间存在 4Bytes 的空白区域。此时优先插入非 oop 类型的数值类变量,没有数值变量时才插入 oop 变量,这样做是为了避免拆散多个 oop 而增加oopmap 表项。另外在 FieldsAllocationStyle = 0 时,oop 变量会被提前,4 Bytes oop和double变量之间可能产生 4 Bytes空白区,也能用其他短变量来填充。

伪共享

伪共享是指在一个缓存行内的不同变量,频繁的被两个线程同时修改,造成双方缓存行频繁失效,最终影响数据处理效率。处于伪共享状态的两个变量,操作开销与竞争处理同一个变量相同。

下表是MESI在伪共享状态下工作举例,所有操作引用同一缓存行 (例如: "R2" 值处理器2的读操作)

本地 请求P1P2产生的总线请求数据提供者
0最初----
1R1E-BusRdMem
2R2SSBusRdP1's Cache
3W1MIBusUpgr-
4W2IMBusRdXP1's Cache
5W1MIBusRdXP2's Cache
6W2IMBusRdXP1's Cache

hotspot 在 jdk8 里提供 @Contended 注解,被该注解标记的变量会在变量前后保持一个ContendedPaddingWidth 的空白,使得该变量不会和非同一竞争组的变量共享一个缓存行。

b8e5b626a87e2700e38cd934acacd544.png

只是 @Contended 注解默认只给 jdk 内部使用,使用 -XX:-RestrictContended 去除限制。

public 

因为标记了 @Contended 的变量会被排在本类的末尾,为了体现末尾的 padding,查询子类 B 的内存布局:

12:

看到char1和char2变量前后有超过128Bytes的空缺(受内存对齐影响)

如果标记char1和char2在同一个竞争组:

public 

此时,char1和char2可以共享缓存行

12:

变量布局获取示例

也可以用 jol-core。

import 

参考

CompressedOops - CompressedOops - OpenJDK

Wikihttp://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/classfile/classFileParser.cpp

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值