目录
[5.2、伪共享(False Sharing)](#5.2、伪共享(False Sharing))
[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 中,**对象的所有实例字段(如 x
和 y
)默认会连续存储在对象的内存布局中,**减少内存碎片,一次性分配内存。
代码示例:
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缓存行 两个角度分析。
-
x
和y
的地址相差 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 缓存频繁失效。
运行结果:
-
由于
x
和y
在同一缓存行,两个线程会互相使对方的缓存失效。 -
耗时可能高达 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) { /* 同上 */ }
}
效果:
-
x
和y
被隔离到不同的缓存行。 -
耗时可能降至 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) { /* 同上 */ }
}
关键步骤:
- 添加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 大小)和优化代码(如使用对象池),可以显著降低多线程内存抢占的开销。