一款好用又強大的開源社區,採用主流的互聯網技術架構、全新的UI設計、支持一鍵源碼部署,擁有完整的文章&教程發佈/搜索/評論/統計流程等,代碼完全開源,沒有任何二次封裝,是一個非常適合二次開發/實戰的現代化社區項目。 ...
垃圾回收
垃圾回收需要完成的三件事情
- 哪些記憶體需要回收?
- 什麼時候回收?
- 如何回收?
1. 如何判斷對象是否存活
在堆裡面存放著 Java 世界中幾乎所有的對象實例,垃圾收集器在對堆進行回收前,首先就要確定對象的存活狀態
1.1 對象存活演算法
1.1.1 引用計數演算法(Reference Counting)
在對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任何時刻計數器為零的對象就是不可能再被使用的
引用計數演算法雖然占用了一些額外的記憶體空間來進行計數,但它的原理簡單,判定效率也很高,在大多數情況下它都是一個不錯的演算法
迴圈引用
這個看似簡單的演算法有很多例外情況要考慮,必須要配合大量額外處理才能保證正確地工作,譬如單純的引用計數就很難解決對象之間相互迴圈引用的問題
public class ReferenceCountingGC {
public Object instance = null;
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
除了兩個對象互相引用外,這兩個對象再無任何引用,實際上這兩個對象已經不可能再被訪問,但是它們因為互相引用著對方,導致它們的引用計數都不為零,引用計數演算法也就無法回收它們
1.1.2 可達性分析演算法(Reachability Analysis)
通過一系列稱為 GC Roots 的根對象作為起點,從這些節點開始,根據引用關係向下搜索,搜索過程所走過的路徑稱為引用鏈(Reference Chain),如果某個對象到 GC Roots 間沒有任何引用鏈相連,則證明此對象是不可能再被使用的
深入理解Java虛擬機(第3版) - 圖3-1 利用可達性分析演算法判定對象是否可回收
可作為 GC Roots 的對象
- 在虛擬機棧(棧幀中的本地變數表)中引用的對象
- 在方法區中類靜態屬性引用的對象
- 在方法區中常量引用的對象
- 在本地方法棧中 JNI(即通常所說的 Native 方法)引用的對象
- Java虛擬機內部的引用
- 如基本數據類型對應的 Class 對象,一些常駐的異常對象(NullPointExcepiton、OutOfMemoryError)等,還有系統類載入器
- 所有被同步鎖(synchronized 關鍵字)持有的對象
- 反映 Java 虛擬機內部情況的 JMXBean、JVMTI 中註冊的回調、本地代碼緩存等
除了這些固定的 GC Roots 集合以外,根據用戶所選用的垃圾收集器以及當前回收的記憶體區域不同,還可以有其他對象臨時性地加入,共同構成完整 GC Roots 集合
1.2 緩刑階段
即使在可達性分析演算法中判定為不可達的對象,也不是非死不可的,這時候它們暫時還處於緩刑階段
要真正宣告一個對象死亡,至少要經歷兩次標記過程
- 如果對象在進行可達性分析後發現沒有與 GC Roots 相連接的引用鏈,那它將會被第一次標記,隨後進行一次篩選,篩選的條件是此對象是否有必要執行 finalize 方法。假如對象沒有覆蓋 finalize 方法,或者 finalize 方法已經被虛擬機調用過,那麼虛擬機將這兩種情況都視為沒有必要執行
- 如果這個對象被判定為確有必要執行 finalize 方法,那麼該對象將會被放置在一個隊列中進行第二次標記,如果對象要在 finalize 方法中成功拯救自己,只要重新與引用鏈上的任何一個對象建立關聯即可
1.2.1 finalize 方法
對象逃脫死刑的最後機會,當垃圾收集器發現一個對象實例沒有任何的引用與之關聯,在準備執行垃圾回收之前該方法才會被調用,且 所有對象的 finalize 方法都只會被系統自動調用一次
public class Test {
private static Test test;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("execute finalize");
test = this;
}
private static void print(Test t) {
if (t == null) {
System.out.println("dead!!!");
} else {
System.out.println("alive...");
}
}
public static void main(String[] args) throws InterruptedException {
test = new Test();
// 第一次成功自救
test = null;
System.gc();
Thread.sleep(500);
print(test);
// 第二次自救失敗
test = null;
System.gc();
Thread.sleep(500);
print(test);
}
}
- 執行結果
execute finalize
alive...
dead!!!
1.3 引用
在 JDK1.2 版之前,Java 裡面的引用是很傳統的定義:如果 reference 類型的數據中存儲的數值代表的是另外一塊記憶體的起始地址,就稱該 reference 數據是代表某塊記憶體、某個對象的引用
為了實現對象在記憶體空間足夠時,能保留在記憶體之中,如果記憶體空間在進行垃圾收集後仍然非常緊張,可以自動捨棄。在 JDK1.2 版之後,Java 對引用的概念進行了擴充
1.3.1 強引用(Strongly Reference)
最傳統的引用的定義,是指在程式代碼之中普遍存在的引用賦值。無論任何情況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的對象
Test test = new Test();
1.3.2 軟引用(Soft Reference)
描述一些還有用,但非必須的對象。只被軟引用關聯著的對象,在系統將要發生記憶體溢出異常前,會把這些對象列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的記憶體,才會拋出記憶體溢出異常
Reference<Object> reference = new SoftReference<>(new Test());
System.out.println(reference.get() == null);
System.gc();
Thread.sleep(500);
System.out.println(reference.get() == null);
- 執行結果
false
false
1.3.3 弱引用(Weak Reference)
描述那些非必須對象,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生為止。當垃圾收集器開始工作,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的對象
Reference<Object> reference = new WeakReference<>(new Test());
System.out.println(reference.get() == null);
System.gc();
Thread.sleep(500);
System.out.println(reference.get() == null);
- 執行結果
false
true
1.3.4 虛引用(Phantom Reference)
虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的只是為了能在這個對象被收集器回收時收到一個系統通知
ReferenceQueue queue = new ReferenceQueue();
Reference<Object> reference = new PhantomReference<>(new Test(), queue);
System.out.println(reference.get() == null);
System.gc();
Thread.sleep(500);
System.out.println(reference.get() == null);
- 執行結果
true
true
2. 分代收集理論(Generational Collection)
當前商業虛擬機的垃圾收集器,大多數都遵循了分代收集的理論進行設計,它建立在兩個分代假說之上
- 弱分代假說(Weak Generational Hypothesis):絕大多數對象都是朝生夕滅的
- 強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的對象就越難以消亡
這兩個分代假說共同奠定了多款常用的垃圾收集器的一致的設計原則:收集器應該將堆劃分出不同的區域,然後將回收對象依據其年齡(即對象熬過垃圾收集過程的次數)分配到不同的區域之中存儲
- 如果一個區域中大多數對象都是朝生夕滅,難以熬過垃圾收集過程的話,那麼把它們集中放在一起,每次回收時只關註如何保留少量存活而不是去標記那些大量將要被回收的對象,就能以較低代價回收到大量的空間;如果剩下的都是難以消亡的對象,那把它們集中放在一塊,虛擬機便可以使用較低的頻率來回收這個區域,這就 同時兼顧了垃圾收集的時間開銷和記憶體的空間有效利用
設計者一般至少會把堆劃分為新生代(Young Generation)和老年代(Old Generation)兩個區域。在新生代中,每次垃圾收集時都發現有大批對象死去,而每次回收後存活的少量對象,將會逐步晉升到老年代中存放
在堆中劃分出不同的區域之後,垃圾收集器才可以每次只回收其中某一個或者某些部分的區域
- 部分收集
- 新生代收集(Minor GC / Young GC):指針對新生代
- 老年代收集(Major GC / Old GC):指針對老年代
- 混合收集(Mixed GC):針對整個新生代和部分老年代
- 全部收集(Full GC):針對整個堆和方法區
2.1 跨代引用
分代收集並非只是簡單劃分一下記憶體區域那麼容易,例如對象不是孤立的,對象之間會存在跨代引用
假如要現在進行一次 Minor GC,但新生代中的對象是完全有可能被老年代所引用的,為了找出該區域中的存活對象,不得不在固定的 GC Roots 之外,再額外遍歷整個老年代中所有對象來確保可達性分析結果的正確性,反過來也是一樣。遍歷整個老年代所有對象的方案雖然理論上可行,但無疑會為記憶體回收帶來很大的性能負擔。為瞭解決這個問題,就需要對分代收集理論添加第三條經驗法則
- 跨代引用假說(Intergenerational Reference Hypothesis):跨代引用相對於同代引用來說僅占極少數
- 這其實是可根據前兩條假說邏輯推理得出的隱含推論:存在互相引用關係的兩個對象,是應該傾向於同時生存或者同時消亡的
依據這條假說,我們就不應再為了少量的跨代引用去掃描整個老年代,也不必浪費空間專門記錄每一個對象是否存在及存在哪些跨代引用,只需在新生代上建立一個全局的數據結構(記憶集,Remembered Set),這個結構把老年代劃分成若幹小塊,標識出老年代的哪一塊記憶體會存在跨代引用。此後當發生 Minor GC 時,只有包含了跨代引用的小塊記憶體里的對象才會被加入到 GC Roots 進行掃描。雖然這種方法需要在對象改變引用關係(如將自己或者某個屬性賦值)時維護記錄數據的正確性,會增加一些運行時的開銷,但比起收集時掃描整個老年代來說仍然是划算的
3. 記憶體分配與回收策略
3.1 對象優先在 Eden 區分配
大多數情況下,對象在新生代 Eden 區中分配。當 Eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC
據統計新生代中的對象有 98% 熬不過第一輪收集。可以把新生代分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次分配記憶體只使用 Eden 和其中一塊 Survivor。發生垃圾收集時,將 Eden 和 Survivor 中仍然存活的對象一次性複製到另外一塊 Survivor 空間上,然後直接清理掉 Eden 和已用過的那塊 Survivor 空間
HotSpot 虛擬機預設 Eden 和 Survivor 的大小比例是 8∶1,即每次新生代中可用記憶體空間為整個新生代容量的 90%,只有一個 Survivor 空間,即 10% 的新生代是會被浪費的
3.2 大對象直接進入老年代
大對象就是指需要 大量連續記憶體空間 的 Java 對象,比如很長的字元串,或者元素數量很龐大的數組
在分配空間時,大對象容易導致記憶體明明還有不少空間時就提前觸發垃圾收集,以獲取足夠的連續空間才能安置好它們,而當複製對象時,大對象就意味著高額的記憶體複製開銷
比一個大對象更糟糕的是一群短命的大對象
3.3 長期存活的對象將進入老年代
虛擬機給每個對象定義了一個對象年齡(Age)計數器。對象通常在 Eden 區里誕生,如果經過第一次 Minor GC 後仍然存活,並且能被 Survivor 容納的話,該對象會被移動到 Survivor 空間中,並且將其對象年齡設為 1 歲。對象在 Survivor 區中每熬過一次 Minor GC,年齡就增加 1 歲,當它的年齡增加到一定程度(預設為 15),就會被晉升到老年代中
3.4 動態對象年齡判定
如果在 Survivor 空間中相同年齡所有對象大小的總和大於 Survivor 空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代
3.5 空間分配擔保
當 Survivor 空間不足以容納一次 Minor GC 之後存活的對象時,就需要依賴其他記憶體區域(實際上大多就是老年代)進行分配擔保(Handle Promotion)
在發生 Minor GC 之前,虛擬機必須先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那這一次 Minor GC 可以確保是安全的
如果不成立,則虛擬機會先檢查是否允許擔保失敗。如果允許,那會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試進行一次 Minor GC,儘管這次 Minor GC 是有風險的;如果小於或者不允許,那這時就要改為進行一次 Full GC
3.5.1 風險
新生代使用複製收集演算法,但為了記憶體利用率,只使用其中一個 Survivor 空間來作為輪換備份,因此當出現大量對象在 Minor GC 後仍然存活的情況,且 Survivor 空間無法全部容納,就需要老年代進行分配擔保。當然前提是老年代本身還有容納這些對象的剩餘空間,但一共有多少對象會在這次回收中活下來在實際完成記憶體回收之前是無法明確知道的,所以只能取之前每一次回收晉升到老年代對象容量的平均大小作為經驗值,與老年代的剩餘空間進行比較,決定是否進行 Full GC 來讓老年代騰出更多空間
4. 垃圾回收演算法
4.1 標記-清除演算法(Mark-Sweep)
最早出現也是最基礎的垃圾收集演算法,演算法分為標記和清除兩個階段:首先標記出所有需要回收的對象,在標記完成後,統一回收掉所有被標記的對象,也可以反過來,標記存活的對象,統一回收所有未被標記的對象
深入理解Java虛擬機(第3版) - 圖3-2 “標記-清除”演算法示意圖
後續的收集演算法大多都是以標記-清除演算法為基礎,對其缺點進行改進而得到的。它的主要缺點有兩個
- 執行效率不穩定,如果堆中包含大量對象,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個過程的執行效率都隨對象數量增長而降低
- 記憶體空間的碎片化,標記、清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致當以後在程式運行過程中需要分配較大對象時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作
4.2 標記-複製演算法(Copying)
為瞭解決標記-清除演算法面對大量可回收對象時執行效率低的問題,複製演算法將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的對象複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉
深入理解Java虛擬機(第3版) - 圖3-3 標記-複製演算法示意圖
如果記憶體中多數對象都是存活的,這種演算法將會產生大量的記憶體間複製的開銷,但對於多數對象都是可回收的情況,演算法需要複製的就是占少數的存活對象,而且每次都是針對整個半區進行記憶體回收,分配記憶體時也就不用考慮有空間碎片的複雜情況,只要移動堆頂指針,按順序分配即可。這樣實現簡單,運行高效,不過其缺陷也顯而易見,可用記憶體縮小為了原來的一半,空間浪費未免太多了一點
現在的商用 Java 虛擬機大多都優先採用了這種收集演算法去回收新生代,新生代中的對象有 98% 熬不過第一輪收集,因此並不需要按照 1∶1 的比例來劃分新生代的記憶體空間
4.3 標記-整理演算法(Mark-Compact)
標記過程與標記-清除演算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向記憶體空間一端移動,然後直接清理掉邊界以外的記憶體
深入理解Java虛擬機(第3版) - 圖3-4 “標記-整理”演算法示意圖
如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區域,移動存活對象並更新所有引用這些對象的地方將會是一種極為負重的操作。但如果跟標記-清除演算法那樣完全不考慮移動和整理存活對象的話,彌散於堆中的存活對象導致的空間碎片化問題就只能依賴更為複雜的記憶體分配器和記憶體訪問器來解決
是否移動對象都存在弊端,移動則記憶體回收時會更複雜,不移動則記憶體分配時會更複雜。從垃圾收集的停頓時間來看,不移動對象停頓時間會更短,甚至可以不需要停頓,但是從整個程式的吞吐量來看,移動對象會更划算
5. 垃圾收集器
5.1 Serial 收集器
Serial 收集器是最基礎、歷史最悠久的收集器,是一個單線程工作的收集器,但它的單線程的意義並不僅僅是說明它只會使用一個處理器或一條收集線程去完成垃圾收集工作,更重要的是強調在它進行垃圾收集時,必須暫停其他所有工作線程(Stop The World),直到它收集結束
- 即便越來越構思精巧,越來越優秀,也越來越複雜的垃圾收集器不斷涌現,用戶線程的停頓時間在持續縮短,但是仍然沒有辦法徹底消除
- 安全點(Safepoint):用戶程式執行時並非在代碼指令流的任意位置都能夠停頓下來開始垃圾收集,而是強制要求必須執行到達安全點後才能夠暫停
- 安全點的選定既不能太少以至於讓收集器等待時間過長,也不能太過頻繁以至於過分增大運行時的記憶體負荷
Serial 收集器依然是客戶端模式下預設的新生代收集器,簡單而高效(與其他收集器的單線程相比),對於記憶體資源受限的環境,它是所有收集器里額外記憶體消耗最小的;對於單核處理器或處理器核心數較少的環境來說,Serial 收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率
5.2 ParNew 收集器
實質上是 Serial 收集器的多線程並行版本,在實現上這兩種收集器也共用了相當多的代碼
是不少運行在服務端模式下虛擬機的首選收集器,尤其是 JDK1.7 之前的遺留系統,其中有一個與功能、性能無關但其實很重要的原因是:除了 Serial 收集器外,目前只有它能與 CMS 收集器配合工作
5.3 Parallel Scavenge 收集器
基於標記-複製演算法實現的收集器,是一款新生代收集器,也是能夠並行收集的多線程收集器
它的關註點與其他收集器不同,CMS 等收集器的關註點是儘可能地縮短垃圾收集時用戶線程的停頓時間,而 Parallel Scavenge 收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是處理器用於運行用戶代碼的時間與處理器總消耗時間的比值 吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 運行垃圾收集時間)
停頓時間越短就越適合需要與用戶交互或需要保證服務響應質量的程式,良好的響應速度能提升用戶體驗;而高吞吐量則可以最高效率地利用處理器資源,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多交互的分析任務
5.4 Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本,它同樣是一個單線程收集器,使用標記-整理演算法。這個收集器的主要意義也是供客戶端模式下的虛擬機使用。如果在服務端模式下,它也可能有兩種用途:一種是在 JDK1.5 以及之前的版本中與 Parallel Scavenge 收集器搭配使用,另外一種就是作為 CMS 收集器發生失敗時的後備預案
5.5 Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本,支持多線程併發收集,基於標記-整理演算法實現
直到 Parallel Old 收集器出現後,吞吐量優先收集器終於有了比較名副其實的搭配組合,在註重吞吐量或者處理器資源較為稀缺的場合,都可以優先考慮 Parallel Scavenge 加 Parallel Old 收集器這個組合
- Parallel Scavenge 與 Parallel Old 也是 JDK1.8 預設收集器
5.6 CMS 收集器
CMS(Concurrent Mark Sweep)收集器基於標記-清除演算法,是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的 Java 應用集中在互聯網網站或者基於瀏覽器的 B/S 系統的服務端上,這類應用通常都會較為關註服務的響應速度,希望系統停頓時間儘可能短,以給用戶帶來良好的交互體驗。CMS 收集器就非常符合這類應用的需求
- 初始標記:標記一下 GC Roots 能直接關聯到的對象,需要停頓線程,但耗時很短
- 併發標記:從 GC Roots 開始對堆中對象進行可達性分析,遞歸掃描整個堆里的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程式併發執行
- 重新標記:是為了修正併發標記期間,因用戶程式繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比併發標記階段的時間短
- 併發清除:清理刪除掉標記階段判斷的已經死亡的對象,由於不需要移動存活對象,所以這個階段也是可以與用戶線程同時併發的
由於在整個過程中耗時最長的併發標記和併發清除階段中,垃圾收集器線程都可以與用戶線程一起工作,所以從總體上來說,CMS 收集器的記憶體回收過程是與用戶線程一起併發執行的
5.6.1 缺點
對處理器資源非常敏感
事實上,面向併發設計的程式都對處理器資源比較敏感。在併發階段,它雖然不會導致用戶線程停頓,但卻會因為占用了一部分線程(或者說處理器的計算能力)而導致應用程式變慢,降低總吞吐量
無法處理浮動垃圾(Floating Garbage)
在 CMS 的併發標記和併發清理階段,用戶線程是還在繼續運行的,程式在運行自然就還會伴隨有新的垃圾對象不斷產生,但這一部分垃圾對象是出現在標記過程結束以後,CMS 無法在當次收集中處理掉它們,只好留待下一次垃圾收集時再清理掉
空間碎片
基於標記-清除演算法實現的收集器,收集結束時會有大量空間碎片產生
5.7 Garbage First (G1)收集器
G1 是一款主要面向服務端應用的垃圾收集器。在 G1 收集器出現之前的所有其他收集器,包括 CMS 在內,垃圾收集的目標範圍要麼是整個新生代(Minor GC),要麼就是整個老年代(Major GC),再要麼就是整個堆(Full GC)。而 G1 可以面向堆記憶體任何部分來組成回收集(Collection Set,CSet)進行回收,衡量標準不再是它屬於哪個分代,而是哪塊記憶體中存放的垃圾數量最多,回收收益最大,這就是 G1 收集器的 Mixed GC 模式
G1 開創了基於 Region 的堆記憶體佈局,不在按照分代區域劃分,而是把堆劃分為多個大小相等的獨立區域(Region),每一個 Region 都可以根據需要,扮演新生代的 Eden 空間、Survivor 空間,或者老年代空間。收集器能夠對扮演不同角色的 Region 採用不同的策略去處理
Region 中還有一類特殊的 Humongous 區域,專門用來存儲大對象。G1 認為只要大小超過了一個 Region 容量一半的對象即可判定為大對象。而對於那些超過了整個 Region 容量的超級大對象,將會被存放在多個連續的 Humongous Region 之中,G1 會把 Humongous Region 作為老年代的一部分來進行看待
G1 收集器將 Region 作為單次回收的最小單元,即每次收集到的記憶體空間都是 Region 大小的整數倍,這樣可以有計劃地避免在整個堆中進行全區域的垃圾收集。更具體的處理思路是讓 G1 收集器去跟蹤各個 Region 裡面的垃圾堆積的價值大小,價值即回收所獲得的空間大小以及回收所需時間的經驗值,然後在後臺維護一個優先順序列表,每次根據用戶設定允許的收集停頓時間(預設值是 200 毫秒),優先處理回收價值收益最大的那些 Region,這也就是 Garbage First 名字的由來
5.7.1 工作流程
- 初始標記(Initial Marking):標記一下 GC Roots 能直接關聯到的對象,需要停頓線程,但耗時很短
- 併發標記(Concurrent Marking):從 GC Roots 開始對堆中對象進行可達性分析,遞歸掃描整個堆里的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程式併發執行
- 最終標記(Final Marking):對用戶線程做另一個短暫的暫停,用於處理併發標記階段中產生的垃圾
- 篩選回收(Live Data Counting and Evacuation):負責更新 Region 的統計數據,對各個 Region 的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個 Region 構成回收集,然後把決定回收的那一部分 Region 的存活對象複製到空的 Region 中,再清理掉整個舊 Region 的全部空間。這裡的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程並行
完成的
5.8 垃圾收集器的選擇
- 串列:Serial、Serial Old
- 併發:ParNew、Parallel Scavenge、Parallel Old、CMS、G1
- 新生代:Serial、ParNew、Parallel Scavenge
- 老年代:Serial Old、Parallel Old、CMS
- G1 保留了分代的概念,但並不局限於收集某個分代
- 標記-清除:CMS
- 標記-複製:Serial、ParNew、Parallel Scavenge
- 標記-整理:Serial Old、Parallel Old
- G1 從整體來看是基於標記-整理演算法,但從局部(兩個 Region 之間)上看又是基於標記-複製演算法
- 側重響應時間:CMS、G1(在保證延遲可控的情況下獲得儘可能高的吞吐量)
- 側重吞吐量:Parallel Scavenge、Parallel Old
- 硬體配置低:Serial、Serial Old、ParNew
- 優先調整堆的大小讓伺服器自己來選擇
- 如果記憶體小於 100M,或者是單核 CPU,並且沒有停頓時間的要求,選擇串列收集器
- 如果優先考慮應用程式的峰值性能,選擇 Parallel 收集器
- 如果側重響應時間,選擇併發收集器。4G 以下可以用 parallel,4-8G 可以用 ParNew + CMS,8G 以上可以用 G1
5.8.1 CMS 與 G1
G1 是 JDK9 的預設收集器
CMS 採用複製-清除演算法,可能會產生的大量的記憶體空間碎片。而 G1 從整體來看是基於標記-整理演算法,但從局部(兩個 Region 之間)上看又是基於標記-複製演算法,這兩種演算法都意味著 G1 在運行期間不會產生記憶體空間碎片
G1 需要記憶集來記錄新生代和老年代之間的引用關係,這種數據結構在 G1 中需要占用大量的記憶體,可能達到整個堆記憶體容量的 20% 甚至更多。而且 G1 中維護記憶集的成本較高,帶來了更高的執行負載,影響效率。相比較而言 CMS 的記憶集就簡單得多記憶體占用也更少
CMS 在小記憶體應用上的表現要優於 G1,而大記憶體應用上 G1 更有優勢,大小記憶體的界限是 6 ~ 8 GB