內存分配,回收策略與垃圾收集器

前序

Java內存區域的程序計數器,虛擬機棧,本地方法棧3個區域隨着線程而生,隨線程而滅;棧中的棧幀隨着方法的進入和退出而執行着出棧和入棧操作。每一個棧幀中分配多少內存基本在類結構確定下來時就已知了。因此這幾個區域的內存分配和回收都具有確定性,當方法結束或線程結束時,內存自然跟着回收。

而Java堆和方法區有顯著的不確定性:一個接口的多個實現類 需要的內存可能不一樣,一個方法執行的不同條件分支所需要的內存也可能不一樣。只有處於運行期間,我們才能知道程序會創建哪些對象,創建多少個對象,這部分內存的分配和回收是動態的,而垃圾收集器關注的正是這部分內存該如何管理。

 


內存分配回收策略

堆的簡介

Java堆是垃圾收集器管理的主要區域,因此也稱爲GC堆。由於如今的收集器基本採用分帶垃圾收集算法,因此Java堆還可以細分爲新生代和老年代,下圖中的eden區,s0(from)區,s1(to)區都屬於新生代,tentired區屬於老年代。

大部分情況,對象首先在Edon區域分配,當Eden空間滿了後,會觸發一次Minor GC。存活下來的對象移動到from(s0)區。from區滿了後觸發Minor GC,存活的對象移動到to(s1)區,並交換from指針和to指針,這樣在一段時間內to指向的區域是空的。經過多次Minor GC後仍然存活的對象會移到老年代。老年代是長期存活的對象,若空間佔滿則觸發Full GC。

爲什麼要分代

若堆內存沒有區域劃分,新創建的對象和生命週期很長的對象放在一起,隨着程序的執行,堆內存需要頻繁進行垃圾收集,每次回收都要遍歷所有對象,所消耗的時間是巨大的。而在有了內存分代,新創建對象在新生代中分配內存,經過多次回收後存活的對象放在老年代。這樣對於存活時間短的新生代對象,需要對其區域頻繁進行GC;而對於生命週期長的老年代對象,內存回收的頻率則相對較低,無需頻繁回收。

 

Minor GC和Full GC

Minor GC:回收新生代,因爲新生代對象存活時間很短,因此 Minor GC 會頻繁執行,執行的速度較快。

Full GC:回收老年代和新生代,老年代對象其存活時間長,因此 Full GC 很少執行,執行速度會比 Minor GC 慢很多。

 

內存分配策略

(1)對象優先在Eden分配

大多情況下,對象在新生代Eden上分配。若Eden空間不夠,則發起Minor GC。

(2)大對象直接進入老年代

大對象是指需要連續內存空間的對象,比如很長的字符串和數組。經常出現大對象會提前觸發 垃圾收集來獲取足夠的連續空間,分配給大對象。

-XX:PretenureSizeThreshold。大於該值的對象直接在老年代分配,避免在Eden和Survivor之間的內存複製。

(3)長期存活的對象進入老年代。

爲對象定義年齡計數器,在 Eden 出生的對象經過 Minor GC 依然存活,將移動到 Survivor 中,年齡就增加 1 歲,增加到一定年齡闊值則移動到老年代中。

-XX:MaxTenuringThreshold 用來定義年齡的閾值

(4)動態年齡判定

虛擬機並非要求對象的年齡一定要大於闊值MaxTenuringThreshold後才能晉升老年代。如果在Survivor中相同年齡的相同年齡的對象大小總和大於Survivor空間的一半,則年齡大於或等於該年齡的對象可以直接進入老年代。

(5)空間分配

在發生Minor GC前,虛擬機先檢查老年代最大可用連續空間是否大於新生代所有對象總空間,若是則認爲Minor GC是安全的;若不是,則虛擬機會查看HandlePromotionFailure 的值是否允許擔保失敗:

1)若允許則繼續檢查老年代中最大可用的連續空間是否大於 歷次晉升到老年代對象的平均大小,若大於,則嘗試進行1次Minor GC;

2)若小於,或者HandlePromotionFailure不允許冒險,則進行1次Full GC。

 

Full GC觸發條件

Full GC的觸發條件較爲複雜:

1)調用System.gc():該方法只是建議虛擬機區執行Full GC,但不一定真正執行。我們建議讓虛擬機自己管理內存。

