一个 Java 对象到底有多大?—— 从内存对齐到字段重排的“精算师”之旅

📅 2026/7/5 1:04:46
一个 Java 对象到底有多大?—— 从内存对齐到字段重排的“精算师”之旅
你用jmap -histo看一眼堆内存发现一个看起来“很小”的对象居然占了好几十字节。你用new Object()创建了一个空对象它真的“空”吗你写了一个只有boolean字段的类觉得它应该只占 1 字节——结果 JVM 告诉你16 字节。这不是 JVM 在“浪费内存”而是它在遵守一套底层硬件的铁律内存对齐Memory Alignment。大家好我是Evan一个曾被jmap -histo显示的对象大小惊到、然后用jol-core揭开真相的 JavaAI 学生。今天我们从计算机组成原理的内存对齐讲起拆解 Java 对象在 HotSpot JVM 中的真实布局对象头、实例数据、对齐填充以及 JVM 如何通过字段重排帮你“省”内存。最后用jol-core亲手验证boolean和String的真实大小。读完这篇你再看到new Object()脑子里会浮现出一张精确到字节的内存账单。 写在前面在知识汇项目中我用jmap -histo分析内存占用时发现一个只有三个字段的简单 POJO 类单个对象居然占了 40 字节。我算了一下int(4) long(8) boolean(1) 13 字节怎么也不该是 40。后来用jol-core一看才发现 JVM 不仅加了对象头还把字段重新排了序最后用对齐填充把整个对象“撑”到了 8 的倍数。那一刻我才明白对象的大小不是字段大小之和而是一场由 CPU 内存访问规则主导的“精算游戏”。一、为什么要内存对齐—— 从 CPU 访存说起1.1 CPU 不是“按字节”读内存的现代 CPU 从内存读取数据时不是一次读 1 个字节而是以字word为单位——通常是 4 字节、8 字节甚至 16 字节。如果一个 8 字节的long类型字段恰好存放在地址0x03开始的位置未对齐CPU 可能需要两次内存访问才能读完这个long然后再拼接起来。而如果把它放在0x00或0x08这样对齐的地址上CPU 一次就能读完。这就是内存对齐的核心目的以空间换时间。1.2 JVM 的 8 字节对齐铁律HotSpot JVM 规定所有对象的大小必须是 8 字节的整数倍。如果对象头 实例数据正好是 8 的倍数就不需要填充。如果不是就在末尾补上若干字节凑成 8 的倍数。这个 8 字节对齐和 64 位 CPU 的访存宽度完美匹配。二、Java 对象的内存布局三块区域一个 Java 对象在 HotSpot JVM 中由三部分组成区域作用大小64位开启压缩对象头Header存储元数据Mark Word Klass Pointer12 字节实例数据Instance Data存储成员变量含父类继承的字段视字段而定对齐填充Padding保证总大小是 8 的倍数视情况而定2.1 对象头Mark Word8 字节 Klass Pointer4 字节Mark Word8 字节存储对象的运行时元数据——哈希码、GC 分代年龄、锁状态标志等。它是synchronized锁升级的“指挥部”。Klass Pointer4 字节指向方法区中该对象的 Class 元数据。64 位 JVM 默认开启-XX:UseCompressedClassPointers将原本 8 字节的指针压缩为 4 字节。空对象new Object()的大小开启指针压缩8 (Mark Word) 4 (Klass Pointer) 4 (对齐填充) 16 字节关闭指针压缩8 8 16 字节刚好对齐无需填充2.2 实例数据字段重排的“精算艺术”很多开发者以为字段按代码声明顺序存储。实际上JVM 会对字段进行重排序目标是最小化内存间隙提升内存利用率。HotSpot 默认的字段分配优先级从先到后long/double8 字节int/float4 字节short/char2 字节byte/boolean1 字节引用类型开启压缩 4 字节未开启 8 字节同时父类的实例字段永远排在子类字段之前。看一个例子java class Demo { byte a; // 1 字节 long b; // 8 字节 int c; // 4 字节 }如果按声明顺序存放text [header 12B][a 1B][padding 7B][b 8B][c 4B][padding 4B] 36B → 对齐到 40BJVM 字段重排后先放long再放int最后放bytetext [header 12B][b 8B][c 4B][a 1B][padding 3B] 28B → 对齐到 32B省了 8 个字节三、用jol-core亲手验证眼见为实3.1 引入依赖xml dependency groupIdorg.openjdk.jol/groupId artifactIdjol-core/artifactId version0.10/version /dependency3.2 查看 JVM 基本信息java System.out.println(VM.current().details());输出64位 HotSpot开启压缩指针text # 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]关键信息引用类型压缩后4 字节boolean/byte1 字节int/float4 字节long/double8 字节3.3 验证boolean的真实大小java class BooleanWrapper { private boolean value; } System.out.println(ClassLayout.parseClass(BooleanWrapper.class).toPrintable());输出text 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结论一个只有boolean字段的对象占16 字节——对象头 12 字节 boolean1 字节 对齐填充 3 字节。那boolean本身呢作为字段或数组元素它占1 字节8 bit不是 1 bit。为什么不用 1 bit因为 CPU 无法原子操作单个 bit。如果多个boolean共享一个字节修改其中一个可能“误伤”相邻的 bit——这叫Word Tearing字撕裂。JVM 规范明确禁止这种现象。3.4 验证String的内部结构java System.out.println(ClassLayout.parseClass(String.class).toPrintable());String是一个“重量级”对象。在 JDK 9 中它内部包含byte[] value存储字符串内容引用4 字节byte coder编码标识1 字节int hash哈希值4 字节一个空字符串new String()的真实大小String对象本身对象头 12B byte[]引用 4B coder1B hash4B 对齐填充 24 字节内部的空byte[]数组数组对象头 16BMark Word 8B Klass Pointer 4B 数组长度 4B 对齐填充 16 字节总计40 字节一个看起来“空”的字符串实际占用了40 字节四、开发中的实战意义4.1jmap -histo看到的和预想不一样因为对象大小 对象头 实例数据经字段重排 对齐填充。算的时候别忘了这三部分。4.2 如何减少内存占用将大字段long/double放在前面声明——虽然 JVM 会重排但某些场景下如通过 JNI 或序列化声明顺序仍有影响。使用Contended注解需开启-XX:-RestrictContended可以在字段前后插入填充避免伪共享False Sharing但会增加内存占用。慎用String一个空字符串就占 40 字节大量小字符串会迅速撑爆堆内存。4.3 指针压缩的影响64 位 JVM 默认开启-XX:UseCompressedOops将引用从 8 字节压缩为 4 字节。如果堆内存超过32GB指针压缩会自动关闭每个引用变成 8 字节对象大小会显著增加。 总结概念说明对 Java 对象的影响内存对齐CPU 以字为单位访存对齐可减少访问次数JVM 要求对象大小是 8 的倍数对象头Mark Word (8B) Klass Pointer (4B压缩后)每个对象至少 12 字节字段重排JVM 按大小从大到小排列字段减少填充节省内存对齐填充末尾补 0 凑 8 的倍数可能导致“看不见”的额外字节boolean字段/数组元素占 1 字节不是 1 bit防止 Word TearingString对象本身 内部 byte[] 数组空字符串也占 40 字节核心结论Java 对象的大小 对象头 实例数据经字段重排 对齐填充。boolean占 1 字节不是 1 bit因为 CPU 无法原子操作单个 bit。new Object()占 16 字节new String()占 40 字节——每一个对象都比你想的“重”。jol-core是揭开对象内存布局真相的“X 光机”。思考题你有一个高并发服务每秒创建数百万个new Object()作为临时“标记”对象。你发现 GC 压力很大想通过减少对象大小来缓解。你想到一个“聪明”的办法用boolean数组代替大量独立对象——把 100 个标记位放在一个boolean[100]里。问题一个boolean[100]数组占多少内存相比创建 100 个独立的BooleanWrapper对象能节省多少内存提示数组对象头 16 字节 每个 boolean 元素 1 字节 对齐填充欢迎在评论区留下你的计算 —— 下一篇我会聊聊“CPU 缓存行与伪共享你的并发 Map 为什么越跑越慢”