JVM如何优雅地分对象?揭秘HotSpot并发分配的神操作!

58 阅读6分钟



前言:线程一多,问题就多

那是一个平凡的工作日,我正打算下楼拿奶茶,刚要起身,就听隔壁小王爆出一声吼:

“我这代码怎么突然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岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!