2)老年代空間不足:大對象直接進入老年代,長期存活對象進入老年代等情況會造成老年代空間不足。我們需要儘量不創建過大的對象和數組;此外還可以通過-Xmn 虛擬機參數調大新生代的大小,讓對象儘量在新生代被回收掉,不進入老年代;還可以通過-XX:MaxTenuringThreshold 來調大 對象進入老年代的年齡限制,使對象在新生代多存活一段時間。

3)Concurrent Mode Failure錯誤:使用CMS垃圾收集器的同時有對象要放入老年代,而老年代的空間不足,便會報此錯誤,並觸發Full GC。

 


如何確定對象存活

垃圾收集器在對堆進行回收前,首先要確定這些對象哪些是存活,哪些已死去(不可能再被任何途徑使用的對象)。

引用計數算法

在對象中添加一個引用計數器,每當有一個地方引用時,計時器加一;當引用失效時,計數器值減一。

雖然它的原理簡單,判定效率高,但在主流的Java虛擬機裏,都沒有引用計數算法來管理內存。主要原因是:算法有很多例外情況要考慮,必須配合大量額外處理才能保證正確工作。

測試:下面的代碼中,對象objA和objB都有字段Instance,賦值令objA.instance=objB及objB.instance=objA,此後賦值null斷掉引用,此時兩個對象不可能再被訪問,但由於它們互相引用着對方,導致它們的引用計數器爲不爲0,這種算法就無法回收它們。

public class JavaVM {
    public Object instance = null;

    private static final int _1MB = 1024 * 1024;

    private byte[] bigSize = new byte[2 * _1MB];

    public static void testGC() {
        JavaVM objA = new JavaVM();
        JavaVM objB = new JavaVM();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;            // 斷掉引用
        objB = null;
        System.gc();
    }

    public static void main(String[] args) throws Exception {
        JavaVM objA = new JavaVM();
        JavaVM objB = new JavaVM();
        objA.instance = objB;
        objB.instance = objA;

        System.gc();
    }
}

代碼示意圖

 

可達性分析算法

通過一系列稱爲“GC Roots”的根對象作爲起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過程中走過的路徑稱爲“引用鏈”,若某個對象到GC Roots間沒有任何引用鏈連接,則證明此對象不可能再被引用。

如下所示,對象object 5、object 6、object 7雖然互有關聯,但是它們到GC Roots是不可達的, 因此它們將會被判定爲可回收的對象。

在Java體系裏,可作爲GC Roots的對象如下:

  • 在虛擬機棧(棧幀中的局部變量表)中引用的對象,比如各個線程被調用的方法堆棧中使用到的參數,局部變量,臨時變量等。
  • 在方法區中類靜態屬性引用的對象,比如Java類的引用類型靜態變量。
  • 在方法區中常量引用的對象,比如字符串常量池裏的引用。
  • Native方法引用的對象。
  • 所有被同步鎖(synchronized關鍵字)持有的對象。

除以上外,根據用戶選用的垃圾收集器以及當前回收的內存區域不同,還會有其他對象臨時性加入,共同構成完整的GC Roots集合。

 

引用分類

引用分爲強引用,軟引用,弱引用和虛引用,它們的強度依次減弱。

(1)強引用是代碼中普遍存在的引用賦值,類似“Object obj=new Object()”這種引用關係。只要強引用關係還存在,垃圾收集器永遠不會回收掉被引用的對象。

(2)軟引用描述還有用,但非必須的對象。只被軟引用關聯着的對象,在系統將要發生內 存溢出異常前,會把這些對象列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的內存, 纔會拋出內存溢出異常。

(3)弱引用也是描述那些非必須對象,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只 能生存到下一次垃圾收集發生爲止。當垃圾收集器開始工作,無論當前內存是否足夠,都會回收掉只 被弱引用關聯的對象。

(4)虛引用:一個對象是否有虛引用的 存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛 引用關聯的唯一目的只是爲了能在這個對象被收集器回收時收到一個系統通知,用來跟蹤對象被垃圾回收的活動。

虛引用與 軟引用和弱引用的一個區別

虛引用必須和引用隊列聯合使用。當垃圾回收器準備回收一個對象時,若發現它還有虛引用,則在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。 程序可以通過 判斷引用隊列中是否有加入虛引用的,來了解被引用對象是否將要被垃圾回收。

程序設計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因爲軟引用可以加速 JVM 對垃圾內存的回收速度,可以維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生

 

