boolean 与boolean数组内存布局-Java快速进阶教程
1. 概述
在这篇快速文章中,我们将看到在不同情况下 JVM 中布尔值的足迹是多少。
首先,我们将检查 JVM 以查看对象大小。然后,我们将了解这些尺寸背后的基本原理。
扩展阅读
Java字节码-Java快速进阶教程
JVM规范定义运行时数据区详解-Java快速进阶教程
什么是 Java 中的 JVM-Java快速进阶教
什么是 JRE-Java快速进阶教程
什么是 Java 编译器-Java快速进阶教程
java垃圾回收机制GC(Garbage Collection)-Java快速进阶教程
2. 设置
为了检查 JVM 中对象的内存布局,我们将广泛使用 Java 对象布局 (JOL)。因此,我们需要添加jol-core依赖项:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
3. 对象尺寸
如果我们要求 JOL 根据对象大小打印虚拟机详细信息:
System.out.println(VM.current().details());
启用压缩引用(默认行为)后,我们将看到输出:
# 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个字节,布尔/字节是1个字节,char/short是2个字节,int/float是4个字节,最后,long /double是8个字节
- 即使我们将这些类型用作数组元素,它们也会消耗相同的内存量
因此,在存在压缩引用的情况下,每个布尔值占用 1 个字节。同样,布尔数组 中的每个布尔值消耗 1 个字节。但是,对齐填充和对象标题会增加布尔和布尔数组占用的空间,我们将在后面看到。
3.1. 无压缩引用
即使我们通过 XX:-UseCompressedOops 禁用压缩引用,布尔大小也不会改变:
# 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 位。
3.2. 单词撕裂
在大多数体系结构中,没有办法以原子方式访问单个位。即使我们想这样做,我们可能会在更新另一个位的同时写入相邻的位。
JVM的设计目标之一就是防止这种现象,即所谓的单词撕裂。也就是说,在JVM中,每个字段和数组元素都应该是不同的;对一个字段或元素的更新不能与任何其他字段或元素的读取或更新交互。
概括一下,可寻址性问题和单词撕裂是布尔值不止一个比特的主要原因。
4. 普通对象指针 (OOP)
现在我们知道布尔值是 1 个字节,让我们考虑这个简单的类:
class BooleanWrapper {
private boolean value;
}
如果我们使用 JOL 检查此类的内存布局:
System.out.println(ClassLayout.parseClass(BooleanWrapper.class).toPrintable());
然后 JOL 将打印内存布局:
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 个字节,包括两个标记字和一个klass字。HotSpot JVM使用标记字来存储GC元数据,身份哈希码和锁定信息。此外,它还使用klass字来存储类元数据,例如运行时类型检查
- 1 字节的实际布尔值
- 3字节的填充用于对齐目的
默认情况下,对象引用应按 8 个字节对齐。因此,JVM 将 3 个字节添加到 13 个字节的标头和布尔值中,使其成为 16 个字节。
因此,布尔字段可能会因其字段对齐而消耗更多内存。
4.1. 自定义对齐方式
如果我们将对齐值更改为 32 via-XX:ObjectAlignmentInBytes=32,则相同的类布局将更改为:
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. Array OOPs
让我们看看 JVM 如何在内存中布置一个布尔数组:
boolean[] value = new boolean[3];
System.out.println(ClassLayout.parseInstance(value).toPrintable());
这将打印实例布局,如下所示:
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 means boolean array
19 5 (loss due to the next object alignment)
除了两个标记字和一个klass字外,数组指针还包含额外的 4 个字节来存储它们的长度。
由于我们的数组有三个元素,因此数组元素的大小为 3 个字节。但是,这 3 个字节将填充 5 个字段对齐字节以确保正确对齐。
尽管数组中的每个布尔元素只有 1 个字节,但整个数组消耗的内存要多得多。换句话说,在计算数组大小时,我们应该考虑标头和填充开销。
6. 结论
在这个快速教程中,我们看到布尔字段消耗 1 个字节。此外,我们了解到我们应该考虑对象大小的标题和填充开销。
对于更详细的讨论,强烈建议查看JVM 源代码的 oops 部分。此外,阿列克谢·希皮利夫在这方面有一篇更深入的文章。