当前位置: 首页 > news >正文

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)

1、对象内存结构

2、对象内存布局

3、问题表现

4、解决方案

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(可能触发锁竞争)。

-XX:+UseTLAB  # 默认启用
-XX:TLABSize=512k  # 调整 TLAB 大小

如下图所示:

4.2、CAS

(Compare-And-Swap)原子操作

适用场景

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

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

// 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 分析对象是否可能被其他线程访问(即是否“逃逸”)。如果对象未逃逸,可直接在 栈上分配,避免堆内存竞争。

    如下图所示:

    启用方式

    -XX:+DoEscapeAnalysis  # 默认启用
    -XX:+EliminateAllocations  # 开启标量替换

    5、问题

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

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

    表现

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

      解决方案

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

      5.2、伪共享(False Sharing)

      1、对象内存结构

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

      代码示例:

      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缓存行 两个角度分析。

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

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

      代码示例:

      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 缓存频繁失效。

        运行结果

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

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

        原因如下图所示:

        4、解决方案

        1、手动解决

        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) { /* 同上 */ }
        }

        效果

        • x 和 y 被隔离到不同的缓存行。

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

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

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

        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):

        -XX:-RestrictContended

        运行结果

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

          最终内存布局:

          | 对象头 (12字节) | x (8字节) | y (8字节) | 填充 (4字节) |
          |----------------|----------|----------|-------------|

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

          • 表现

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

          • 解决方案

            • 必须 remove()

          try {threadLocal.set(value);// 业务逻辑
          } finally {threadLocal.remove(); // 清理
          }

          总结


          总结

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

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

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

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

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

          相关文章:

        1. 王者荣耀游戏测试场景题
        2. 上位机知识篇---流式Web服务器模式的实现
        3. 为什么需要加密机服务?
        4. 大模型deepseek如何助力数据安全管理
        5. 使用国内源加速Qt在线安装
        6. C++笔试题(金山科技新未来训练营):
        7. 基于CNN的猫狗识别(自定义CNN模型)
        8. SpringBoot快速上手
        9. Spring AI 从入门到精通
        10. 07、基础入门-SpringBoot-自动配置特性
        11. Python Logging 模块完全指南
        12. 基于OpenCV的实时文档扫描与矫正技术
        13. mvc-ioc实现
        14. NB-IoT技术深度解析:部署模式与节能机制全指南
        15. 计算机系统的层次结构
        16. 算法刷题Day9 5.18:leetcode定长滑动窗口3道题,结束定长滑动窗口,用时1h
        17. 【android bluetooth 协议分析 01】【HCI 层介绍 5】【SetEventMask命令介绍】
        18. 2025年- H29-Lc137- 19.删除链表的倒数第N个节点(快慢指针)---java版
        19. STC89C52单片机模拟洗衣机源代码程序 C语言程序 按键设置洗衣模式和洗衣时间 洗衣完成后语音报警提示
        20. 机器学习(11)——xgboost
        21. 中国旅游日|上天当个“显眼包”!体验低空经济的“飞”凡魅力
        22. 人民日报大家谈:为基层减负,治在根子上减到点子上
        23. 旅马大熊猫“福娃”“凤仪”平安回国
        24. 三方合作会否受政局变化影响?“中日韩+”智库合作论坛在沪举行
        25. 十年牢狱倒计时,一名服刑人员的期待与惶恐
        26. 上海:到2027年,实现近海航线及重点海域5G网络高质量覆盖