在 JVM 中的 boolean 和 boolean[] 内存布局

转自 boolean and boolean[] Memory Layout in the JVM

1. 概述

在这篇快速文章中,我们将探讨JVM中不同情况下boolean值的占用内存情况。

首先,我们将检查JVM以了解对象大小。然后,我们将理解这些大小背后的原因。

2. 设置

为了检查JVM中对象的内存布局,我们将广泛使用Java对象布局(JOL)。因此,我们需要添加jol-core依赖项:

1
2
3
4
5
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>

3. 对象大小

如果我们要求JOL以对象大小打印VM的详细信息:

1
System.out.println(VM.current().details());

当启用压缩引用(默认行为)时,我们将看到输出:

1
2
3
4
5
6
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

在前几行中,我们可以看到有关VM的一些常规信息。然后,我们了解到对象大小:

  • Java引用占用4字节,boolean/byte占用1字节,char/short占用2字节,int/float占用4字节,long/double占用8字节
  • 即使我们将它们用作数组元素,这些类型的内存消耗量也是相同的

因此,在存在压缩引用的情况下,每个boolean值占用1字节。同样,boolean[]中的每个boolean也占用1字节。 但是,对齐填充和对象头可能会增加boolean和*boolean[]*所占用的空间,后面我们会看到这一点。

3.1. 禁用压缩引用

即使我们通过*-XX:-UseCompressedOops*禁用了压缩引用,boolean的大小将完全不会改变

1
2
# Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

另一方面,Java引用占用的内存是之前的两倍。

因此,尽管我们可能首先期望的是布尔值占用1位而不是1字节,但实际上它们占用了1字节。

3.2. Word Tearing

在大多数架构中,没有一种方法可以原子地访问单个位。即使我们想这样做,我们可能在更新一个位时同时写入相邻的位。

**JVM的设计目标之一是防止这种现象,称为Word Tearing**。也就是说,在JVM中,每个字段和数组元素都应该是独立的;对一个字段或元素的更新不得与任何其他字段或元素的读取或更新交互。

简而言之,地址可寻址性问题和Word Tearing是布尔值占用不止一个位的主要原因。

4. 普通对象指针(OOPs)

现在我们知道boolean字段占用1字节,让我们考虑这个简单的类:

1
2
3
class BooleanWrapper {
private boolean value;
}

如果我们使用JOL检查这个类的内存布局:

1
System.out.println(ClassLayout.parseClass(BooleanWrapper.class).toPrintable());

然后JOL将打印出内存布局:

1
2
3
4
5
6
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
0 12 (object header) N/A
12 1 boolean BooleanWrapper.value N/A
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

BooleanWrapper布局包括:

  • 12字节的头部,包括两个*mark字和一个klass字。HotSpot JVM使用mark字来存储GC元数据、标识哈希码和锁定信息。此外,它使用klass*字来存储类元数据,如运行时类型检查
  • 1字节用于实际的boolean
  • 3字节的填充用于对齐目的

默认情况下,对象引用应该按8字节对齐。因此,JVM在13字节的头部和boolean后面添加了3字节的填充,使其成为16字节。

因此,boolean字段可能会因为它们的字段对齐而占用更多的内存。

4.1. 自定义对齐

如果我们通过*-XX:ObjectAlignmentInBytes=32*将对齐值更改为32,那么相同类的布局将变为:

1
2
3
4
5
6
7
8
OFFSET

SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 1 boolean BooleanWrapper.value N/A
13 19 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 19 bytes external = 19 bytes total

如上所示,JVM添加了19字节的填充以使对象大小为32的倍数。

5. 数组OOPs

让我们看一下JVM如何在内存中布局boolean数组:

1
2
boolean[] value = new boolean[3];
System.out.println(ClassLayout.parseInstance(value).toPrintable());

这将打印出实例布局如下:

1
2
3
4
5
6
7
OFFSET  SIZE      TYPE DESCRIPTION
0 4 (object header) # mark word
4 4 (object header) # mark word
8 4 (object header) # klass word
12 4 (object header) # array length
16 3 boolean [Z.<elements> # [Z表示boolean数组
19 5 (loss due to the next object alignment)

除了两个mark字和一个klass字,**数组指针还包含额外的4字节来存储它们的长度。**

由于我们的数组有三个元素,数组元素的大小为3字节。然而,这3字节将被填充5个字段对齐字节,以确保正确的对齐。

尽管数组中每个boolean元素只有1字节,但整个数组占用了更多的内存。换句话说,我们在计算数组大小时应考虑头部和填充开销。

6. 结论

在这篇快速教程中,我们看到boolean字段占用1字节。此外,我们学习了在计算对象大小时应考虑头部和填充开销。

如需更详细的讨论,强烈建议查看JVM oops源代码部分。此外,Aleksey Shipilëv在这个领域有一篇更深入的文章

如往常一样,所有的示例都可以在GitHub上找到