前言:线程一多,问题就多
那是一个平凡的工作日,我正打算下楼拿奶茶,刚要起身,就听隔壁小王爆出一声吼:
“我这代码怎么突然OOM了?不是用了并发队列吗?”
我探过头一看,小王的眼睛都快盯穿了堆栈日志。线程一多,对象分配就开始翻车,不是内存乱飞,就是崩溃卡死。我们立刻意识到,幕后那个“对象分配”的黑箱得好好扒一扒了。
于是,这杯奶茶我喝得格外认真,边喝边翻 JVM 的源码,一段 HotSpot 虚拟机中处理并发对象分配的“玄机”悄然浮现——没错,我们今天要聊的主角,是 JVM 如何在高并发环境下高效且安全地分配对象内存的秘密。
对象是怎么“诞生”的?
在 Java 世界里,万物皆对象。每当我们执行一句 new 操作,就相当于按下了“制造”按钮,JVM 背后的内存管理机制就开始运行:
- 首先要在堆中划出一块连续的内存空间;
- 接着对这块空间进行对象头填充、字段初始化;
- 最后把这块“对象胚胎”交到程序手中,完成生命周期的起点。
这个过程看似简单,但你想啊——如果上百个线程同时“点单”,这工厂还不得挤爆了?
所以,HotSpot 虚拟机到底怎么解决这个问题的呢?
谜底就是我们今天要讲的两大核心策略:
- 同步处理:用 CAS + 失败重试 来实现原子分配
- 本地线程分配缓冲:每个线程分自己的一摊,减少锁竞争
第一招:同步处理,CAS+失败重试,稳了!
在没有 TLAB 的情况下,所有线程都要从一个“共享堆空间”里划内存。这个时候,“抢地盘”就成了问题。
JVM 没有选择加锁,而是更优雅地采用了 CAS(Compare-And-Swap)+ 自旋重试 的策略。
1、什么是 CAS?
CAS 是一种原子操作,允许你去比较一块内存的当前值是否是你期望的,如果是就更新成新值。
比如现在堆的分配指针指向地址 0x1000,线程 A 想分配 100 字节,它就用 CAS 尝试把指针更新为 0x1064,如果成功,它就“占坑成功”,分配搞定。
如果失败,说明别的线程抢先一步改了分配指针,那线程 A 就重新来一遍。
2、CAS 的优势:
- 无需加锁,性能高;
- 是硬件级别支持的原子操作;
- 多线程下仍然能保证数据一致性。
HotSpot 正是基于这种机制来让对象分配“线程安全”,避免你争我夺乱成一锅粥。
不过,这种方式毕竟是所有线程在一个“大锅”里抢饭吃,线程越多,CAS 失败率就越高,效率会下降。
于是,JVM 又出了第二招。
第二招:TLAB,划地为营,各吃各的!
还记得小时候大家抢糖吃吗?如果所有糖果放在一个盘子里,肯定鸡飞狗跳。但如果每个人发一个小袋子,就能自己慢慢吃,互不打扰。
这就是 TLAB(Thread Local Allocation Buffer)思路的灵感:
JVM 给每个线程分配一个“小堆”,线程创建对象时,优先从自己的 TLAB 分配。
是不是特别熟悉?这就好比线程版的“私房钱”。
1、为什么要用 TLAB?
- 避免线程之间的同步冲突,自己地盘自己用,爽!
- 小对象频繁创建也不会频繁竞争锁,极大提高性能;
- TLAB 是在线程创建时初始化的,不影响其他线程的内存分配。
当线程使用 new 创建对象时,JVM 会先判断当前线程的 TLAB 是否还有足够空间:
- 有空间: 直接从 TLAB 中分配,对象创建快如闪电。
- 没空间: 就申请新的 TLAB,如果申请失败了才回到老办法——用 CAS 从全局堆中分配。
2、TLAB 的大小是固定的吗?
不是。TLAB 的大小一般由 JVM 根据堆总大小、线程数、GC 频率等动态调整,而且也可以通过参数手动干预。
比如你可以在启动参数中控制是否启用 TLAB:
- -XX:+UseTLAB # 启用(默认开启)
- -XX:-UseTLAB # 禁用
还能设置分配比例等细节:
- -XX:TLABSize=512k
- -XX:+ResizeTLAB
3、TLAB 用完了怎么办?
TLAB 可不是无限大。一旦当前 TLAB 用完,线程会尝试向堆申请一个新的 TLAB。这个时候才会触发同步机制,可能导致暂停、GC 或老年代分配。
所以说,TLAB 是一个优化策略,不是银弹。它非常适合“短平快”的对象,比如业务中频繁构建的临时对象、包装类等。
模拟生活:对象分配的快与慢
为了让你更有画面感,我们模拟一下现实场景:
有一个奶茶工厂,老板(JVM)分给每个员工(线程)一块原料池(TLAB),大家自己打奶茶,不用排队。只有当原料池见底了,员工才去找仓库经理(堆)申请新原料(新 TLAB)。
如果这个工厂一天要出 10000 杯奶茶,光靠一个仓库经理肯定手忙脚乱。但每个员工有一小桶原料,效率就高了。
TLAB 就像是让线程各自“带着干粮上工”,分摊了压力。
深入理解:TLAB + CAS 是搭配来的
可能你注意到了一个细节:哪怕使用了 TLAB,也还是会有 CAS 的影子。
那是因为创建新 TLAB 本身是从堆中分配空间,这个动作还是要用 CAS 保证线程安全。但因为这个过程不是每次 new 都触发,所以平均下来效率非常高。
换句话说:
- 平时靠 TLAB,自己吃饭;
- 用完再找 JVM,JVM 用 CAS 给你分新地盘。
这才是 HotSpot 的“并发分配两步走”策略:分而治之 + 最小化同步。
TLAB 的开关和调优姿势
TLAB 并不是强制开启的,你可以通过启动参数控制它:
- -XX:+UseTLAB # 启用 TLAB(默认)
- -XX:-UseTLAB # 禁用 TLAB
- -XX:+PrintTLAB # 打印 TLAB 使用情况
如果你想观察 TLAB 的命中率、利用率,可以开启诊断命令:
- jstat -gcutil PID 1000 5
还可以在 JFR(Java Flight Recorder)中看到 TLAB 的命中分析,精确到线程级别。
尾声:对象分配的智慧
JVM 世界里,对象分配看似一个“小动作”,却蕴藏了大智慧。
从一开始的 CAS 原子操作,到后来的 TLAB 线程划分策略,再到按需同步、动态调优,HotSpot JVM 已经把对象创建的“并发冲突”压缩到了最低。
而你写下的每一个 new,背后都经历了精妙的内存布局计算、线程调度逻辑,以及无数 JVM 工程师的血泪优化。
想想看,我们是不是应该对 JVM 稍微多一点敬意?
下次你再喝奶茶的时候,不妨想象一下那一杯“对象”,是不是正是 JVM 高效调度出来的一杯热奶香浓呢?
END
如果你觉得这篇文章有点意思,记得点个 “赞” 和 “在看” 支持小米呀!
我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!