JVM如何处理多线程内存抢占问题

目录

1、堆内存结构

2、运行时数据

3、内存分配机制

3.1、堆内存结构

3.2、内存分配方式

1、指针碰撞

2、空闲列表

4、jvm内存抢占方案

4.1、TLAB

4.2、CAS

4.3、锁优化

4.4、逃逸分析与栈上分配

5、问题

5.1、内存分配竞争导致性能下降

[5.2、伪共享(False Sharing)](#5.2、伪共享(False Sharing))

1、对象内存结构

2、对象内存布局

3、问题表现

4、解决方案

[5.3、内存泄漏(ThreadLocal 未清理)](#5.3、内存泄漏(ThreadLocal 未清理))


前言:

在多线程环境下,JVM 需要高效、安全地管理内存分配,避免多个线程同时竞争内存资源导致的性能下降或数据不一致问题。

如下图所示:

了解更多jvm的知识,可参考:关于对JVM的知识整理_谈谈你对jvm的理解-CSDN博客


1、堆内存结构

由年前代和老年代组成。年轻代分为eden和survivor1和survivor2区。

年轻代和老年代分别站别1/3和2/3。而eden区占比年轻代8/10,s1和s2分别占比1/10,1/10。

如下图所示:

java堆里面存放的是数组和对象实例,字符串常量池、静态变量和TLAB。

如下图所示:

由上图可知:可以看到TLAB存储在堆中。

TLAB 本身是存储在堆中,但它对每个线程都是独立的。一个线程在创建对象时会使用其自己的 TLAB 来进行分配,而不是直接访问共享的堆内存区域。

如下所示:


2、运行时数据

由下图所示:运行数据区由堆和方法区(元空间)组成。

完整的执行过程由类加载系统、运行时数据区和执行引擎及本地方法库和接口组成。


3、内存分配机制

JVM 的内存分配主要发生在 堆(Heap) 上,涉及以下几个关键组件:

3.1、堆内存结构

  • 新生代(Young Generation) :存放新创建的对象,分为 Eden区Survivor区

  • 老年代(Old Generation):存放长期存活的对象。

  • TLAB(Thread-Local Allocation Buffer):每个线程私有的内存分配缓冲区。

3.2、内存分配方式

1、指针碰撞

如下图所示:

Bump-the-Pointer :适用于 连续内存空间(如 Serial、ParNew 等垃圾收集器)。

通过原子操作移动指针分配内存。

2、空闲列表

如下图所示:

Free List :适用于 不连续内存空间(如 CMS、G1 等垃圾收集器)。

维护一个空闲内存块列表,分配时查找合适的内存块。


4、jvm内存抢占方案

4.1、TLAB

全名(Thread-Local Allocation Buffer)。

1、作用

每个线程在 Eden区 拥有一块私有内存(TLAB),用于分配小对象(默认约 1% Eden 大小)。避免多线程竞争全局堆内存指针,提升分配效率。

2、特点

TLAB 分配无需加锁,因为每个线程操作自己的缓冲区。

当 TLAB 用尽时,线程会申请新的 TLAB(可能触发锁竞争)。

java 复制代码
-XX:+UseTLAB  # 默认启用
-XX:TLABSize=512k  # 调整 TLAB 大小

如下图所示:

4.2、CAS

(Compare-And-Swap)原子操作

适用场景

当 TLAB 不足或分配大对象时,线程需在 全局堆 分配内存。

JVM 使用 CAS(如 Atomic::cmpxchg 确保指针更新的原子性。

java 复制代码
// HotSpot 源码中的内存分配逻辑(伪代码)
if (使用 TLAB) {
    从 TLAB 分配内存;
} else {
    do {
        old_value = 当前堆指针;
        new_value = old_value + 对象大小;
    } while (!CAS(&堆指针, old_value, new_value)); // 原子更新指针
    返回 old_value;
}

4.3、锁优化

如偏向锁、自旋锁

问题

如果多个线程同时竞争全局堆内存,可能触发锁竞争。

解决方案

JVM 使用 偏向锁自旋锁 减少线程阻塞。

例如,G1 垃圾收集器在分配时采用 分区(Region)锁,降低冲突概率。

4.4、逃逸分析与栈上分配

逃逸分析(Escape Analysis)

JVM 分析对象是否可能被其他线程访问(即是否"逃逸")。如果对象未逃逸,可直接在 栈上分配,避免堆内存竞争。

如下图所示:

启用方式

java 复制代码
-XX:+DoEscapeAnalysis  # 默认启用
-XX:+EliminateAllocations  # 开启标量替换

5、问题

在上面介绍中,关于jvm如何可以解决内存抢占,下面解释下内存抢占引发的典型问题及解决方案。

5.1、内存分配竞争导致性能下降

表现

多线程频繁分配对象时,new 操作变慢。

解决方案

增大 TLAB-XX:TLABSize)。使用对象池(如 Apache Commons Pool)。

5.2、伪共享(False Sharing)

1、对象内存结构

在 Java 中,**对象的所有实例字段(如 xy)默认会连续存储在对象的内存布局中,**减少内存碎片,一次性分配内存。

代码示例:

java 复制代码
class FalseSharingExample {
    volatile long x; // 8字节
    volatile long y; // 8字节
}
  • 对象头(Header):12 字节(64位 JVM,未压缩指针时)。

  • 字段 x:8 字节(紧接对象头)。

  • 字段 y :8 字节(紧接 x)。

  • 对齐填充(Padding):4 字节(见下文)。

2、对象内存布局

java对象的内存布局由对象头(12个字节)、实例数据、对象填充(8个字节)组成。

如图所示:

更多了解可参考**:** Java对象的内存布局及GC回收年龄的研究-CSDN博客

3、问题表现

需要从 对象内存布局CPU缓存行 两个角度分析。

  • xy 的地址相差 8 字节 (因为 long 类型占 8 字节)。

  • 它们必然位于同一缓存行(缓存行通常 64 字节)。

代码示例:

java 复制代码
class FalseSharingExample {
    volatile long x; // 线程1修改
    volatile long y; // 线程2修改

    public static void main(String[] args) {
        FalseSharingExample example = new FalseSharingExample();
        
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1_0000_0000; i++) {
                example.x = i; // 高频修改x
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1_0000_0000; i++) {
                example.y = i; // 高频修改y
            }
        });

        long start = System.currentTimeMillis();
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("耗时: " + (System.currentTimeMillis() - start) + "ms");
    }
}

