跳至主要內容

JVM 八股5 - 内存管理4 (对象内存布局)

codejavajvm八股约 1556 字大约 5 分钟

对象内存布局

对象的内存布局是由 Java 虚拟机规范定义的,但具体的实现细节各有不同,如 HotSpot 和 OpenJ9 就不一样

常用的 HotSpot 为例:

对象在内存中包括三部分:对象头、实例数据和对齐填充

对象头 (Mark World + 类型指针 (指向 Class 对象)) + 实例数据 + 对齐填充

对象头 (8字节 + 8字节 (压缩 4 字节)): 12字节 (指针压缩)

对齐填充必须是 8 的倍数 (64位)

alt text
alt text

对象头作用

对象头是对象存储在内存中的元信息,包含了Mark Word、类型指针等信息

Mark Word 存储了对象的运行时状态信息,包括锁、哈希值、GC 标记等。

在 64 位操作系统下占 8 个字节,32 位操作系统下占 4 个字节。

Mark Word 的大小等于一个机器字长

32 位系统是 4 字节,64 位系统是 8 字节。

类型指针指向对象所属类的元数据,也就是 Class 对象,用来支持多态、方法调用等功能。

除此之外,如果对象是数组类型,还会有一个额外的数组长度字段。占 4 个字节。

类型指针会被压缩吗

类型指针可能会被压缩,以节省内存空间。比如说在开启压缩指针的情况下占 4 个字节,否则占 8 个字节。

在 JDK 8 中,压缩指针默认是开启的

可以通过 java -XX:+PrintFlagsFinal -version | grep UseCompressedOops 命令来查看 JVM 是否开启了压缩指针

实例数据

实例数据是对象实际的字段值,也就是成员变量的值,按照字段在类中声明的顺序存储

JVM 会对这些数据进行对齐/重排,以提高内存访问速度

对齐填充 (8 字节的倍数)

由于 JVM 的内存模型要求对象的起始地址是 8 字节对齐(64 位 JVM 中),因此对象的总大小必须是 8 字节的倍数。

如果对象头和实例数据的总长度不是 8 的倍数,JVM 会通过填充额外的字节来对齐。

比如说,如果对象头 + 实例数据 = 14 字节,则需要填充 2 个字节,使总长度变为 16 字节

因为 CPU 进行内存访问时,一次寻址的指针大小是 8 字节,正好是 L1 缓存行的大小。如果不进行内存对齐,则可能出现跨缓存行访问,导致额外的缓存行加载,CPU 的访问效率就会降低

new Object() 对象内存大小是多少

目前的操作系统都是 64 位的,并且 JDK 8 中的压缩指针是默认开启的

在 64 位的 JVM 上,new Object() 的大小是 16 字节(12 字节的对象头 + 4 字节的对齐填充)。

对象头的大小是固定的

  • 在 32 位 JVM 上是 8 字节
  • 在 64 位 JVM 上是 16 字节
    • 如果开启了压缩指针,就是 12 字节。

实例数据的大小取决于对象的成员变量和它们的类型。对于new Object()来说,由于默认没有成员变量,因此我们可以认为此时的实例数据大小是 0

假如 MyObject 对象有三个成员变量,分别是 int、long 和 byte 类型,那么它们占用的内存大小分别是 4 字节、8 字节和 1 字节

考虑到对齐填充,MyObject 对象的总大小为 12(对象头) + 4(a) + 8(b) + 1(c) + 7(填充) = 32 字节

JOL — 分析 JVM 对象工具

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.9</version>
</dependency>

简单示例

public class JOLSample {
  public static void main(String[] args) {
    // 打印JVM详细信息(可选)
    System.out.println(VM.current().details());

    // 创建Object实例
    Object obj = new Object();

    // 打印Object实例的内存布局
    String layout = ClassLayout.parseInstance(obj).toPrintable();
    System.out.println(layout);
  }
}

对象的引用大小

在 64 位 JVM 上,未开启压缩指针时,对象引用占用 8 字节;开启压缩指针时,对象引用会被压缩到 4 字节

HotSpot 虚拟机默认是开启压缩指针的。

JVM 怎么访问对象

主流的方式有两种:句柄和直接指针

两种方式的区别在于:

  • 句柄是通过一个中间的句柄表来定位对象的
  • 而直接指针则是通过引用直接指向对象的内存地址

句柄的优点是,对象被移动时只需要修改句柄表中的指针,而不需要修改对象引用本身

alt text
alt text

在直接指针访问中,引用直接存储对象的内存地址;对象的实例数据和类型信息都存储在堆中固定的内存区域

优点是访问速度更快,因为少了一次句柄的寻址操作。缺点是如果对象在内存中移动,引用需要更新为新的地址

HotSpot 虚拟机主要使用直接指针来进行对象访问。

alt text
alt text

对象引用方式

四种,分别是强引用、软引用、弱引用和虚引用。

alt text
alt text

强引用

强引用是 Java 中最常见的引用类型

使用 new 关键字赋值的引用就是强引用,只要强引用关联着对象,垃圾收集器就不会回收这部分对象,即使内存不足

软引用

软引用于描述一些非必须对象,通过 SoftReference 类实现。软引用的对象在内存不足时会被回收

// softRef 就是一个软引用
SoftReference<String> softRef = new SoftReference<>(new String("PPP"));

弱引用

弱引用用于描述一些短生命周期的非必须对象,如 ThreadLocal 中的 Entry,就是通过 WeakReference 类实现的

弱引用的对象会在下一次垃圾回收时会被回收,不论内存是否充足

虚引用

虚引用主要用来跟踪对象被垃圾回收的过程,通过 PhantomReference 类实现。虚引用的对象在任何时候都可能被回收

上次编辑于: