《深入理解Java虛擬機》閱讀——垃圾回識訓制
- 前言
- why——為什么需要垃圾回收
- what——垃圾回收做些什么
- where——去哪里回收垃圾
- how——垃圾回收是怎么做的
- 垃圾是否要回收
- 參考計數法
- 可達性分析演算法
- 方法區判斷是否可回收
- 垃圾回收的方式
- 方法論
- 標記-清除演算法
- 復制演算法
- 標記-整理演算法
- 分代收集演算法
- 實作(HotSpot)
- Serial收集器
- ParNew收集器
- Parallel Scavenge收集器
- Serial Old收集器
- Parallel Old收集器
- CMS收集器
- G1收集器
前言
從小老師就告訴我們學習有3個w和1個h,分別是what做什么、where在哪里、why為什么和how怎么做,于是我們今天也從這四個角度出發,跟著《深入理解Java虛擬機》學習一下jvm的垃圾回識訓制,
why——為什么需要垃圾回收
對于我們Java程式員來說,一開始對于記憶體其實是很不敏感的(至少我是這樣的,因為所有的作業都交給jvm來完成了,我們可以通過一個變數來創建陣列,不需要通過malloc函式來動態分配記憶體;在我們使用完這個陣列之后,不需要通過free函式來手動釋放記憶體,所以為了讓我們變得對計算機不了解一些(不是 所以其實是為了讓我們編程更方便一點,jvm幫我們完成了動態分配記憶體和對記憶體進行垃圾回收,
那么出現了另一個why,為什么我們還要學習記憶體動態分配和垃圾回收呢?
答案很簡單:當需要排查各種記憶體溢位、記憶體泄漏問題時,當垃圾收集成為系統達到更高并發量的瓶頸時,我們就需要對這些“自動化”的技術實施必要的監控和調節 ——《深入理解Java虛擬機》
在上一篇關于Java運行時記憶體的博客中,我們在說到堆疊和堆時,發現堆疊和堆在申請不到足夠的所需的記憶體時就會拋出OutOfMemoryError的例外,這也說明jvm并不是萬能的,也會出錯,因為記憶體是物理設備的,不夠就是不夠,就像褲兜里的錢一樣,不夠就是不夠!雖然我們錢不夠的話我們可以走出店門不買了,但是記憶體不夠程式可就崩潰了,可見我們必須要了解底層的記憶體分配和垃圾回收(這里就先說說垃圾回收,
what——垃圾回收做些什么
有這樣三件事情需要我們思考:
- 哪些記憶體需要回收?
- 什么時候回收?
- 如何回收?
我們思考完之后就是要讓垃圾回收要做的事情了:判斷哪些記憶體區域需要回收、判斷什么時候進行回收、用什么樣的方式進行回收,
判斷哪些記憶體需要回收就是where了,判斷什么時候進行回收和用什么樣的方式回收就是how了,
where——去哪里回收垃圾
首先我們明確垃圾是什么,這里所說的垃圾可不是我們使用電腦時將一個檔案拖到回收站那個垃圾桶里的垃圾,這里的垃圾是指我們在運行Java程式時,new出了一大堆物件,而這堆物件在我們不需要使用的時候它就成了垃圾,因為它“占著茅坑不拉屎”,你都沒有用了還占著記憶體干啥對吧,所以我們就需要去回收它們來釋放掉這些記憶體,
那我們現在要追究去哪里回收記憶體,我們肯定要知道jvm占了哪些記憶體,在上篇博客中我們知道了jvm運行時有堆、虛擬機堆疊、本地方法堆疊、方法區、程式計數器這五個區域,虛擬機堆疊和本地方法堆疊以及程式計數器這三個部分都是在編譯期間可以知道,等于說這三個區域的分配和回收是可以確定的,當方法或者執行緒結束時,記憶體也就跟著回收了,所以我們不用管這三個區域,而堆和方法區不一樣,一個介面中的多個實作類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,所以我們只有在程式處于運行期間才能知道會創建出哪些物件,于是垃圾回收重點關注堆和方法區這兩個區域,
how——垃圾回收是怎么做的
這里的怎么做其實有兩層含義,一方面是人如何實作垃圾回收的,另一方面是垃圾回收是如何回收垃圾的,其實也就是前面說的判斷什么時候進行回收和用什么樣的方式進行回收了,
垃圾是否要回收
進行垃圾回收的第一步就是我們要知曉某物件是否還存活著,也就是它是否還有用,這里我們會學習到兩種方法進行物件死活的判斷,分別是參考計數法和可達性分析演算法,
參考計數法
參考計數法的概念是這樣的:給物件中添加一個參考計數器,每當有一個地方參考這個物件,計數器就加1,當參考失效時就給計數器減1,如果一個物件的計數器的值為0就代表這個物件沒有用了需要回收,
是不是很簡單,事實上這個演算法不僅實作起來簡單,效率還高,但是幾乎當前主流的Java虛擬機都沒用使用這個演算法來進行垃圾的判斷,這是為什么呢?我們思考到如果兩個物件互相參考,那豈不是哪怕沒有其他物件對它們進行參考,它們的計數器的值永遠都是1,永遠不會被回收,這樣就會產生記憶體泄漏了,
于是我們再瞅瞅另一個演算法是怎樣的,
可達性分析演算法
這個演算法的思路是這樣的:通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為參考鏈(Reference Chain),當一個物件到GC Roots沒有任何參考鏈相連時,則證明此物件是沒用了的,

看上圖,根據可達性分析演算法可知,以GC Roots為起始點向下搜索,搜索到的Object 1、Object 2、Object 3、Object 4這四個物件都是仍然存活著的,而Object 5、Object 6、Object 7這三個物件沒有被搜索到,所以它們是被判定可以被回收的,
那么什么物件可以被當作GC Roots呢?
在Java語言中,可作為GC Roots的物件包括下面幾種:
- 虛擬機堆疊(堆疊幀中的本地變數表)中參考的物件,
- 方法區中類靜態屬性參考的物件,
- 方法區中常量參考的物件,
- 本地方法堆疊中JNI(即一般說的Native方法)參考的物件,
被上述GC Roots物件直接參考或間接參考的物件就被認為是可達的,而不可達的物件就被認為是可以被回收的物件,
所以你們注意到沒有,無論是參考計數法還是可達性分析演算法,都與物件之間的參考有關,可見參考是個重點來的,具體內容請看之后的博客,這里只參考一段書中的內容,
在JDK 1.2以前,Java中的參考的定義很傳統:如果reference型別的資料中存盤的數值代表的是另外一塊記憶體的起始地址,就稱這塊記憶體代表著一個參考,這種定義很純粹,但是太過狹隘,一個物件在這種定義下只有被參考或者沒有被參考兩種狀態,對于如何描述一些“食之無味,棄之可惜”的物件就顯得無能為力,我們希望能描述這樣一類物件:當記憶體空間還足夠時,則能保留在記憶體之中;如果記憶體空間在進行垃圾收集后還是非常緊張,則可以拋棄這些物件,
在JDK 1.2之后,Java對參考的概念進行了擴充,將參考分為強參考(Strong Reference)、軟參考(Soft Reference)、弱參考(Weak Reference)、虛參考(Phantom Reference)4種,這4種參考強度依次逐漸減弱, ——《深入理解Java虛擬機》
另外,其實即使在可達性分析種被判定為可回收的物件也不是一定會被回收的,要真正宣布一個物件死亡,至少要經歷兩次標記程序:在物件經過了可達性分析后發現沒有與GC Roots相連接的參考鏈,那么這個物件會被第一次標記并且進行一次篩選,篩選是看這個物件有沒有必要進行finalize()方法,如果該物件沒有重寫finalize()方法或者finalize()方法已經被呼叫過一次了,就說明該物件沒有必要進行finalize()方法了,也就是說它死亡了,
而如果該物件被認為有必要進行finalize()方法,也就是說這個物件重寫了finalize()方法且還沒有呼叫過,那么如果在重寫的finalize()方法里進行了“自救”也就是重新被GC Roots直接或間接參考上,那么在第二次進行標記時這個物件就不會被認為要被回收了,具體程序參考書上的內容:
如果這個物件被判定為有必要執行
finalize()方法,那么這個物件將會放置在一個叫做F-Queue的佇列之中,并在稍后由一個由虛擬機自動建立的、低優先級的Finalizer執行緒去執行它,這里所謂的“執行”是指虛擬機會觸發這個方法,但并不承諾會等待它運行結束,這樣做的原因是,如果一個物件在finalize()方法中執行緩慢,或者發生了死回圈(更極端的情況),將很可能會導致F-Queue佇列中其他物件永久處于等待,甚至導致整個記憶體回收系統崩潰,finalize()方法是物件逃脫死亡命運的最后一次機會,稍后GC將對F-Queue中的物件進行第二次小規模的標記,如果物件要在finalize()中成功拯救自己——只要重新與參考鏈上的任何一個物件建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變數或者物件的成員變數,那在第二次標記時它將被移除出“即將回收”的集合;如果物件這時候還沒有逃脫,那基本上它就真的被回收了, ——《深入理解Java虛擬機》
注意:通過在finalize()方法里進行自救或者做些什么善后作業的方式并不推薦,可以使用try-finally的方式或者其他的方式來做會更好,《深入理解Java虛擬機》的作者建議大家忘記有finalize()這個方法,
方法區判斷是否可回收
參考計數法和可達性分析演算法是用來判斷new出來的物件是否可以回收的,物件都是在堆里的,前面也說了垃圾回收關注堆和方法區這兩塊區域,那么接下來我們說說方法區里是如何判斷是否有垃圾要回收的,
方法區在各個Java虛擬機中的實作不盡相同,但是主要回收的還是廢棄常量和無用的類,判斷廢棄常量跟判斷廢棄物件很像,如果沒有參考指向該常量,那么就說明該常量是廢棄常量,而判斷一個類是否是無用的類的條件就會嚴苛許多,類需要同時滿足下面三個條件才能夠算是一個無用的類:
- 該類所有的實體都已經被回收,也就是說堆里沒有任何該類的實體物件,
- 加載該類的ClassLoader已經被回收,
- 該類對應的Class物件沒有在任何地方被參考,無法在任何地方通過反射訪問該類的方法,
即使一個類滿足了上述三個條件,也不一定會被回收,虛擬機提供了一些引數對其進行控制,例如-verbose:class、-XX:+TraceClassLoading等等,
在大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢位,
到這里說完了如何判斷是否可回收,接下來就是說說回收的方式了,
垃圾回收的方式
方法論
標記-清除演算法
標記-清除演算法是最簡單基礎的演算法,后面的演算法都是基于它的,它的主要思路是先標記出所有需要回收的物件,在標記完候對所有被標記的物件進行統一回收,如下圖:

該演算法有兩個不足,分別是效率問題和會產生大量記憶體碎片,
標記和清除的效率都不高,記憶體碎片太多會導致以后在分配較大物件時因為找不到足夠的連續記憶體而不得不提前觸發另一次垃圾回收,
復制演算法
復制演算法的基本思路是:劃分出兩個相同的區域,每次只在其中一個區域里存放物件,在進行一次gc后將依然存活的物件復制到另一個區域,然后將要回收的物件一次性回收掉,如下圖:

很容易發現這種演算法的優缺點,這種演算法雖然避免了出現大量記憶體碎片而且只要移動堆頂指標來分配記憶體就可以,但是將原本的記憶體空間縮小了一半屬實有點虧,那既然縮小一半有點太虧了,那我們可以少縮小一些,
現在的商業虛擬機都采用這種收集演算法來回收新生代,IBM公司的專門研究表明,新生代中的物件98%是“朝生夕死”的,所以并不需要按照1:1的比例來劃分記憶體空間,而是將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor, ——《深入理解Java虛擬機》
所以實際上,我們在使用到復制演算法的時候,是在Eden區和一塊Survivor區中分配記憶體,然后在經過一次gc后將幸存物件復制到另一塊Survivor區中,再一次性回收掉要回收的無用物件,HotSpot虛擬機默認Eden區和Survivor區的大小比例為8:1,也就是說如果是100MB的記憶體,Eden區占80MB,兩塊Survivor分別占10MB,每次能使用的記憶體空間是90%,只縮小了10%的記憶體空間,這就比之前損失一半的記憶體空間劃算多了,
但是雖然研究表明每次可能會回收掉98%的物件,但是也可能會出現存活物件大于10%的情況對吧,所以在一塊Survivor區放不下存活物件的時候,就需要放在其他記憶體(老年代)中進行分配擔保,
標記-整理演算法
標記-整理演算法其實是針對老年代物件存活率高的特點對復制演算法進行的改造,它是這樣的:將依然存活的物件向一側移動,然后回收掉端邊界以外的無用物件,如下圖:

分代收集演算法
這個演算法沒有什么好說的,就是將前面三種演算法進行一個結合,直接看書中的內容,
當前商業虛擬機的垃圾收集都采用“分代收集”(Generational Collection)演算法,這種演算法并沒有什么新的思想,只是根據物件存活周期的不同將記憶體劃分為幾塊,一半是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集演算法,在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用復制演算法,只需要付出少量存活物件的復制成本就可以完成收集,而老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清除”或者“標記-整理”演算法來進行回收, ——《深入理解Java虛擬機》
實作(HotSpot)
注:這里有一部分是如何進入到垃圾回收的實作,因為我暫時還沒有搞懂,就先不寫了,等以后搞懂了再來補充,
首先我們要明確一個概念:垃圾回收器通常是組合起來使用的,也就是說有些垃圾回收器負責回收新生代,有些垃圾回收器負責回收老年代,也有的垃圾回收器都可以回收,我們先來看看JDK1.7的HotSpot虛擬機有哪些垃圾回收器(JDK1.8跟1.7是一樣的),

兩個垃圾回收器之間存在連線的表示它們可以搭配使用,在上面的區域的垃圾回收器表示它們負責新生代的垃圾回收,而在下面的區域的垃圾回收器表示它們負責老年代的垃圾回收,而在中間的表示既可以進行新生代的垃圾回收也可以進行老年代的垃圾回收,
接下來我們就分別學習一下這幾個垃圾回收器的特性、基本原理和使用場景,
雖然我們是在對各個收集器進行比較,但并非為了挑選出一個最好的收集器,因為直到現在為止還沒有最好的收集器出現,所以我們選擇的只是對具體應用最合適的收集器, ——《深入理解Java虛擬機》
Serial收集器
Serial收集器是最基本、發展歷史最悠久的收集器, ——《深入理解Java虛擬機》
在jdk1.3之前,Serial收集器是新生代垃圾回收的唯一選擇,由此可見它的歷史真的十分悠久了,看它的名字serial串行就可以見名知義了,它在執行垃圾回收的時候是單執行緒執行的嘛,但是這里的單執行緒不止是說它使用一個CPU或一條收集執行緒去進行垃圾回收,這里要重點強調它在執行的時候只有它能執行!也就是會產生“Stop The World”,別的作業執行緒都必須停下來等Serial收集器作業完了才能繼續作業,Serial收集器和Serial Old收集器合作運行的運行程序如下圖:

從圖中可以看出,Serial負責收集新生代的垃圾使用的是復制演算法,Serial Old負責收集老年代的垃圾使用的是標記-整理演算法,這兩個垃圾收集器在運行的時候都需要將用戶正在作業的行程暫停,所以這就是它們串行運行的缺點,因為如果用戶在使用計算機的時候,突然后臺就來了一下垃圾回收,就啥都暫停了,多難受是不,所以后面就出現了很多并行和并發的垃圾回收器,但是它也有它的優點:簡單且高效,
在用戶的桌面應用場景中,分配給虛擬機管理的記憶體一般來說不會很大,收集幾十兆甚至一兩百兆的新生代(僅僅是新生代使用的記憶體,桌面應用基本上不會再大了),停頓時間完全可以控制在幾十毫秒最多一百多毫秒以內,只要不是頻繁發生,這點停頓是可以接收的,所以,Serial收集器對于運行在Client模式下的虛擬機來說是一個很好的選擇, ——《深入理解Java虛擬機》
注:上面提到了并行和并發,,在垃圾回收器的背景關系語境中這兩個詞語與我們平時理解的會有些許的不同,這里解釋一下,并行(Parallel)是指多條垃圾收集執行緒并行作業,但此時用戶執行緒仍然處于等待狀態,并發(Concurrent)是指用戶執行緒與垃圾收集執行緒同時執行(但不一定是并行的,可能會交替執行),用戶程式在繼續運行,
ParNew收集器
ParNew其實是Serial收集器的多執行緒版本,也是上面說的并行垃圾收集器,它與Serial的差別只是Serial是單執行緒而ParNew是多執行緒,其余的比如jvm控制引數(如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold)、收集演算法(復制)、會發生STW、物件分配規則、回收策略等等都是一樣的,所以它和Serial Old垃圾回收器配合使用的運行程序是這樣的:

由此圖可看出ParNew垃圾收集器相對Serial收集器并沒有做出多大的創新,而且ParNew也不見得就一定比Serial的效率要高,在單CPU的場景下,Serial的效率肯定是要高于ParNew的,因為ParNew還有執行緒切換帶來的性能消耗,但是如果在多物理CPU或者多邏輯CPU(超執行緒)的情況下ParNew的優勢就體現出來了,ParNew默認開啟的收集執行緒數與CPU的數量相同,可以使用-XX:ParallelGCThreads引數來進行控制,
ParNew是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的原因是,除了Serial收集器外,目前
jdk7剛出來的時候只有它能與CMS收集器配合作業, ——《深入理解Java虛擬機》
CMS收集器是作業在老年代的垃圾回收器,是JDK1.5的時候推出的,它是第一款真正意義上的并發收集器,CMS之后再學習,另外新生代還有一個垃圾收集器也就是Parallel Scavenge收集器,它是JDK1.4推出的,因為它沒用使用傳統的GC收集器代碼框架,是另外獨立實作的,所以在老年代使用CMS收集器的時候,新生代只能選擇ParNew或者Serial收集器,ParNew收集器也是使用-XX:+UseConcMarkSweepGC選項后的默認新生代收集器,也可以使用-XX:+UseParNewGC選項來強制指定它,
Parallel Scavenge收集器
Parallel Scavenge收集器和ParNew收集器一樣是運行在新生代的、并行的、使用復制演算法的,但是它們之間有一個很大的不同,就是它們的目的或者說關注點不一樣,像ParNew或者CMS這樣的收集器主要關注的是垃圾回收導致用戶執行緒停頓的時間長短,而Parallel Scavenge收集器關注的是吞吐量,它的目標是達到一個可控制的吞吐量(Throughput),什么是吞吐量呢?直白一點,吞吐量 = 用戶執行緒運行時間 / (用戶執行緒運行時間 + 垃圾回識訓費的時間),也就是說如果jvm運行了100分組,而垃圾回識訓費了1分鐘,用戶執行緒運行了99分鐘,那么吞吐量就等于99%,由于與吞吐量關系密切,Parallel Scavenge收集器也經常被稱為“吞吐量優先”收集器,
停頓時間越短就越適合需要與用戶互動的程式,良好的回應速度能提升用戶體驗,而高吞吐量則可以高效率地利用CPU時間,盡快完成程式的運算任務,主要適合在后臺運算而不需要太多互動的任務, ——《深入理解Java虛擬機》
Parallel Scavenge收集器提供兩個引數用來精準的控制吞吐量,分別是-XX:MaxGCPauseMillis和-XX:GCTimeRatio,-XX:MaxGCPauseMillis引數見名知義能看出來這是用來控制垃圾回收停頓時間的,設定這個引數的時候要傳入一個大于0的值,收集器將盡可能的保證垃圾回收產生的停頓時間不超過該值,但是這個值不是越小越好的,這個值設定的越小,那么垃圾回收的次數必然會增多,-XX:GCTimeRatio引數也能看出來是設定垃圾回收時間比例的,事實上它是用來設定允許的垃圾回收的時間占總時間的最大比例,即1 - 吞吐量,但是它不是直接設定的,它需要傳入一個大于0小于100的整數,假設傳入的是9,那么允許的最大GC時間就占總時間的10%(1/(1 + 9) = 0.1),這個引數的默認值是99,也就是說默認允許最大的垃圾回收時間占總時間的1%,
除了上述兩個引數之外,Parallel Scavenge收集器還有一個引數:-XX:+UseAdaptiveSizePolicy,這個引數很厲害的,看到有+就知道這是個開關引數,當作這個引數被打開之后,就不需要我們去手動的設定新生代的大小、Eden區與Survivor區的比例以及晉升老年代物件年齡這些細節引數了,虛擬機能夠根據當前系統的運行情況收集性能監控資訊,去動態的進行調整以提供最合適的停頓時間或者最大的吞吐量,這種調節方式被稱為GC自適應的調節策略,所以對于我們這種菜鳥來說使用這個Parallel Scavenge收集器配合GC自適應再好不過了,把調優任務交給虛擬機,我們只要設定好最大堆和最大停頓時間或者吞吐量就好了,
Serial Old收集器
Serial Old收集器在前面就有提到,它是單執行緒的,使用的是標記-整理演算法,運行的時候會產生STW現象,而且我們在之前的垃圾回收器圖中可以看到,它可以和新生代的Serial收集器、ParNew收集器、Parallel Scavenge收集器配合使用,也可以和老年代的CMS收集器配合使用,按道理它是單執行緒的垃圾回收器,主要意義就是運行在Client模式下了,但是在Server模式下,它可以作為CMS收集器的后備預案,在并發收集發生Concurrent Mode的時候,也就是說新產生的垃圾大于回收的速度了,CMS無法承受壓力了就會啟動Serial Old來進行一次串行的垃圾回收,
另外在Server模式下,在JDK1.5及之前的版本中與Parallel Scavenge收集器搭配使用,
注:需要說明一下,Parallel Scavenge收集器架構中本身有PS MarkSweep收集器來進行老年代收集,并非直接使用了Serial Old收集器,但是這個PS MarkSweep收集器與Serial Old的實作非常接近,所以在官方的許多資料中都是直接以Serial Old代替PS MarkSweep進行講解, ——《深入理解Java虛擬機》
Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用的標記-整理演算法,在JDK1.6的時候開始提供,我個人認為Parallel Old收集器是來拯救Parallel Scavenge的,不然Parallel Scavenge在老年代垃圾回收這一塊可太尷尬了,不能和CMS搭配使用,Serial Old收集器又太慢了,白瞎了Parallel Scavenge在新生代的高吞吐量了,所以在Parallel Old收集器出現了才讓“吞吐量優先”收集器有了比較名副其實的應用組合,Parallel Old和Parallel Scavenge搭配使用的運行程序如下圖:

CMS收集器
CMS全稱Concurrent Mark Sweep,即并發標記清除,所以從名字中我們能獲取到兩個資訊,CMS是并發執行的,這個之前也說過,還有就是它使用的標記-清除演算法,前面也提到過CMS追求的是最短回收停頓時間,所以在互聯網站或者B/S系統的服務端的場景中,CMS就很符合這類應用的需求,
CMS的作業程序分四個階段:
- 初始標記(CMS initial mark):標記一下GC Roots能直接關聯到的物件,單執行緒會產生STW,但速度很快,
- 并發標記(CMS concurrent mark):進行GC Roots Tracing,trace是追溯的意思,就是說完善GC Roots參考鏈,并發執行的不會產生STW,需要比較長的時間,
- 重新標記(CMS remark):修正并發標記的時候因為用戶程式在繼續運行而產生的變動,多執行緒會產生STW,時間比初始標記長但遠小于并發標記,
- 并發清除(CMS mark sweep):并發地清除被標記的物件,
因為并發標記和并發清除的時間遠大于初始標記和重新標記的時間,而且初始標記和重新標記的時間非常短,所以總體上來說CMS收集器回收程序是和用戶執行緒一起并發執行的,根據上面四個步驟我們也很容易能夠畫出CMS的運行流程圖:

CMS的優缺點也十分明顯,優點就是它是并發收集的,停頓時間非常短,缺點就要分三個點細細說了:
- 面向并發設計的程式都對CPU資源非常敏感,CMS也不例外,在并發階段,CMS雖然不會導致用戶執行緒停頓,但是也會因為占用了一部分CPU資源而導致用戶執行緒變慢,CMS默認啟動的回收執行緒數是 (CPU數量 + 3) / 4,所以說當CPU在4個以上時比如5個,CMS會占用 2 / 5 = 40%的CPU資源,并且會隨著CPU個數的增加而降低占用資源比例,但是如果CPU少于4個,比如2個,那么就會占用 1 / 2 = 50%也就是一半的CPU資源,會嚴重影響用戶執行緒的執行速度,
- CMS收集無法處理浮動垃圾,這個在上面聊Serial Old收集器的時候就說到了,并且說到了jvm會將CMS作為CMS的后備預案,但是注意是在出現了“Concurrent Mode Failure”之后才會啟動Serial Old收集器的,那么怎樣會出現這樣的失敗呢?其實是因為CMS是并發執行的嘛,在并發的程序中會放入新的物件到老年代,所以CMS必須要預留一部分記憶體來存放可能會新增的物件,在JDK1.5的默認設定中,CMS收集器當老年代使用了68%的空間后就會被激活,也可以使用-XX:CMSInitiatingOccupancyFraction來控制,很明顯,如果這個百分比越高的話,CMS被觸發的次數就越少,在JDK1.6中,CMS收集器的啟動閾值已經提升到92%,但是預留記憶體越少就越容易出現“Concurrent Mode Failure”,所以我覺得這個預留記憶體就需要我們自己去根據實際場景進行衡量了,
- 標記-清除演算法會產生大量的記憶體碎片,針對這個缺點,CMS提供了一個-XX:+UseCMSCompactAtFullCollection開關引數,是說在CMS撐不住要進行Full GC的時候進行一次記憶體整理,但是這個整理程序是沒有辦法并發進行的,所以會產生停頓時間,另外這個引數默認是開啟的,然后虛擬機設計者還提供了一個引數-XX:CMSFullGCsBeforeCompaction,這個引數是設定CMS在經歷過多少次不整理記憶體的Full GC后來一次整理記憶體的Full GC,默認是0,表示每次進入Full GC都進行記憶體整理,
然后這里補充一下CMS第一個缺點的一個過時的解決方案,當時虛擬機提供了一種名叫增量式并發收集器(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器變種,它就是在并發標記和并發清理的時候讓GC執行緒、用戶執行緒交替運行,盡量減少GC執行緒的獨占資源時間,這樣整個垃圾收集的程序就會更長,但對用戶程式的影響就會顯得少一些,實踐證明,這個i-CMS的效果很一般,所以在JDK1.7的時候它就被宣告為“deprecated”過時的了,
G1收集器
G1(Garbage-First)收集器是當今收集器
當時jdk1.7是最新的jdk技術發展的最前沿成果之一,早在JDK1.7剛剛確立專案目標,Sun公司給出的JDK1.7RoadMap里面,它就被視為JDK1.7中HotSpot虛擬機的一個重要進化特征,從JDK 6u14中開始就有Early Access版本的G1收集器供開發人員實驗、試用,由此開始G1收集器的“Experimental(試驗性的)”狀態持續了數年時間,直至JDK 7u4,Sun公司才認為它達到足夠成熟的商用程度,移除了“Experimental”的標識, ——《深入理解Java虛擬機》
而在JDK1.9的時候,jvm就將G1收集器作為默認的收集器了,
由于G1的內容比較多,而且實作與前面學習的收集器有很大區別,所以我就放在后續博客中詳細聊了,
如有錯誤,歡迎指正!!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/262037.html
標籤:其他
上一篇:計算機網路篇 | 分層的協議架構