不可達對象是否必須回收

即使可達性分析算法判定爲不可達的對象,也不一定會被回收。在回收之前,至少要經歷兩次標記過程:

  1. 對象在進行可達性分析後發現沒 有與GC Roots相連接的引用鏈,那它將會被第一次標記。
  2. 隨後進行一次篩選,篩選的條件是此對象是 否有必要執行finalize()方法。假如對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用 過,那麼虛擬機將這兩種情況都視爲“沒有必要執行”。

1)若這個對象被判定爲有必要執行finalize()方法,則該對象將會被放在F-Queue隊列之中,並在稍後 由一條虛擬機自動建立,低調度優先級的Finalizer線程去執行它們的finalize() 方法。虛擬機會觸發這個方法開始運行,但並不承諾一定會等待它運行結束。這是因爲若某個對象的finalize()方法執行緩慢或發生死循環,很可能導致隊列中其他對象永久等待,進而導致整個內存回收子系統崩潰。

2)接下來,收集器將對隊列中的對象進行第二次小規模標記,若對象在finalize()方法中重新與引用鏈上任何一個對象建立管理(比如把自己(this)賦值給某個類變量或對象的成員變量),則它將被移除“即將回收”的集合;而如果對象此時還沒逃脫,那基本上它就真的被回收了

一個對象的finalize()方法最多隻會被系統自動調用一次

 

回收方法區

方法區的垃圾收集主要回收:廢棄的常量和不再使用的類型。

以常量池中字面量回收爲例,假如一個字符串“java”曾進入常量池中,但當前系統沒有任何一個字符串對象的值是“java",即沒有任何字符串對象 引用常量池中的"java"變量,且虛擬機中也沒有其他地方引用這個字面量。若此時發生內存回收,且垃圾收集器判斷有必要的話,這個"java"常量將會被系統清理出常量池。常量池中其他類(接口),方法,字段的符號引用的判斷標準與此類似。

判定一個類是否屬於”不再被使用的類“的條件較苛刻,需要同時滿足三個條件:

  • 該類所有實例都已被回收,即Java堆中不存在該類及任何派生子類的實例。
  • 加載該類的類加載器已經被回收。該條件通常很難達成。
  • 該類對於的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

在大量使用反射,動態代理等自定義類加載器的場景中,通常需要Java虛擬機具備類型卸載的能力,以保證不會對方法區造成過大的內存壓力。

 


垃圾收集算法

在如何判定對象消亡的角度上,垃圾收集算法劃分爲“引用計數式垃圾收集”(直接垃圾收集)和“追蹤式垃圾收集”(間接垃圾收集)兩大類,此處主要討論追蹤式垃圾收集的算法。

 

分代收集理論

常用的垃圾收集器有一個一致的設計原則:收集器應將Java堆劃分出不同的區域,將回收對象依據年齡(熬過垃圾收集過程的次數)分配到不同的區域之中存儲。垃圾收集器每次只回收其中一個或某些部分的區域。

Java堆劃分爲新生代和老生代兩個區域。在新生代中,每次垃圾收集時都有大量對象被回收,而每次回收後存活的少量對象,將會逐步晉升到老年代中存放。

此外,對於存在跨帶互相引用(新生代引用老年代)的兩個對象,應該傾向於同時生成或同時消亡,如果某個新生代對象存在跨代引用,由於老年代對象難以消亡,這使得新生代對象得以存活,進而在年齡增長之後晉升到老年代。因此,相對於同代引用,跨代引用僅佔極少數。依據這條假說,需要在新生代中建立一個全局數據結構(被稱爲記憶集),這個結果把老年代劃分成若干小塊,標識出老年代的哪一塊內存會存在跨代引用。此後發生Minor GC時,只有包含了跨代引用的小塊內存裏的對象 纔會被加入到GC Roots進行掃描。雖然這種方法增加開銷,但比起收集時掃描整個老年代來說仍是划算的。

 

標記-清除算法

首先標記出所有需要回收的對象,在標記完成後,統一回收掉所有被標記的對象,也可以反過來,標記存活的對象,統一回 收所有未被標記的對象。標記過程就是對象是否屬於垃圾的判定過程。

