基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

对象在堆内存中分配空间时,JVM 是如何解决并发安全问题的?

知识点图片

在 Java 中,堆内存(Heap)是所有线程共享的区域。当多个线程同时在堆上分配对象时,如果不进行控制,就会出现两个线程同时使用同一块内存地址的指针碰撞(并发冲突)问题。

为了在保证线程安全的同时最大化分配效率,JVM 主要采用了两种核心机制来解决这个问题:TLAB(本地线程分配缓冲)CAS(Compare-And-Swap)搭配失败重试

以下是详细的解析:

1. 第一道防线:TLAB(Thread Local Allocation Buffer)

这是 JVM 解决对象分配并发冲突最主要、最高效的手段。

  • 核心思想(空间换时间): 把共享的堆内存“私有化”。JVM 在 Eden 区为每个线程预先分配一小块私有的内存区域,这就是 TLAB。
  • 如何保证安全: 因为每个线程只能在自己的 TLAB 中分配对象,线程之间互不干扰,所以在这个小区域内分配对象时,完全不需要加锁
  • 分配方式: 采用指针碰撞(Bump the Pointer)。只需要记录 TLAB 的起始地址和当前分配到的指针位置,分配新对象时,把指针往后移动对象大小的距离即可,速度极快。
  • 相关配置: TLAB 默认是开启的(可以通过 -XX:+UseTLAB 参数控制)。TLAB 的空间通常很小(默认占 Eden 区的 1%)。

2. 第二道防线:CAS(Compare-And-Swap) + 失败重试

当 TLAB 空间不足,或者对象太大无法放入 TLAB 时,对象就必须直接在共享的 Eden 区进行分配。此时,JVM 会采用 CAS 操作来保证线程安全。

  • 核心思想(乐观锁): 假设没有冲突去尝试分配,如果发现有冲突就重试,直到成功。
  • 如何保证安全: CAS 是一种硬件级别的原子操作(例如 x86 架构下的 lock cmpxchg 指令)。
    1. 线程首先读取当前 Eden 区的空闲内存指针位置(假设为 old_ptr)。
    2. 计算出分配该对象后指针的新位置(假设为 new_ptr = old_ptr + object_size)。
    3. 利用 CAS 指令尝试将内存指针从 old_ptr 更新为 new_ptr
    4. 成功: 如果此时内存指针仍然是 old_ptr,说明没有其他线程干扰,更新成功,对象分配完毕。
    5. 失败: 如果指针不再是 old_ptr(说明有其他线程抢先分配了内存),CAS 失败。此时线程会重新读取最新的指针位置,循环重试(Spin),直到分配成功。
  • 应用场景: 除了直接在 Eden 区分配对象外,线程向 JVM 申请新的 TLAB 空间时,由于也是在共享区域划分内存,同样使用的是 CAS + 重试机制。

3. 完整的对象分配并发控制流程

综合以上两点,当你在代码中 new 一个对象时,JVM 在内存分配上的完整流程如下:

  1. 判断是否开启了栈上分配: 如果经过逃逸分析,发现该对象不会被其他线程访问,JVM 会尽量将对象打散分配在线程栈上(无并发问题)。如果不能,进入下一步。
  2. 尝试在 TLAB 中分配:
    • 判断当前线程的 TLAB 剩余空间是否能容纳该对象。
    • 如果能容纳,直接无锁分配(指针移动),结束。
    • 如果不能容纳(TLAB 满了),进入下一步。
  3. 判断对象大小与 TLAB 重新申请逻辑:
    • 如果对象比较小,只是当前 TLAB 用完了,线程会通过 CAS操作 向 JVM 申请一块新的 TLAB,然后在新 TLAB 中无锁分配。
    • 如果对象比较大(超过了 TLAB 的最大限制),不适合放入 TLAB,进入下一步。
  4. 直接在 Eden 区分配: JVM 使用 CAS + 失败重试 的机制,直接在共享的 Eden 区为其分配内存。
  5. 直接进入老年代: 如果是一个超大对象(比如一个巨大的数组),可能会跳过 Eden 区,直接在老年代分配(同样需要同步机制/CAS 保证安全)。

总结

JVM 解决对象分配的并发安全问题,采用的是分层策略

  • 99% 的情况(常规对象): 通过 TLAB 实现无锁的极速分配。
  • 1% 的情况(大对象或申请新 TLAB 时): 通过底层的 CAS + 重试 机制保证共享内存更新的原子性。

这种设计既避免了传统重量级锁(如 synchronized)带来的上下文切换和性能损耗,又完美地保证了多线程环境下堆内存分配的绝对安全。

00:00
00:00