不止新生代與老年代:深入Java虛擬機堆內存布局與TLAB、卡表等優化機制
Java虛擬機運行數據區域
在JDK 8及以上版本中,Java虛擬機運行時數據區域主要包括以下部分:
1)堆(Heap):這是Java虛擬機中最大的內存區域,所有線程共享,主要用于存放對象實例和數組。這也是垃圾回收的主要區域,因此也被稱作GC堆(Garbage Collection Heap)。
2)方法區(Method Area):在JDK 8之前,這被稱為永久代(PermGen)。但從JDK 8開始,被替換為元空間(Metaspace)。主要用于存儲已加載的類信息、常量、靜態變量、編譯后的代碼等。
3)棧(Stack):每個線程創建時都會有一個對應的Java棧,用于存儲局部變量、操作數棧、動態鏈接、方法出口等信息。
4)程序計數器(Program Counter Register):這是一塊小內存區域,作為當前線程執行的字節碼的行號指示器。每個線程都有一個獨立的程序計數器。
5)本地方法棧(Native Method Stack):類似于Java棧,用于支持本地方法的執行。
6)直接內存(Direct Memory):雖然不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域,但這部分內存被頻繁使用,主要被NIO用于提高IO效率。

Java虛擬機的堆劃分
當前,主流的Java虛擬機主要采用分代回收(Generational Garbage Collection)。分代回收,更準確地說,它是一種理念。這種理念將系統中的所有對象劃分為不同的代(Generation),并根據對象的生命周期長度將其分類到相應的代中,每個代則采用適合其特性的垃圾回收算法。這種理念主要基于兩個分代假設。
1)弱分代假說(Weak Generational Hypothesis):大部分對象都會在創建后不久就變得不可達。也就是說,許多對象的生命周期都很短;
2)強分代假說(Strong Generational Hypothesis):存活時間較長的對象,很可能會引用存活時間較短的對象,但反之則不然。也就是說,老的對象很少引用新的對象。
Java虛擬機將堆劃分為新生代(Young Generation)和老年代(Old Generation)。其中,新生代又被劃分為Eden區,以及兩個大小相同的Survivor區。默認情況下,Java虛擬機采取一種動態分配的策略,根據生成對象的速率,以及Survivor區的使用情況動態調整Eden區和Survivor區的比例。

TLAB
通常,調用new指令時,會在Eden區中劃出一塊作為存儲對象的內存。由于堆空間是線程共享的,在多線程環境下,為了避免多個線程同時向堆內存申請空間而產生的競爭,Java虛擬機為每個線程分配了一個私有的緩沖區,即TLAB(Thread-Local Allocation Buffer)。由于TLAB是線程私有的,因此在分配對象時不需要進行線程同步,大大提高了對象分配的效率。如果TLAB空間不足,那么線程可能需要申請新的TLAB。需要注意的是,TLAB只適用于小對象的分配。對于大對象,通常直接在堆內存中進行分配。

Minor GC、Minor GC、Full GC
當Eden區空間不足時,會觸發Minor GC,對年輕代進行垃圾回收。存活下來對象,會被遷移到Survivor區。新生代包含兩個Survivor區,分別為from和to區,其中to區始終保持空閑。
Minor GC期間,Eden區和from區的存活對象被復制至to區,隨后交換from和to,確保下次Minor GC時,to區為空。
Java虛擬機會跟蹤Survivor區對象的復制次數,當達到15次或單個Survivor區占用率超過50%時,那么較高復制次數的對象,將被晉升(Promote)至老年代。
Minor GC主要采用復制算法,Survivor區中的老存活對象晉升至老年代,然后將剩余存活對象與Eden區的存活對象復制至另一Survivor區。在理想情況下,Eden區的對象大多已死亡,因此需要復制的數據量較小,因此采用復制算法效率較高。
當老年代空間不足時,觸發Major GC,對老年代進行垃圾回收。Major GC通常采用清除或整理算法,由于需要掃描整個老年代并可能進行對象移動以整理內存,因此會導致較長的停頓時間。
當Java堆內存空間不足時,觸發Full GC,對整個堆內存(包括新生代和老年代)進行垃圾回收。由于需要掃描整個堆內存并可能進行對象移動以整理內存,Full GC會導致比Major GC更長的停頓時間。
卡表
Minor GC期間,在標記存活對象的時候,可能會碰到跨代引用對象的問題。由于年輕代大都是存活時間較短的對象,跨代引用通常是指老年代對象存在對新生代對象的引用。為了保證對年輕代存活對象標記準確性,就不得不把老年代也納入到掃描范圍。
為了解決跨代引用的問題,可以在新生代可以引入記憶集(RememberSet)。記憶集位于新生代中,是一種用于記錄從非回收區域指向回收區域的指針集合的抽象數據結構。

記憶集是一種概念,在Java虛擬機中,記憶集通常通過卡表(Card Table)實現,它也是目前最常用的一種方式。卡表是一個字節數組,其中每個元素對應一塊特定大小(默認512字節)的內存區域,稱為卡頁(Card Page)。
當對象的引用發生修改時,寫屏障會被觸發,將對應的卡頁標記為"臟",表示該內存區域的對象已被修改。為了降低寫屏障的開銷并保持其生成指令的簡潔性,寫屏障并不判斷更新后的引用是否指向新生代對象,而是統一視為可能指向新生代對象的引用。
if (CARD_TABLE [this address >> 9] != DIRTY)
CARD_TABLE [this address >> 9] = DIRTY;
在進行Minor GC的時,就無需掃描整個老年代,而是在卡表中尋找標記為臟卡的區域,并將這些臟卡區域的對象加入到Minor GC的GC Roots中。完成所有臟卡的掃描后,Java虛擬機會清除所有臟卡的標記。

未完待續
很高興與你相遇!如果你喜歡本文內容,記得關注哦
本文來自博客園,作者:poemyang,轉載請注明原文鏈接:http://www.rzrgm.cn/poemyang/p/19187510
浙公網安備 33010602011771號