多个线程修改同一缓存行(Cache Line)的不同变量,导致 CPU 缓存频繁失效。

运行结果

  • 由于 xy 在同一缓存行,两个线程会互相使对方的缓存失效。

  • 耗时可能高达 5000ms(实际结果依赖CPU架构)。

原因如下图所示:

4、解决方案

1、手动解决

java 复制代码
class ManualPaddingExample {
    volatile long x;
    // 填充56字节(64字节缓存行 - 8字节long)
    private long p1, p2, p3, p4, p5, p6, p7; 
    volatile long y;

    public static void main(String[] args) { /* 同上 */ }
}

效果

  • xy 被隔离到不同的缓存行。

  • 耗时可能降至 1000ms(性能提升5倍)。

2、使用 @Contended 自动解决(Java 8+)

@Contended 让 JVM 自动完成填充,代码更简洁:

java 复制代码
import sun.misc.Contended;

class ContendedExample {
    @Contended  // 确保x独占缓存行
    volatile long x;
    
    @Contended  // 确保y独占缓存行
    volatile long y;

    public static void main(String[] args) { /* 同上 */ }
}

关键步骤

  1. 添加JVM参数 (允许使用@Contended):
java 复制代码
-XX:-RestrictContended

运行结果

耗时与手动填充一致(约 1000ms),但代码更干净。

最终内存布局:

bash 复制代码
| 对象头 (12字节) | x (8字节) | y (8字节) | 填充 (4字节) |
|----------------|----------|----------|-------------|

5.3、内存泄漏(ThreadLocal 未清理)

  • 表现

    • 线程池中 ThreadLocal 未调用 remove(),导致内存无法释放。
  • 解决方案

    • 必须 remove()
java 复制代码
try {
    threadLocal.set(value);
    // 业务逻辑
} finally {
    threadLocal.remove(); // 清理
}

总结


总结

  • TLAB 是 JVM 解决多线程内存竞争的核心机制,通过 线程私有缓冲区 减少锁竞争。

  • CAS 操作 用于全局堆内存分配,保证原子性。

  • 逃逸分析栈上分配 可彻底避免堆内存竞争。

  • 伪共享ThreadLocal 泄漏 需额外注意,通过缓存行填充和及时清理避免。

通过合理配置 JVM 参数(如 TLAB 大小)和优化代码(如使用对象池),可以显著降低多线程内存抢占的开销。

相关推荐
num_killer4 小时前
小白的Langchain学习
java·python·学习·langchain
期待のcode5 小时前
Java虚拟机的运行模式
java·开发语言·jvm
程序员老徐5 小时前
Tomcat源码分析三(Tomcat请求源码分析)
java·tomcat
a程序小傲5 小时前
京东Java面试被问:动态规划的状态压缩和优化技巧
java·开发语言·mysql·算法·adb·postgresql·深度优先
仙俊红5 小时前
spring的IoC(控制反转)面试题
java·后端·spring
阿湯哥5 小时前
AgentScope Java 集成 Spring AI Alibaba Workflow 完整指南
java·人工智能·spring
小楼v5 小时前
说说常见的限流算法及如何使用Redisson实现多机限流
java·后端·redisson·限流算法
与遨游于天地5 小时前
NIO的三个组件解决三个问题
java·后端·nio
czlczl200209256 小时前
Guava Cache 原理与实战
java·后端·spring
yangminlei6 小时前
Spring 事务探秘:核心机制与应用场景解析
java·spring boot