Java虛擬機(三) 一、JVM 中常見的垃圾回收器

Android知識總結

一、JVM 中常見的垃圾回收器

在新生代中,每次垃圾回收時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成回收。而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清理或者標記—整理算法來進行回收。

1)、單線程垃圾回收器 - Serial/Serial Old

JVM 剛誕生就只有這種,最古老的,單線程,獨佔式,成熟,適合單 CPU,一般用在客戶端模式下。

這種垃圾回收器只適合幾十兆到一兩百兆的堆空間進行垃圾回收(可以控制停頓時間再 100ms 左右),但是對於超過這個大小的內存回收速度很慢,所以對於現在來說這個垃圾回收器已經是一個雞肋。

  • 參數設置
    -XX:+UseSerialGC 新生代和老年代都用串行收集器

2)、多線程並行垃圾回收器 - Stop The World(STW)

單線程進行垃圾回收時,必須暫停所有的工作線程,直到它回收結束。這個暫停稱之爲“Stop The World”,但是這種 STW 帶來了惡劣的用戶體驗,例如:
應用每運行一個小時就需要暫停響應 5 分。這個也是早期 JVM 和 java 被 C/C++語言詬病性能差的一個重要原因。所以 JVM 開發團隊一直努力消除或降低 STW 的時間。

  • Parallel Scavenge (ParallerGC )/Parallel Old(重點)

爲了提高回收效率,從 JDK1.3 開始,JVM 使用了多線程的垃圾回收機制,關注吞吐量的垃圾收集器,高吞吐量則可以高效率地利用 CPU 時間,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務。
所謂吞吐量就是 CPU 用於運行用戶代碼的時間與 CPU 總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了 100 分鐘,其中垃圾收集花掉 1 分鐘,那吞吐量就是 99%
該垃圾回收器適合回收堆空間上百兆~幾個 G。

  • 參數設置
    開啓參數
    JDK1.8 默認就是以下組合 默認就是以下組合
    -XX:+UseParallelGC 新生代使用 Parallel Scavenge ,老年代使用 Parallel Old。
    收集器提供了兩個參數用於精確控制吞吐量,分別控制的停頓時間的-XX:MaxGCPauseMillis 參數以及直接設置吞吐量大小的-XX:GCTimeRatio 參數https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
  • -XX:MaxGCPauseMillis
    不過大家不要異想天開地認爲如果把這個參數的值設置得更小一點就能使得系統的垃圾收集速度變得更快,垃圾收集停頓時間縮短是以犧牲吞吐量和新生代空間爲代價換取的:系統把新生代調得小一些,收集 300MB 新生代肯定比收集 500MB 快,但這也直接導致垃圾收集發生得更頻繁,原來 10秒收集一次、每次停頓 100 毫秒,現在變成 5 秒收集一次、 每次停頓 70 毫秒。停頓時間的確在下降,但吞吐量也降下來了。
  • -XX:GCTimeRatio
    -XX:GCTimeRatio 參數的值則應當是一個大於 0 小於 100 的整數,也就是垃圾收集時間佔總時間的比率,相當於吞吐量的倒數。
    例如:把此參數設置爲 19, 那允許的最大垃圾收集時佔用總時間的 5% (即 1/(1+19)), 默認值爲 99,即允許最大 1% (即 1/(1+99))的垃圾收集時間由於與吞吐量關係密切,ParallelScavenge 是“吞吐量優先垃圾回收器”。
  • -XX:+UseAdaptiveSizePolicy
    -XX:+UseAdaptiveSizePolicy (默認開啓)。這是一個開關參數, 當這個參數被激活之後,就不需要人工指定新生代的大小(-Xmn)、Eden 與 Survivor區的比例(-XX:SurvivorRatio)、 晉升老年代對象大小(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。
  • 動態機制
    JVM 的參數中 -Xms 和 -Xmx 設置的不一致,在初始化時只會初始 -Xms 大小的空間存儲信息,每當空間不夠用時再向操作系統申請,這樣的話必然要進
    行一次 GC。
    另外,如果空間剩餘很多時也會進行縮容操作,JVM 通過 -XX:MinHeapFreeRatio 和 -XX:MaxHeapFreeRatio 來控制擴容和縮容的比例,調節這兩個值也可以控制伸縮的時機。
    所以動態擴容會引發 GC,同時縮容的話 JVM 也要處理。
    在 高 並 發 應 用 中 , 盡 量 將 成 對 出 現 的 空 間 大 小 配 置 參 數 設 置 成 固 定 的 , 如 -Xms 和 -Xmx , -XX:MaxNewSize 和 -XX:NewSize ,-XX:MetaSpaceSize 和 -XX:MaxMetaSpaceSize 等,保證 Java 虛擬機的堆是穩定的,確保 -Xms 和 -Xmx 設置的是一個值(即初始值和最大值一致),獲得一個穩定的堆。

3)、ParNew

多線程垃圾回收器,與 CMS 進行配合,對於 CMS(CMS 只回收老年代),新生代垃圾回收器只有 Serial 與 ParNew 可以選。和 Serial 基本沒區別,唯一的區別:多線程,多 CPU 的,停頓時間比 Serial 少。(在 JDK9 以後,把 ParNew 合併到了 CMS 了)大致瞭解下搭配關係即可,後續版本已經接近淘汰。

4)、併發垃圾垃圾回收器 - CMS(Concurrent Mark Sweep )