後續的收集算法大多是以標記-清除算法爲基礎,對其缺點改進而得到。他的主要缺點有:

  • 執行效率不穩定,若Java堆包含大量對象,且其中大部分是需要被回收掉,則必須進行大量標記和清除的動作,導致標記和清除兩個過 程的執行效率都隨對象數量增長而降低;
  • 標記、清除之後會產生大 量不連續的內存碎片,空間碎片太多可能會導致當以後在程序運行過程中需要分配較大對象時無法找 到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

 

標記-複製算法

它將可用 內存按容量劃分爲大小相等的兩塊,所有分配的對象只使用其中的一塊,另一塊是空閒的。當這一塊的內存用完了,JVM開啓複製算法GC線程,該線程將存活的對象複製到另外一塊上面,在複製的同時,停留在原來活動區間的垃圾對象就會被回收。現代的Java大多優先採用這種算法回收新生代。

  • 優點:對於多數對象都是可回收的情況,算法複製的是少數的存活對象,且每次都是針對半區進行內存回收,不用考慮有空間碎片的複雜情況。
  • 缺點:可用內存縮小爲原來一般,浪費空間;如果存活對象較多,則需要進行較多的複製操作,效率降低。

由於新生代中的對象大多熬不過第一輪收集,因此不需要按照1:1的比例劃分新生代的內存空間。HotSpot虛擬機的Serial、ParNew等新生代收集器採用了“Appel式回收”策略來設計新生代的內存佈局:

  1. 把新生代劃分爲一塊較大的Egen空間和兩塊較小的Survivor空間,每次分配內存只使用Eden和其中一塊Survivor。
  2. 發生垃圾收集時,將Eden和Survivor中仍然存活的對象一次性複製到另一塊Survivor空間上,然後直接清理Eden和已用過的Survivor空間,HotSpot虛擬機默認Eden和Survivor的大小比例是8∶1。
  3. 如果Survivor空間不足以容納一次次Minor GC之後存活的對象時,就需要其他內存區域進行分配擔保(實際上大多數進入老年代)

標記-整理算法

標記-整理算法的標記過程與標記-清除算法一樣,但其後續步驟是回收完對象後,讓所有存活對象都向一端移動,並更新引用對象的指針。

在標記-清除的基礎上還進行對存活對象的移動,這樣不會產生內存碎片。

但對於老年代這種每次回收都有大量對象存活而言,移動存活對象並更新所有引用這些對象的指針 是一種極爲負重的操作,且移動對象操作必須暫停用戶應用程序才能進行。但如果與標記-清除算法一樣 ,完全不考慮移動和整理存活對象,則空間碎片化問題只能依賴更爲複雜的內存分配器和內存訪問器來解決,而內存訪問是用戶程序最頻繁的操作,若增加了此項負擔,則會直接影響應用程序的吞吐量。

分代收集算法

根據對象存活週期的不同 將內存劃分爲幾塊,一般是把java堆分爲新生代和老年代。然後根據各個年代的特點採用適當的收集算法:

  • 新生代選用標記-複製算法。因此每次垃圾收集都會有大批對象被回收,只需要付出 複製少量存活對象的成本即可完成收集;
  • 老年代採用標記-清理或者“標記-整理算法。因爲老年代裏的對象存活率高,沒有額外的空間對他分配擔保。

 


經典垃圾收集器

Serial(串行)收集器

它是一個單線程工作的收集器,這意味着它只會使用一個處理器或一條收集線程來完成垃圾收集,同時它在進行垃圾收集時,必須暫停其他所有工作線程,直到它收集結束( "Stop The World" )。Serial收集器的新生代版本採用複製算法

它是是HotSpot虛擬機運行在客戶端模式下的默認新生代收集器。與其他收集器的單線程相比:

  • 它簡單高效,且在內存資源受限的環境下,它是所有收集器裏額外內存消耗最小的;
  • 對於單核處理器或處理器核心數較少的環境下,由於Serial收集器沒有線程交互的開銷,單線程收集效率是最高的;
  • 在用戶桌面的應用常見以及部分微服務應用中,分配給虛擬機管理的內存一般不會特別大,收集幾十兆甚至一兩百兆的新生代,且不是頻繁發生收集,垃圾收集的停頓時間是可以接受的。因此,Serial收集器對於運行在客戶端模式下的虛擬機來說是一個很好的選擇。

 

ParNew收集器

ParNew收集器實質上是Serial收集器的多線程並行版本,它的所有控制參數,收集算法,Stop The World,回收策略等都與Serial收集器完全一致。ParNew收集器的新生代版本採用複製算法

