1、简述
在 Java 中,对象创建是非常频繁的操作。你可能会疑问:
当多个线程同时创建对象时,它们是否会在 JVM 堆中“抢占”内存?
本文将从 JVM 的内存模型、对象分配机制出发,揭开“堆抢占”的本质,探讨多线程下的并发内存分配策略,并通过实践样例进行验证。
2、对象创建的内存来源:Java 堆
Java 中大多数对象都分配在堆内存(Heap)中。堆是所有线程共享的内存区域,因此在多线程环境下,如果不加管理,就可能存在多个线程争用堆中内存的问题——也就是所谓的“堆抢占”。
2.1 什么是“堆抢占”?
“堆抢占”本质上是:
多线程并发创建对象时,竞争堆内存的可用空间,导致同步开销或分配延迟。
如果 JVM 采用的是共享指针分配策略,每个线程都需要对同一个内存指针进行加锁或原子操作,这会产生性能瓶颈。
2.2 JVM 如何避免堆抢占?——TLAB(线程本地分配缓冲)
为了优化对象分配的并发性能,JVM 引入了 TLAB(Thread Local Allocation Buffer)。
TLAB 机制:
- JVM 会为每个线程分配一小块堆内存区域。
- 线程创建对象时,优先从自己的 TLAB 中分配,无需加锁。
- 只有当 TLAB 用尽时,才会申请新的 TLAB 或走全局堆分配(可能引起“堆抢占”)。
这意味着:大多数情况下,不会发生堆抢占。
2.3 TLAB 开启状态
TLAB 在大多数 JVM 实现中是默认开启的。可以使用以下参数查看状态:
java -XX:+PrintTLAB -XX:+UseTLAB -Xlog:gc -Xms128m -Xmx128m Demo
3、实践样例:观察是否存在堆抢占
下面通过一个多线程对象创建示例,对比 TLAB 开启与关闭下的分配效率。
public class AllocationTest {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1_000_000; i++) {
byte[] buffer = new byte[1024]; // 每次分配 1KB
}
};
Thread[] threads = new Thread[8];
long start = System.currentTimeMillis();
for (int i = 0; i < 8; i++) {
threads[i] = new Thread(task);
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
long end = System.currentTimeMillis();
System.out.println("Total time: " + (end - start) + " ms");
}
}
3.1 对比执行方式:
开启 TLAB(默认)
java -XX:+UseTLAB -Xms512m -Xmx512m AllocationTest
关闭 TLAB
java -XX:-UseTLAB -Xms512m -Xmx512m AllocationTest
观察指标:
- 执行时间:关闭 TLAB 后通常变慢。
- GC 日志:关闭 TLAB 会增加堆分配冲突,导致更多 GC 或停顿。
- CPU 使用率:可能增加同步操作占用。
3.2 深入:TLAB 分配过程图
+---------------------------+
Thread ---> | TLAB (Thread-local heap) |
+---------------------------+
↓
+-----------+
| allocate()| ——> Fast path
+-----------+
TLAB 不足时:
→ 请求新 TLAB
→ 或 走 Global Allocation (需同步)
3.3 JVM 其他避免堆抢占的机制
除了 TLAB,JVM 还通过以下方式优化并发对象分配:
机制 | 说明 |
---|---|
Eden 区并发分配 | 年轻代中的 Eden 区为对象分配提供连续空间 |
指针碰撞算法 | 避免查找空闲块,提升分配效率 |
多线程 GC 支持 | 如 G1、ZGC 支持并发标记/复制,降低暂停 |
4、结语
JVM 设计了诸如 TLAB、指针碰撞、内存分代等机制,最大限度地避免多线程对象创建时的“堆抢占”。理解这些原理不仅能帮助我们写出更高效的代码,还能在面对 GC 性能瓶颈时,做出更精确的诊断与调优。
问题 | 是否发生? | 说明 |
---|---|---|
多线程下是否争抢堆空间? | ✅ 有可能 | 当 TLAB 用尽或关闭时,发生堆全局分配争抢 |
TLAB 是否能避免堆抢占? | ✅ 是 | 提供线程本地内存,提升并发分配效率 |
是否推荐关闭 TLAB? | ❌ 否 | 除非做性能调试,否则不建议关闭 |
如果你想进一步实践,我可以提供:
- 📦 自定义 TLAB 大小对性能影响的测试代码
- 🧠 多线程 + Off-Heap(堆外)对象分配对比
- 📈 使用 JMH 测试分配速度差异
是否需要我继续拓展?