Boolean数组在Jvm中占多少B内存?浅谈对象在内存中的结构

122 阅读7分钟

先从最朴素的想法开始:对象长啥样?

咱们写Java代码的时候,new一个对象扔那儿就完事儿了,比如new Object()或者new MyClass(),但你有没有想过,这个对象在JVM里到底是个啥模样?简单点想,对象总得有点“身份信息”吧,得告诉JVM这是啥玩意儿,还有它里头装的数据,对不对?所以,咱们可以猜猜,一个Java对象大概分成两部分:一个是“头”,告诉JVM“我是谁”,另一个是“身子”,装着具体的数据。

对象头里有什么呢?最直白地想,得有个标记告诉JVM这是啥类型的对象,比如“我是String”或者“我是ArrayList”,不然JVM咋知道咋处理它?再想想,可能还得有点状态信息,比如对象有没有被锁住(多线程里synchronized得用吧)。至于数据部分,就是咱们定义的字段了,比如一个int,一个boolean啥的。

这么想好像挺合理,但问题来了:JVM真是这么简单粗暴地存对象吗?内存咋分配?boolean这种小东西占多大地方?咱们一步步挖下去。

对象头的真相:不只是“身份证”

先说对象头。JVM里用HotSpot实现的话(毕竟这是主流),对象头其实没那么简单。它分成俩主要部分:Mark WordClass Pointer,有时候还可能多个锁记录

  • Mark Word:这个是个“多面手”,大小一般是32位(32位JVM)或者64位(64位JVM)。它存啥呢?看情况!平时可能是对象的hashCode,或者垃圾回收的标记(比如分代信息),如果对象被锁住了,它就变成锁状态的记录,比如轻量级锁的指针,或者重量级锁的Monitor地址。总之,这家伙是个动态的“身份牌”,内容随着对象状态变。
  • Class Pointer:这个是指向类的元数据的指针,告诉JVM这个对象是哪个类的实例。32位系统里占32位,64位系统占64位。不过呢,HotSpot有个优化叫压缩指针-XX:+UseCompressedOops),64位系统里可以压到32位,省一半空间。

所以,对象头多大?假设64位JVM,没开压缩指针,Mark Word 64位 + Class Pointer 64位 = 128位,也就是16字节。开了压缩指针呢?Mark Word 64位 + Class Pointer 32位 = 96位,12字节。这还不算锁记录,多线程用synchronized可能会再加点东西。

数据部分:字段怎么存?

对象头搞明白了,接下来看数据部分,也就是实例字段。比如我定义个类:

class Demo {
    int a = 42;
    boolean b = true;
}

这个a是int,32位,4字节,很好理解。b是boolean,理论上1位就够了(true/false嘛),但JVM会直接给它分配1字节,也就是8位。为什么?因为现代计算机内存是以字节为单位寻址的,1位没法单独操作,太麻烦了。所以,一个boolean字段占1字节。

但这只是单个对象的情况。如果是boolean数组呢?比如boolean[] arr = new boolean[10],会咋样?数组也是对象,也有对象头,但数据部分是连续的boolean值。按理说10个boolean应该是10字节,但真是这样吗?咱们得考虑内存对齐。

内存对齐:为啥要“凑整”?

说到内存对齐,咱们得聊聊JVM的一个习惯。HotSpot里,对象总大小得是8字节的倍数。为什么?因为CPU处理数据的时候,喜欢按8字节(64位)对齐访问,效率更高。如果不对齐,CPU可能得多跑几趟,性能就掉下去了。

回头看看刚才的Demo类:

  • 对象头(64位JVM,压缩指针):12字节
  • int a:4字节
  • boolean b:1字节
  • 总共:12 + 4 + 1 = 17字节

17不是8的倍数啊!JVM咋办?它会自动填充(padding),补3字节空位,让总大小变成24字节(8的3倍)。这3字节啥也不干,就是占地方。

再看boolean数组,new boolean[10]

  • 对象头:12字节(压缩指针)
  • 数组长度字段:4字节(int,表示元素个数)
  • 数据:10个boolean,10字节
  • 总共:12 + 4 + 10 = 26字节

26也不是8的倍数,JVM会补6字节,凑到32字节。这么一看,内存对齐带来的“浪费”还挺明显,尤其是小对象或者短数组。

朴素策略的问题:在哪儿卡住了?

从这个朴素的分析看,对象头固定开销不小(12字节起步),字段按字节存还得对齐,boolean这种小东西直接膨胀到1字节,数组还得加长度字段。问题暴露出来了:

  1. 空间浪费:对象头和padding占了不少地方,小对象效率低。
  2. boolean效率低:1位的数据硬塞进8位,太奢侈了,尤其是大量boolean时。
  3. 对齐开销:补的那几字节纯属“摆设”,没实际用处。

特别是boolean数组,10个元素占32字节,平均一个3.2字节,离理论上的1位差太远了。这要是boolean多了,比如存100万个开关状态,得有多大浪费啊?咱们得想想咋优化。

优化方向:逼近现代方案

基于这些问题,咋改进呢?咱们从朴素策略出发,逐步靠近JVM和现代编程的聪明办法。

  1. 压缩对象头
    刚才说了,-XX:+UseCompressedOops能把Class Pointer从64位压到32位,对象头从16字节变12字节。这已经是HotSpot默认开的优化了,挺香。但Mark Word还能不能再压压?其实不行,因为它得存锁和GC信息,太压缩会影响功能。

  2. 字段布局调整
    对单个对象,boolean占1字节还得padding,能不能塞更多字段,减少空位?比如:

    class Demo2 {
        int a = 42;
        boolean b1 = true;
        boolean b2 = false;
        boolean b3 = true;
    }
    

    对象头12字节,int 4字节,3个boolean 3字节,总共19字节,补5字节到24字节。比单个boolean省了点padding,但还是浪费。现代JVM有个思路:字段重排序,尽量把小字段放一起,减少对齐空隙。不过这得靠JVM自己调,开发者控制不了。

  3. boolean数组的终极武器:BitSet
    boolean数组是重灾区,咋办?JVM里有个神器叫BitSet,专门干这个活儿。它用byte数组存数据,但每个byte能表示8个boolean,用位操作(bit operation)访问。比如new boolean[10]占32字节,换成BitSet

    • 10个boolean,2字节就够(16位),加上点管理开销,远小于32字节。
    • 100万个boolean,理论上125000字节(100万位 ÷ 8),比100万字节省一大截。

    BitSet的核心是位压缩,把1字节8位用满,完美贴近理论极限。现代框架里,存大量标志位都爱用这个。

  4. 自定义内存管理
    如果BitSet还不够极致,可以自己动手。比如用long[]存boolean,一个long 64位,能表示64个状态。100万个boolean只要15625个long(100万 ÷ 64),总共125000字节,跟BitSet差不多,但你能精细控制布局。这种思路在高性能场景(比如游戏引擎)很常见。

总结:从简单到精妙

从最朴素的“对象头+数据”想法,到挖出对象头细节(Mark Word、Class Pointer)、内存对齐,再到boolean的内存浪费,咱们一步步逼近了问题本质。朴素策略暴露的空间效率问题,通过压缩指针、字段调整、BitSet、甚至手写位操作逐步解决。这些优化方向,跟JVM和现代库的实现不谋而合:尽量榨干每一位的空间,减少无用填充。