除了Serial收集器,只有它能與CMS收集器配合工作。因此它是不少運行在服務器端模式下的HotSpot虛擬機 首選的新生代收集器。

由於存在線程交互的開銷,該收集器在通過超線程技術實現的僞雙核處理器環境中都不能百分比保證超越Serial收集器。不過,隨着可以被使用的處理器核心數量的增加,ParNew收集器對於垃圾收集時系統資源的高效利用是有好處。它默認開啓的收集線程數與處理器核心數相同,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。

 

Parallel Scavenge收集器

Parallel Scavenge收集器是基於標記-複製算法實現且能並行收集的新生代收集器。CMS等收集器關注於盡可能縮短垃圾收集時 用戶線程的停頓時間,而Parallel Scavenge收集器目標則是達到一個可控制的吞吐量。

吞吐量是處理器運行用戶代碼的時間 / 處理器總消耗時間(運行用戶代碼時間+ 運行垃圾收集時間)

停頓時間越短就越適合需要與用戶交互或需要保證服務響應質量的程序,而高吞吐量可以保證最高效率利用處理器資源,儘快完成程序運算任務,主要適合在後臺運算而不需要太多交互的分析任務。

Parallel Scavenge收集器提供了兩個用於精確控制吞吐量的參數以及一個開關參數:

(1)-XX:MaxGCPauseMillis:控制最大垃圾收集停頓時間

該值是一個大於0 的毫秒數,收集器儘量保證內存回收花費的時間不超過用戶設定值。不要覺得該值設定得越小越好,因爲垃圾收集停頓時間的縮短是以犧牲吞吐量和新生代空間換來的:因爲會導致垃圾收集發生得更加頻繁,造成吞吐量下降。

(2)-XX:GCTimeRatio:直接設置吞吐量大小

該值是一個大於0小於100的整數。比如該參數設置爲19,則允許的最大垃圾收集時間佔用時間的 1 / (1 + 19) = 5%,默認值爲99。

(3)-XX:+UseAdaptiveSizePolicy

該參數被激活後,就不需要人工指定新生代的大小,Eden與Survivor區 的比例(-XX:SurvivorRatio)、晉升老年代對象大小(-XX:PretenureSizeThreshold)等細節參數,虛擬機會根據當前系統的運行情況,動態調整這些參數。這種調節方式稱爲垃圾收集的自適應調節策略。

如果對收集器運作不瞭解,使用Scavenge收集器配合自適應調節策略是一個不錯的選擇:把基本的內存數據設置好(如-Xmx設置最大堆),然後使用-XX:MaxGCPauseMillis參數(更關注最大停頓時間)或XX:GCTimeRatio(更關注吞吐量)參數給虛擬機設立一個優化目標,那具體細節參數的調節工作就 由虛擬機完成了。

自適應調節策略是Parallel Scavenge收集器區別於ParNew收集器的一個重要特性。

 

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器。它採用標記-整理算法。該收集器主要提供客戶端模式下的HotSpot虛擬機使用;若是在服務端模式下,有兩種用途:

  • 在JDK 5以及之前的版本中與Parallel Scavenge收集器搭配使用
  • 作爲CMS 收集器發生失敗時的後備預案,在併發收集發生Concurrent Mode Failure時使用。

 

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多線程併發收集,基於標記-整理算法實現。在注重吞吐量或處理器資源較爲稀缺的場合,可以優先考慮Parallel Scavenge加Parallel Old收集器這個組合。

 

CMS收集器

一種以獲取最短回收停頓時間爲目標的收集器,它是基於標記-清除算法實現的。目前很大一部分的Java應用集中在互聯網網站或着基於瀏覽器的B/S系統的服務端上,這類應用通常較爲關注服務的響應速度,希望系統停頓時間儘可能短,而CMS收集器非常符合此類需求。

CMS的運作過程如下:

(1)初始標記

標記一下GC Roots能直接關聯到的對象,速度很快;該過程需要”Stop the World“(停止其他所有工作線程)。

(2)併發標記

從GC Roots直接關聯的對象開始 遍歷整個對象圖的過程。過程耗時較長但不停頓用戶線程。

(3)重新標記

爲了修正併發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄。該階段的停頓時間通常比初始標記階段稍長一些,遠比並發標記階段的時間短。該過程需要”Stop the World“。

(4)併發清除

