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

深入学习Java基础知识
2021-05-17 14:29 · 阅读时长7分钟
小课

本文学习一下在不同的环境下,boolean类型的的值在JVM中的内存占用,首先我们借助工具查看Java运行时某些对象占用的内存大小,然后再分析其中的原理。

下面通过gradle引入jol-core,它的全称是Java Object Layout Core,一个用来分析JVM中对象内存布局的工具。

implementation 'org.openjdk.jol:jol-core:0.16'

引入成功之后,先使用JOL打印关于不同类型的对象占用的内存情况,你可以点击下方运行代码按钮来运行代码和查看输出结果。

import org.openjdk.jol.vm.VM;

public class Main {
    public static void main(String[] args) {
        System.out.println(VM.current().details());
    }
}
注意: 这个Java运行环境不支持自定义包名,并且public class name必须是Main

如果你本地也是运行在一台64位的计算机,且开启了压缩指针(Compressed OOPs)选项的话,会看到和以上一样的输出,前几行输出的是关于VM的基本信息,下面第一行输出的数字分别表示的是,Java引用类型占4个字节,boolean和byte类型占1个字节,char和short类型占2个字节,int和float类型占4个字节,long和double类型占8个字节,第二行表示的是当以上类型作为数组元素时的占用大小,这里的输出说明当作为数组元素和直接使用占用的内存大小一样。

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]

在64位环境下,JVM默认开启压缩指针,可通过-XX:-UseCompressedOops参数关闭

注释

通过上面的输出我们可以看出,在开启压缩指针的情况下,一个boolean类型占用1个字节,那么如果在没有开启压缩指针的情况下,boolean还是占用一个字节吗?通过设置-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中的引用类型占用8个字节,比之前多了一倍,但是boolean类型还是占用一个字节,其他基本类型的内存占用也没有变化。

现在确定了boolean类型占用1个字节,那么我们思考一个问题,boolean类型只有true/false两个值,1个bit就能存放,为什么要设计用1个字节来存储boolean类型呢

在思考这个问题之前,我们先了解一个概念Word Tearing,通俗的解释就是说假如某个处理器不支持单字节写入,那么我们通过直接读取整个字,然后更新其中一个字节,再把整个字写入,这么做就可能会出现问题,这个问题就被称为Word Tearing。不过现在应该没有多少处理器不支持单字节写入了,所以Word Tearing的问题应该很少了,但是现在很多处理器还是不支持单bit写入,如果boolean类型只占一个bit的话,那么在更新它的时候,势必需要读取整个byte,然后更新其中一个bit再写入,这样就会造成类似Word Tearing的问题。

boolean类型是占有1个字节,但是在实际类的使用中,我们多定义一个boolean类型的值,真的只会增加1字节的开销吗?下面定义一个包含boolean类型值的简单类,然后使用JOL打印它们的内存布局情况。

1import org.openjdk.jol.info.ClassLayout;
2
3class BooleanWrapper {
4    private boolean value;
5}
6
7public class Main {
8
9    public static void main(String[] args) {
10        System.out.println(
11                ClassLayout.parseClass(BooleanWrapper.class).toPrintable()
12        );
13    }
14}
注意: 这个Java运行环境不支持自定义包名,并且public class name必须是Main

从输出中可以看出总共占用16个字节,其中

  • 对象头部的Mark Word占8个字节,主要用于标记对象的状态,比如锁的状态信息,是轻量级锁、偏向锁还是重量级锁,还有hashCode值以及GC标识等等。
  • 对象头部的Klass Pointer占4个字节,用于记录当前实例所属的Class实例的指针。
  • 咱们定义的boolean类型成员变量,占1个字节。
  • 内存对齐填充,不足8字节(可通过-XX:ObjectAlignmentInBytes)的倍数,需要填充。

正因为内存对齐的原因,boolean类型消耗的内存可能往往会1字节多。当然上面这个类就算没有boolean类型的字段,它仍然会占16个字节,它仅仅是说明由于内存对齐的存在,所以boolean类型可能会需要额外消耗。

下面看看boolean数组的内存布局情况。

import org.openjdk.jol.info.ClassLayout;

public class Main {

    public static void main(String[] args) {
        boolean[] value = new boolean[3];
        System.out.println(ClassLayout.parseInstance(value).toPrintable());
    }
}
注意: 这个Java运行环境不支持自定义包名,并且public class name必须是Main

如果运行环境是64位且开启压缩指针的情况下,会得到如下输出

BooleanWrapper object internals:
OFF  SZ      TYPE DESCRIPTION               VALUE
  0   8           (object header: mark)     N/A
  8   4           (object header: class)    N/A
 12   1   boolean BooleanWrapper.value      N/A
 13   3           (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

从输出中可以看出,除了Mark Word和Klass Pointer,对象头部还多了4个字节,用于记录数组的长度。数组中3个boolean元素占3个字节,末尾5个字节用于内存对齐填充。

至此我们可以总结boolean类型会占用1个字节的内存,但是需要注意的是,由于对象头和内存对齐填充的原因,其真实的开销可能要比预计的多一点。

memory layout内存布局booleanWord Tearing