收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的 Java 應用集中在互聯網站或者 B/S 系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS 收集器就非常符合這類應用的需求。
從名字(包含“Mark Sweep”)上就可以看出,CMS 收集器是基於標記—清除算法實現的,它的運作過程相對於前面幾種收集器來說更復雜一些,

整個過程分爲 4 個步驟,包括:

  • 初始標記
    短暫,僅僅只是標記一下 GC Roots 能直接關聯到的對象,速度很快。

  • 併發標記
    和用戶的應用程序同時進行,進行 GC Roots 追蹤的過程,標記從 GCRoots 開始關聯的所有對象開始遍歷整個可達分析路徑的對象。這個時間比較長,所以採用併發處理(垃圾回收器線程和用戶線程同時工作。

  • 重新標記
    短暫,爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。

  • 併發清除
    由於整個過程中耗時最長的併發標記和併發清除過程收集器線程都可以與用戶線程一起工作,所以,從總體上來說,CMS 收集器的內存回收過程是與用戶線程一起併發執行的。

  • 參數
    -XX:+UseConcMarkSweepGC ,表示新生代使用 ParNew,老年代的用 CMS

  • 預清理與併發可中斷預清理
    這兩個處理都是併發的,所以如果是比較泛的講的話,都可以說成併發標記階段,如果是要抓細節,那麼併發標記階段後續還有這兩個處理。
    因爲 CMS 的終極目標是降低垃圾回收時的暫停時間,所以在該階段要盡最大的努力去處理,如果能夠在併發階段處理被應用線程更新的老年代對象,這樣在暫停的重新標記階段就可以少處理一些,暫停時間也會相應的降低。
  • 預清理
    主要做兩件事情:
  • 1、在併發階段(併發階段是不暫停的),在 Eden 區中分配了一個 A 對象,A 對象引用了一個老年代對象 B(這個 B 之前沒有被標記),在這個階段就會標記對象 B 爲活躍對象。


  • 2、在併發標記階段,如果老年代中有對象內部引用發生變化,會把所在的 Card 標記爲 Dirty(其實這裏並非使用CardTable,而是一個類似的數據結構,叫 ModUnionTalble)通過掃描這些 Table,重新標記那些在併發標記階段引用被更新的對象。


  • 併發可中斷預清理
    該階段發生的前提是,新生代 Eden 區的內存使用量大於參數,CMSScheduleRemarkEdenSizeThreshold,默認是 2M,如果新生代的對象太少,就沒有必要執行該階段,直接執行重新標記階段
    在該階段,主要循環的做兩件事:
  • 1、處理 From 和 To 區的對象,標記可達的老年代對象,類似於預處理。


  • 2、預清理的第二個階段。



    這個邏輯不會一直循環下去,打斷這個循環的條件有三個(滿足一個即可):

  • 可以設置最多循環的次數CMSMaxAbortablePrecleanLoops,默認是 0,意思沒有循環次數的限制。
  • 如果執行這個邏輯的時間達到了閾值CMSMaxAbortablePrecleanTime,默認是 5s,會退出循環。
  • 如果新生代 Eden 區的內存使用率達到了閾值CMSScheduleRemarkEdenPenetration,默認 50%,會退出循環。
  • CMS 中的問題
  • 1)、 CPU敏感
    CMS 對處理器資源敏感,畢竟採用了併發的收集、當處理核心數不足 4 個時,CMS 對用戶的影響較大。
  • 2)、浮動垃圾
    由於 CMS 併發清理階段用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS 無法在當次收集中處理掉它們,只好留待下一次 GC 時再清理掉。這一部分垃圾就稱爲“浮動垃圾”。
    由於浮動垃圾的存在,因此需要預留出一部分內存,意味着 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。
    在 1.8 的版本中老年代空間使用率閾值(92%) 備註:一個複雜的公式。不用管。
    當然 CMS 還有參數可以控制觸發回收的條件(堆空間達到多少比例觸發):CMSInitiatingOccupancyFraction
    CMSInitiatingOccupancyFraction的值,如果你沒設置過就是虛擬機自己的默認值,默認-1,-1 就是按照 92%來算。
    如果手動設置-XX:CMSInitiatingOccupancyFraction=70,那麼就是按照手動的設置來算。
  • 3)、 內存碎片
    標記 - 清除算法會導致產生不連續的空間碎片
    碎片帶來了兩個問題:
    1、 空間分配效率較低:如果是連續的空間 JVM 可以通過使用指針碰撞的方式來分配,而對於這種有大量碎片的空閒鏈表則需要逐個訪問空閒列表中的項來訪問,查找可以存放新建對象的地址。
    2、 空間利用效率變低:新生代晉升的對象大小大於了連續空間的大小,即使整個 Old 區的容量是足夠的,但由於其不連續,也無法存放新對象。就是內存碎片導致的 Promotion Failed,Young GC 以爲 Old 有足夠的空間,結果到分配時,晉級的大對象找不到連續的空間存放。
  • CMS 總結

CMS 問題比較多,所以現在沒有一個版本默認是 CMS,只能手工指定。但是它畢竟是第一個併發垃圾回收器,對於瞭解併發垃圾回收具有一定意義,所以我們必須瞭解。
爲什麼 CMS 採用標記-清除,在實現併發的垃圾回收時,如果採用標記整理算法,那麼還涉及到對象的移動(對象的移動必定涉及到引用的變化,這個需要暫停業務線程來處理棧信息,這樣使得併發收集的暫停時間更長),所以使用簡單的標記-清除算法纔可以降低 CMS 的 STW 的時間。
該垃圾回收器適合回收堆空間幾個 G~ 20G 左右。

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