清理刪除標記階段中判定已經死亡的對象。由於不需要移動存活對象,此階段可以與用戶線程同時併發。

它的缺點如下:

(1)CMS收集器對處理器資源非常敏感。在併發階段,他雖然不會導致用戶線程停頓,但會因爲佔用處理器的計算能力而導致應用程序變慢,降低總吞吐量。

(2)在CMS的併發標記和併發清理階段,用戶線程是在繼續運行的,程序在運行自然還會不斷產生新的垃圾對象,但這一部分的垃圾是在標記過程結束以後出現的,CMS無法在當時的收集中處理掉,只能等待下一次的收集;此外,由於用戶線程是在與性能的,因此CMS收集器必須預留一部分空間給併發收集時的程序繼續運作。如果預留的內存無法滿足需要,則虛擬機將凍結用戶程序的運行,臨時啓用Serial Old收集器來重新進行老年代的垃圾收集

(3)CMS基於標記-清除的算法,意味着收集結束時 會有大量空間碎片產生,這給大對象分配帶來了麻煩,即出現老年代還有很多剩餘空間,但無法找到足夠大的連續空間來分配當前對象,而不得不提前觸發一次Full GC(收集整個Java堆和方法區的垃圾收集)的情況

 

G1收集器

它是服務端模式下的默認垃圾收集器,是作爲CMS收集器的替代者和繼承人。

它具備如下特點:

(1)G1能充分利用CPU:多核環境下,使用多個CPU來縮短”Stop The World“停頓時間。其他收集器需要停止其他所有工作線程,執行GC操作,而G1收集器可以通過併發的方式讓Java程序繼續執行

(2)G1雖然遵循分代收集理論設計,但它堆內存的佈局和其他收集器有明顯差異:G1不再堅持固定大小和固定數量的分代區域劃分,而是把連續的Java堆劃分爲多個大小相對的獨立區域Region,每一個區域都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能根據扮演不同角色的區域來採用不同的策略區處理。

(3)G1從整體來看是基於標記-整理實現的,從兩個Region之間看是基於 複製 算法實現的。這意味G1運行期間不會產生內存碎片,收集後能提供規則的可用內存。

(4)降低停頓時間是G1和CMS共同關注點,但G1除了降低停頓外,還能建立可預測的停頓時間模型,讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在GC的時間不得超過N毫秒。之所以能建立這樣的模型,是因爲G1收集器可以有計劃的避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值,在後臺維護一個優先隊列,每次根據允許的收集時間,優先回收佳置最大的Region。這保證了G1收集器在有限的時間內,可以獲取儘可能高的收集效率。

G1把Java堆分爲多個Region,但Region不是孤立的。虛擬機爲G1中每個Region維護了一個與之對應的Remembered Set。當虛擬機發現程序在對Reference類型的數據進行寫操作時,會產生暫時中斷寫操作,檢查Reference引用的對象是否出不同的Region中(在分代的角度就是檢查老年代中的對象引用了新生代中的對象),若是,把相關引用信息記錄到被引用對象所示的Region的Remembered Set中。這樣當進行收集時,在GC根節點的枚舉範圍內添加Remembered Set,便可確保在不掃描全堆的情況下不會有遺漏。

G1收集器的運作過程大致如下:

(1)初始標記

標記一下GC Roots能直接關聯到的對象,並且修改TAMS 指針的值,讓下一階段用戶線程併發運行時,能正確地在可用的Region中分配新對象。這個階段需要停頓線程,但耗時很短,而且是借用進行Minor GC的時候同步完成的。

(2)初始標記

從GC Roots開始 對堆中對象進行可達性分析,找出要回收的對象。此階段耗時較長,但可與用戶程序併發執行。當對象圖掃描完成後,還需要重新處理SATB記錄下的在併發時有引用變動的對象。

(3)最終標記

對用戶線程做另一個短暫的暫停,用於處理併發階段結束後仍遺留下來的最後那少量的SATB記錄。

(4)篩選回收

負責更新Region的統計數據,對各個Region的回 收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region 構成回收集,然後把決定回收的那一部分Region的存活對象複製到空的Region中,再清理掉整個舊 Region的全部空間。此階段需要暫停用戶線程,由多條收集器線程並行完成

 


參考資料

《深入理解Java虛擬機》

https://github.com/Snailclimb/JavaGuide#jvm

https://cyc2018.github.io/CS-Notes/#/README

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章