GC Roots 枚舉需要遍歷整個應用程序的上下文,而在進行可達性分析或者垃圾回收時,如果我們還是進行全堆掃描及收集,那么會非常耗時。JVM 將堆分為新生代及老生代,它們的回收頻率及算法不一樣。
1 回收算法
在進行可達性分析時,我們會對對象進行標記:存活及待回收。然后再進行回收。
標記-清除 | 統一回收所有被標記為“待回收”的對象。 缺陷:1)執行效率不穩定,如果大部分對象需要回收,那清除工作將增加。2)內存空間碎片化,會產生大量不連續的內存碎片。 |
標記-復制 | 半區復制,將空間平分為2,每次只用其中1塊,每次回收時將存活的對象復制到另一塊,將當前塊一次性清理掉。 缺陷:1)如果大部分對象是存活的,就會產生很大的內存復制開銷。2)可用內存縮小到原來一半,內存利用率不高。 |
標記-整理 | 讓所有存活的對象都向內存空間一端移動,然后直接清理掉邊界以外的內存。 缺陷:如果存活對象分布比較分散,那么移動對象將很耗時。 |
表 回收算法
“標記-清除”因為“碎片問題”在虛擬機中較少使用,“碎片問題”會帶來垃圾回收頻繁、大型數據無法存儲等問題。
1.1 分代收集
弱分代假說:絕大多數對象都是朝生夕滅。
強分代假說:熬過越多次垃圾收集的對象就越難以消亡。
JVM 按照對象年齡(熬過垃圾收集過程的次數,默認15次)將堆劃分為新生代與老生代。
新生代 | 依據弱分代假說,每次回收只關注如何保留少部分存活的對象。 算法:標記-復制 |
老生代 | 依據強分代假說,以較低頻率回收這個區域。 算法:標記-整理 |
表 新生代與老生代
1.1.1 新生代 Appel 式垃圾回收
將新生代劃分為三塊空間:Eden、Servivor1、Servivor2,內存大小比例為8:1:1。
每次只使用Eden和一塊Servivor,垃圾回收時,將它們當作還存活的對象一次性復制到另一塊Servivor中,然后清理掉使用的Eden及Servivor塊。
PS:回收時,如果Eden及Servivor存活下來的對象超過Servivor容量時,會將溢出的對象復制到老生代。
1.1.2 晉升到老生代
晉升到老生代有如下場景:
- 對象年齡達到閾值。
- 大對象(大小超過設定的閾值)直接分配至老生代。避免大對象在新生代頻繁復制。
- 老生代空間擔保失敗,Minor GC完成后存活的對象大小超過一塊Servivor的值。
- 動態年齡判定,如果年齡小等于X的對象總大小超過Servivor容量的50%,則所有年齡>=X的對象直接晉升。
1.1.3 Minor GC 與 Full GC
緯度 | Minor GC | Full GC |
作用區域 | 新生代 | 整個堆(新生代+老生代)+方法區 |
觸發 | 頻率高。 當Eden區空間不足時觸發。 (并非每次Eden滿都觸發,若開啟空間分配擔保,則可能直接觸發Full GC)。 | 頻率低。
|
算法 | 標記-復制 | 標記-整理/標記-清除 |
表 Minor GC 與Full GC的對比
1.2 跨代引用
新生代的對象可能被老生代引用。Minor GC時,進行可達性分析前還需要將引用了新生代對象的老生代對象加入到GC Roots中。
跨代引用假說:跨代引用相對于同代引用來說僅占少數。
1.2.1 卡表
為了能快速收集到引用了新生代的老生代對象,在新生代上建立一個全局的“記憶集”,來記錄老生代中跨代引用的信息。
“卡表”是記憶集的一種實現,是一個字節類型的數組。數組中每個元素對應老生代中一塊固定大小的內存區域。該區域稱為卡頁(默認512字節)。當卡頁中有對象存在跨代引用時,卡表對應數組的元素值為1,否則為0。
1.2.2 卡表的“偽共享”
“偽共享”是一種性能問題。
操作系統中內存與緩存的取值最小單位是緩存行(64字節),多線程并發的場景下,不同線程對不同卡表項修改可能操作的是同一緩存行,從而引發頻繁的緩存同步,降低性能。
優化方案:
- 在標記卡表前,如果該條目未被標記,才能進行標記。(減少不必要的寫操作)
- 卡表稀疏化,調整卡表條目粒度。