前阵子一个朋友去某大厂一面,被问了一道看起来特别基础的题:new Object() 在 JVM 里到底占多大内存?他脱口而出“8 字节”,因为印象中对象头就是 8 字节。面试官没肯定也没否定,又追问了一句:如果堆超过 32G 呢?如果指针压缩关掉呢?他当场就有点懵。
回来找我复盘,我说你第一步就答偏了:对象头不是 8 字节,Mark Word 才是 8 字节,对象头上还挂着类型指针。这道题表面问“new Object() 多大”,实际上是在考你有没有把 JVM 对象内存布局、指针压缩、对象对齐、JOL 工具这一整条链路搭起来。答好了,后面关于内存优化、GC 的问题基本都能顺着答下去。
1. 面试题到底在考什么
1.1 一句话答案先给出来
在大多数 64 位 JVM 默认参数下,new Object()的实际占用是16 字节。
拆开看是这样的:
| 部分 | 字节数 | 说明 |
|---|---|---|
| Mark Word | 8 | 存放 hashCode、GC 分代年龄、锁状态标志等 |
| 类型指针 | 4 | 开启指针压缩时,指向类元数据的指针为 4 字节 |
| 实例数据 | 0 | Object 没有字段 |
| 对齐填充 | 4 | 让对象大小成为 8 的倍数 |
也就是说对象头一共 12 字节,但 JVM 分配时会按 8 字节对齐,所以最终分配 16 字节。
这里最容易踩的坑是:把 Mark Word 当成整个对象头。Mark Word 只是对象头的一部分,真正的对象头还包含 Klass Pointer,也就是用来定位“这个对象到底是什么类”的指针。
1.2 为什么答案不是“一个固定值”
很多背过八股的人会直接背一句“new Object() 是 16 字节”,这本身没错,但不严谨。JVM 规范只规定了对象的“外部行为”,并没有强制规定每个对象的内存布局长什么样,布局是由具体的 JVM 实现自己定的。
所以同一个new Object(),在不同环境里可能是:
- 32 位 JVM:对象头 8 字节,总大小 8 字节。
- 64 位 JVM,开启指针压缩:对象头 12 字节,对齐后 16 字节。
- 64 位 JVM,关闭指针压缩:对象头 16 字节,总大小 16 字节。
如果对象还有字段,差异会更明显。比如一个带int字段的对象,开启指针压缩时是 12 + 4 = 16 字节,关闭指针压缩后是 16 + 4 = 20 字节,对齐后变成 24 字节,差距一下子就大了。
这道题真正想考察的,是你愿不愿意承认“JVM 参数会影响结论”,并且能不能给出可信的验证方式。
2. Object 对象在 JVM 里的真实内存布局
2.1 Mark Word 和类型指针各司其职
先捋一下对象布局的基本公式:
对象内存 = 对象头(Mark Word + 类型指针) + 实例数据 + 对齐填充
Mark Word 的 8 字节存了很多东西:对象的 hashCode、GC 分代年龄、偏向锁的线程 ID、轻量级锁和重量级锁的标志位。它更像是一块“多功能标签”,空间固定不变,内容会根据锁状态和 GC 状态来回变化。
类型指针也叫 Klass Pointer,指向方法区里的类元数据,JVM 靠它知道这个对象是Object还是ArrayList。开启指针压缩时是 4 字节,关闭后是 8 字节。
对于new Object()来说,没有实例数据,所以真正参与计算的就只有这两部分。
为什么 12 字节要补齐成 16?因为 HotSpot 系 JVM 默认要求对象起始地址按 8 字节对齐。牺牲 4 字节,换来的好处是 CPU 访问更快、GC 扫描对象更高效、缓存行利用更友好。这跟“内存以 8 字节为单位”的硬件设计非常相关。
2.2 不同 JVM 参数下的尺寸变化
我把常见场景整理成了表格,方便面试时直接念:
| 场景 | Mark Word | 类型指针 | 实例数据 | 对齐前 | 对齐后 |
|---|---|---|---|---|---|
| 64 位 JVM,开启指针压缩 | 8 | 4 | 0 | 12 | 16 |
| 64 位 JVM,关闭指针压缩 | 8 | 8 | 0 | 16 | 16 |
| 32 位 JVM | 4 | 4 | 0 | 8 | 8 |
注意一个细节:在 64 位 JVM 上,开启指针压缩时new Object()是 16 字节,关闭指针压缩后依然还是 16 字节。这不是巧合,而是因为空对象没有实例数据,少了压缩省下来的 4 字节又被对齐填充填回去了。
但一旦对象里有引用字段,指针压缩的效果就会立刻体现出来。比如:
class Holder { Object ref; }开启指针压缩时,Mark Word 8 + 类型指针 4 + 引用字段 4 = 16 字节,刚好对齐。
关闭指针压缩时,Mark Word 8 + 类型指针 8 + 引用字段 8 = 24 字节,每个持有引用字段的对象都比压缩时多 8 字节。
2.3 用 JOL 验证,而不是拍脑袋
面试中提到工具链,最稳的就是 JOL(Java Object Layout)。它可以直接输出一个对象在 JVM 里的具体布局。
下载 JOL 命令行工具后,可以这样跑:
java -jar jol-cli.jar internals java.lang.Object输出类似:
java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x000000... 8 4 (object header: class) 0x... 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total可以看到,JOL 明确告诉你“对象头”和“对齐损失”各是多少。面试时如果能说出“我用 JOL 验过”这句话,比单纯背答案要有说服力得多。
3. 从 Object 延伸到对象体的内存估算
3.1 普通 Java 对象实例怎么算
new Object()只是最简单的情况,面试官很喜欢立刻把问题升级到“那一个自定义对象呢?”
估算自定义对象时,流程很固定:
- 算对象头:默认 12 字节(开启压缩)或 16 字节(关闭压缩)。
- 算实例数据:把非静态字段占的字节数加起来。
- 对齐到 8 的倍数。
举个栗子:
class Point { int x; int y; }开启指针压缩时,对象头 12 字节,两个int一共 8 字节,加起来 20 字节,对齐后是 24 字节。
再比如:
class LongWrapper { long value; }对象头 12 +long8 = 20,对齐后 24。long本身要 8 字节对齐,所以这里也会有内部填充。
需要提醒的是,static 字段不属于实例数据,所有实例共享一份,不会算在单个对象头上。父类字段也要算进去,JVM 会把这些字段一并布局在当前对象里。
3.2 数组、String 这些“特殊对象”怎么算
数组也是一种对象,但对象头里多了一个length字段。所以数组的对象头默认是:
- Mark Word:8 字节
- 类型指针:4 字节(压缩时)
- 数组长度:4 字节
- 合计:16 字节
然后再加上实际元素占用的空间,最后对齐到 8 的倍数。
例如:
new byte[10]16 字节对象头 + 10 字节数据 = 26 字节,对齐后 32 字节。
new Object[10]16 字节对象头 + 10 个引用字段 × 4 字节 = 56 字节,刚好是 8 的倍数。
String更特殊,因为它内部还有一个byte[]数组字段。JDK 9 之后String通常由byte[] value、int hash、byte coder等组成,所以一个空String本身可能在 24 字节左右,还要再加上底层byte[]对象的开销,整体算下来一个空字符串就接近 40 多字节。
这里也是一个隐藏考点:很多人以为String就是几个字符的内存,其实它有两层对象。
3.3 对象头里的锁状态和 GC 标记为什么影响内存
Mark Word 的 8 字节里有锁状态和 GC 标记,但它不影响对象大小。无锁、偏向锁、轻量级锁、重量级锁,Mark Word 编码内容不同,但字段长度都一样是 8 字节。
GC 的分代年龄也记在 Mark Word 里,每次 YGC 后对象年龄加一,也是在固定空间里写数字,不会让对象变大。
如果在面试里答“加了锁对象会变大”,面试官大概率会皱眉。合理的说法是:锁状态的不同会改变 Mark Word 里几个比特位的含义,但不会改变对象头的大小。
4. 面试官真正想听的答题思路
4.1 先给结论,再给前提
面试官抛出问题后,不要急着报一个数字。最好的结构是:
“如果你们默认是 64 位 JVM、堆小于 32G、指针压缩开启,new Object() 实际占用 16 字节。对象头 12 字节,对齐后 4 字节填充。如果把指针压缩关闭,空 Object 本身也是 16 字节,但一旦有引用字段,会明显涨上去。”
这个回答有两层好处:
第一,先给了明确结论,面试官不用猜你卡在哪儿。
第二,通过“前提条件”展示你理解 JVM 参数对内存布局的影响,而不是背题。
4.2 证明你懂 JVM 参数和工具链
紧接着可以补一句:
“默认开启的是 UseCompressedOops 和 UseCompressedClassPointers,可以用 PrintFlagsFinal 确认。”
例如:
java -XX:+PrintFlagsFinal -version | grep -E "UseCompressedOops|UseCompressedClassPointers|ObjectAlignmentInBytes"然后进一步解释压缩指针的阈值:
“压缩指针把 64 位引用编码成 32 位偏移,对象默认按 8 字节对齐,所以最大能寻址 2^32 × 8 = 32G。堆超过 32G 之后,引用字段就没法继续压缩了。”
这段内容一旦说出来,基本就能和“背答案”的人拉开差距。
4.3 答好这题的三个细节
细节一:Mark Word 不等于对象头。很多人说“对象头 8 字节”,那是只记住了 Mark Word,忽略了 Klass Pointer。
细节二:对齐填充也是“真实占用”。JOL 输出的 “loss due to the next object alignment” 不代表没占用,分配器是真的会给这块空间的。
细节三:对象本身不等于引用。new Object()返回的是引用,引用在栈上可能 4 字节或 8 字节,但这不能算在堆对象占用里。面试如果把局部变量引用也算进去,概念就混了。
5. 我在实际项目中踩过的坑
5.1 new Object() 内存虽小,但架不住量大
有一个内存型服务,代码里用new Object()当锁对象标记业务状态,每次请求都 new 一个。线上堆里 Object 实例轻松堆到几百万,压测一上来 GC 秒秒钟疯狂。
算一下账:1000 万个new Object()就是 1000 万 × 16 字节 ≈ 160MB。4000 万就是 640MB。这个量级在普通 4G 堆里非常致命。
后来改成 static final 的锁对象,或者直接用并发工具类里的状态判断,内存立刻降下来。
这个场景很常见,很多性能问题不是“数据结构有多重”,而是“对象数量太多了”。
5.2 对齐填充带来的隐性浪费
再看另一种浪费:对象字段很小,却被垫了很多对齐字节。
比如:
class Status { boolean flag; }开启压缩时:对象头 12 + 1 = 13,对齐后 16,等于浪费了 3 个字节。
看起来每个对象才丢 3 字节,但如果你有上千万个小对象,就是 30MB 的隐形开销。这还不算每个对象本身的头部成本。
所以做内存优化时,脑子里要有“对象头 + 对齐”这个概念。很多时候把多个小字段合并成一个状态位,或者批量用数组存int,内存效果比堆一堆小对象好得多。
5.3 用 JOL 做内存分析的正确姿势
我在项目里做内存优化时,最常用的工具就是 JOL。用法很简单:
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.17</version> </dependency>然后在本地打印对象布局:
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());自定义对象也能看:
System.out.println(ClassLayout.parseClass(Point.class).toPrintable());JOL 会告诉你字段偏移、内部填充、外部对齐损耗,非常适合验证“我以为的对象大小”和“真实的对象大小”之间的差距。
注意一个使用习惯:线上不要随便对每个对象打印 JOL,会打爆日志。一般是在本地或者压测环境跑,拿到结论后再回到线上对比堆转储。
6. 常见追问与排查技巧实录
6.1 面试官可能继续问什么
面试官听完你的“16 字节”答案后,常见追问包括:
问:数组对象多大?
答:数组头默认 16 字节(Mark Word 8 + 类型指针 4 + 长度 4),再叠加元素实际大小和对齐。
问:为什么 32G 是个分界线?
答:压缩指针用 32 位表示对象偏移,对象按 8 字节对齐,所以理论上最大能表示 2^32 × 8 = 32G。堆超过这个值,普通对象引用就得回到 64 位,内存占用会明显增加。
问:String 在内存里怎么算?
答:String 对象本身加底层 byte[] 数组。byte[] 是数组对象,有自己的对象头和数据区,不能只按字符数量估。
问:锁状态会改变对象大小吗?
答:不会。Mark Word 大小固定,锁状态改变的是这 8 字节里某些位的含义。
6.2 对象占用内存问题速查表
| 对象 | 默认压缩时估算 | 说明 |
|---|---|---|
| new Object() | 16 字节 | 对象头 12 + 对齐填充 4 |
| class Point { int x, y; } | 24 字节 | 对象头 12 + 8 数据,对齐 |
| class Holder { Object ref; } | 16 字节 | 对象头 12 + 引用 4 |
| new byte[10] | 32 字节 | 数组头 16 + 10 数据,对齐 |
| new Object[10] | 56 字节 | 数组头 16 + 10 × 4 引用 |
| class BooleanBox { boolean b; } | 16 字节 | 对象头 12 + 1 数据,对齐 |
这张表不用背,关键是掌握计算方法。面试时临时算出来,比背结论更让人信服。
6.3 我自己答题时的经验
我在实际工作中最大的体会是:别把 Java 对象看成“字段的集合”,它至少有 12 到 16 字节的“包装成本”。很多内存问题,尤其是高频对象和小对象堆积,罪魁祸首不是字段本身,而是这份包装成本。
所以现在写代码,凡是看到有人用new Object()当锁或者当占位标记,我都会多想一秒:这个对象数量会不会膨胀?能不能用基本类型或者复用某个已有对象来代替?
这道题后来我复盘过很多次,发现它就像一把钥匙,能打开 JVM 内存布局、指针压缩、对齐规则、JOL 工具、数组和 String 的内存模型这一整套知识。如果你正在准备面试,哪怕没有被问到这题,我也建议你亲手跑一次 JOL,看看自己常用的几个类到底占了多少内存。这个动作本身,就能帮你避开不少线上内存陷阱。