目錄
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) { /* 同上 */ }
}
關鍵步驟:
-
添加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 大小)和優化代碼(如使用對象池),可以顯著降低多線程內存搶占的開銷。