[面试题] JVM 如何保证对象分配内存过程的线程安全?一个对象的结构是什么样的?
📖 版块 A:知识讲解版 (温故知新)
-
对象分配为什么会有线程安全问题:
- 堆是线程共享区域。
- 多个线程同时分配对象时,如果没有控制,就可能发生地址重叠和数据覆盖。
-
JVM 常见保障手段:
- CAS + 失败重试:在共享分配区中通过原子操作更新指针。
- TLAB(Thread Local Allocation Buffer):给每个线程在 Eden 中划一小块私有分配缓冲区,绝大多数对象先在这里快速分配。
-
一个对象的典型结构:
-
对象头:
- Mark Word:包含哈希码、GC 年龄、锁状态等。
- 类型指针:指向类元数据。
- 数组对象还会额外记录数组长度。
-
实例数据:对象真正的字段内容。
-
对齐填充:为了满足内存对齐要求,可能补齐若干字节。
-
🎤 版块 B:面试实战版 (高手逐字稿)
Q:JVM 如何保证对象分配内存过程的线程安全?一个对象的结构是什么样的?
💬 高手逐字稿:
JVM 之所以需要考虑对象分配的线程安全,是因为堆本身是线程共享的。多个线程如果同时 new 对象,而内存指针又没有被正确同步,就可能出现两个线程分配到同一段地址的灾难性问题。
常见的解决方案主要有两个。
第一个是 CAS + 重试。也就是在共享分配区里,用原子操作去更新内存分配指针。如果竞争失败,就自旋重试。这种方式比较通用,但并发很高时会有冲突成本。
第二个是 TLAB,也就是线程本地分配缓冲区。JVM 会在 Eden 区里先给每个线程划一小块私有空间,大多数小对象优先在这块私有缓冲区里分配。这样绝大多数时候,线程分配对象根本不需要和别人竞争,大幅降低了同步开销。
再说对象结构。一个 Java 对象在内存里,通常可以拆成三部分:
第一部分是对象头。对象头里最重要的是 Mark Word,它会记录对象的哈希码、GC 年龄、锁状态等信息;另外还有一个类型指针,指向这个对象所属类的元数据。如果是数组对象,还会额外记录数组长度。
第二部分是实例数据,也就是我们真正定义的成员变量。
第三部分是对齐填充。因为 HotSpot 为了提升访问效率,通常要求对象大小满足特定对齐规则,所以必要时会补一些字节。
所以一句话总结:
- 对象分配安全主要靠 TLAB 和 CAS;
- 对象内存布局主要分成 对象头、实例数据、对齐填充。
最后更新于: