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 大小)和优化代码(如使用对象池),可以显著降低多线程内存抢占的开销。

相关推荐
caihuayuan51 小时前
生产模式下react项目报错minified react error #130的问题
java·大数据·spring boot·后端·课程设计
编程、小哥哥1 小时前
Java大厂面试:从Web框架到微服务技术的场景化提问与解析
java·spring boot·微服务·面试·技术栈·数据库设计·分布式系统
界面开发小八哥2 小时前
「Java EE开发指南」如何使用MyEclipse的可视化JSF编辑器设计JSP?(二)
java·ide·人工智能·java-ee·myeclipse
找不到、了3 小时前
Spring-Beans的生命周期的介绍
java·开发语言·spring
caihuayuan43 小时前
React Native 0.68 安装react-native-picker报错:找不到compile
java·大数据·sql·spring·课程设计
爱编程的鱼4 小时前
C#接口(Interface)全方位讲解:定义、特性、应用与实践
java·前端·c#
阿文_ing4 小时前
JVM 调优实战入门:从 GC 日志分析到参数调优
jvm
旋风菠萝4 小时前
深入理解Java中的Minor GC、Major GC和Full GC
java·jvm·gc
苹果酱05674 小时前
React方向:react脚手架的使用
java·vue.js·spring boot·mysql·课程设计