浅谈JVM系列之对象内存布局

浅谈JVM系列之对象内存布局

Tans 1,289 2023-02-26

JVM系列之对象内存布局

在HotSpot虚拟机里,对象在内存中的存储布局可以划分为三个部分:对象头(header)、实例数据(Instance Data)和对齐填充(Padding),如图所示:

image-20230226000007071

各部分作用

一、Header(对象头)

Header是 对象结构的第一部分,通常包括实例对象的Markword、类元指针、长度字段(如果是数组对象)。同时在64位和32位操作系统下,他们占用的空间也不一样,下面我们逐一介绍:

image-20230225234311423

image-20230226002415370

1. Markword

主要包括锁信息、Hashcode、GC年龄、锁状态标志、线程持有的锁、偏向线程的ID、偏向时间戳等等。特别的,这一部分和Java中的锁息息相关。同时在Markword中,为了使得空间利用最大化,Hotspot采用不同的标志位来代表当前对象处于不同的状态,并且代表不同的数据解读方式。

例如32位Java虚拟机中的Markword格式:

image-20230225201531688

同时,下图是64位虚拟机中Markword格式,注意,64位Java虚拟机中的Markword占用8byte

img

2. Klass Pointer

类型指针,这里指向类型元数据的指针,JVM通过该指针确定该对象是哪个类的实例。其中,32位JVM为32位,64位JVM为64位,同时,在64位系统中,可以开启JVM类型压缩参数-XX:-UseCompressedOops来启用指针压缩。通过下图可以看到,虚拟机通过这个指针指向方法区的Klass对象:

img

3. Length(可选)

此部分只在对象类型是数组类型的情况下使用,该字段表明数组的长度。如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位

二、Instance Data(实例数据)

这一部分存储对象真正有效的信息,也就是程序代码里面定义的各种类型的字段。无论是从父类继承下来的,还是子类定义的都将存放在此部分。一个实例对象中通常包含多个引用型和基本数据类型变量,它们在的JVM中分配的顺序遵循以下规则:

  • 默认按照类型,longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs)进行排序分配
  • 在满足上述条件情况下,父类定义的变量会出现在子类之前。也可以使用一些参数来进行优化,例如:
#相关参数
+XX:CompactFields #子类之中较窄的变量也允许插入父类变量的空 隙之中,以节省出一点点空间。默认true

三、Padding(对齐填充)

第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。对齐填充的思想在很多领域都有用到,例如TCP报文段等等。在JVM中,对齐填充也是指针压缩压缩的前提,下面我们会深入分析

疑问解答

1. new Object()产生的对象会占用多大内存?

首先我们要搞清楚JVM虚拟机是多少位?以64位为例:

  • Header
    • Markword : 8 byte (如果是32位系统,这里占用4 byte)
    • Klass Pointer : 4 byte (默认开启指针压缩)
  • Instance Data: 0 byte
  • Padding : 4 byte (对象填充 8 byte 整数倍)

故在64位操作系统环境下共占用 16 byte

2. 为什么需要指针压缩技术

在Klass Pointer中,我们提到了指针压缩技术,那么为什么需要这一项技术呢? 对比开启指针压缩前后,会发现64位操作系统下,原来实例指针大小从原来的 8byte 压缩到了 4 byte, 这样以来,我们的实例对象头所占用的堆空间就减小了,不仅节约了堆空间,也可以缓解GC压力。JVM在1.6以后默认启用指针压缩功能。

 #指针压缩的相关参数
 -XX:+UseCompressedOops           #默认开启的压缩所有指针
 -XX:+UseCompressedClassPointers  #默认开启的压缩对象头里的类型指针Klass Pointer

3. 开启指针压缩,那么压缩后的指针是如何在64位操作系统寻址的?

首先容易理解的是,按字节编址,那么32 bit最多寻址的单元是4GB ,而JVM采用了对象填充这一特性,也就是最少分配单元是8B, 那么我们就无需记录实例对象的真实内存地址,而是使用压缩编码的方式来确定其在第几个8B, 因此我们的 32bit就可以寻址 2^32 * 8B = 32GB地址。因此压缩编码只有在JVM堆内存在32GB以下是有效的,得出结论:

  • 当堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
  • 当堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址, 那这样的话内存占用较大,GC压力等等

因此,在64位操作系统下,JVM的堆内存最好不要超过32GB,因为可能会导致指针压缩功能失效。

总结

本文中,首先解释了JVM实例对象在堆内存中的存储结构分别为:

  1. Header
    1. Markword
    2. Kclass Pointer
    3. Length
  2. Instance Data
  3. Padding

其中,在不同操作系统环境的的JVM中,字段大小有些差异。接着,我们着重分析了指针压缩这项技术所带来的好处以及其实现原理。同时也对Markword这一字段做了深入了解,可以看出 JVM团队在具体实现中在内存占用方面和性能方面所做的非常巧妙的设计和优化。

参考资料

  1. 对象压缩
  2. 对象压缩实现
  3. Java对象头详解
  4. Markword源码
  5. 《深入理解JVM虚拟机》. 周志明. 2.3.2
  6. JVM - 剖析Java对象头Object Header之指针压缩
  7. Java对象内存布局和对象头