对象头由Mark Word和Class Pointer组成,在64位HotSpot JVM中默认压缩指针下占12字节(数组为16字节);它是所有集合实例的固定内存开销基础,直接影响整体内存占用估算。

Java对象头本身不直接决定集合实例的内存开销,但它作为每个对象(包括集合类如ArrayList、HashMap等)的固定组成部分,构成了所有集合实例的“基础内存税”。理解对象头的结构和大小,是准确估算集合实际内存占用的起点。
对象头由哪几部分组成?
在HotSpot虚拟机中(主流JDK默认),一个Java对象的内存布局包含三块:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。其中对象头又分为两部分:
- Mark Word(标记字段):存储哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等。在64位JVM上,未开启指针压缩时占8字节;开启指针压缩(-XX:+UseCompressedOops,默认开启)且未启用压缩类指针时,仍为8字节(HotSpot对Mark Word不做压缩)。
- Class Pointer(类型指针):指向该对象所属类的元数据(Klass结构体)。在64位JVM开启指针压缩时占4字节;未开启则占8字节。
因此,普通Java对象的对象头大小通常为:12字节(压缩指针)或16字节(非压缩)。注意:数组对象额外多4字节长度字段(length),所以数组对象头为16字节(压缩)或20字节(非压缩)。
集合实例的内存开销 = 对象头 + 字段 + 底层数组 + 对齐填充
以ArrayList<Integer>为例(JDK 17,64位JVM,默认开启压缩指针):
立即学习“Java免费学习笔记(深入)”;
- 对象头:12字节(Mark Word 8B + Class Pointer 4B)
- 实例字段:3个int型字段(
size、modCount)和1个Object[]引用(elementData)。其中两个int各占4B,引用占4B → 共12字节 - 底层数组:新创建的空ArrayList默认初始化
elementData = EMPTY_ELEMENTDATA(共享静态空数组),不额外分配数组对象;若调用ensureCapacity(10),则新建Object[10]数组 → 数组对象头16B + 10×4B元素 = 56B(需按8字节对齐,刚好无需填充) - 对象对齐:整个ArrayList对象自身需8字节对齐。当前头+字段=24B,已对齐,无需填充
所以一个空ArrayList(未扩容)仅占24字节;而一个装了10个Integer的ArrayList,其总内存≈24B(ArrayList对象)+ 56B(Object[10]数组)+ 10×16B(10个Integer对象,每个含12B头+4B value + 对齐)= 240B以上——其中对象头贡献了12B(ArrayList)+ 16B(数组)+ 10×12B(Integer)= 148B,占比超60%。
HashMap的开销更复杂,但对象头仍是“固定成本”
HashMap对象本身有对象头(12B)+ 若干字段(loadFactor、threshold、size、modCount、table引用、keySet等,约24–32B),合计约40B左右。但真正吃内存的是其Node[] table底层数组。即使空Map,JDK 8+ 默认延迟初始化,table = null,此时只占对象头+字段;首次put后才创建Node[16](数组对象16B头 + 16×Node对象)。每个Node是内部类对象:对象头12B + 4个字段(hash、key、value、next)共16B → 每个Node至少28B(需对齐到32B)。16个Node就额外增加512B。对象头在这里虽单个体量小,却是每个Node无法省略的“刚性开销”。
如何降低对象头带来的集合内存压力?
对象头大小由JVM决定,不可编程修改,但可通过设计减少对象数量:
- 优先使用基本类型集合库(如Trove、Eclipse Collections、FastUtil),它们用原始数组替代Object[],避免包装类对象头开销
- 对大量短生命周期小集合,考虑对象池复用(如Apache Commons Pool),摊薄对象头分配/回收成本
- 避免过度细粒度集合嵌套(如List<Map<String, List<Integer>>>),每层都新增对象头
- 确认是否真的需要引用语义:能用数组或扁平结构就不用嵌套集合
不复杂但容易忽略——对象头是每个Java对象的“身份证”,集合再轻量,也逃不开这12或16字节的入场费。算清这笔账,才能真正掌控内存水位。

















