动态

详情 返回 返回

不止新生代與老年代:深入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效率。
image

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區的比例。
image

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

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)。記憶集位於新生代中,是一種用於記錄從非回收區域指向回收區域的指針集合的抽象數據結構。
image

記憶集是一種概念,在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虛擬機會清除所有髒卡的標記。
image

未完待續

很高興與你相遇!如果你喜歡本文內容,記得關注哦

Add a new 评论

Some HTML is okay.