在調優Java應用程序時,重點一般放在兩個主要目標上:響應性 或 吞吐量。html
響應性Responsiveness
是指應用程序對請求的數據作出響應的速度:java
吞吐量Throughput
專一於最大程度地提升應用程序在特定時間段內的工做量:算法
較長的暫停時間Pause Time
對於注重響應性的應用程序是不可接受的,但對於注重吞吐量的應用程序來講能夠接受的。前者重點是在短期內作出響應,後者則側重與長時間運行的處理效率。數據庫
可達性分析是 Java GC 算法的基礎,基本思路就是以一系列名爲 GC Roots
對象做爲起始點,經過引用關係遍歷對象圖,若是一個對象到 GC Roots
間沒有任何可達路徑相連時,則說明此對象能夠被回收。segmentfault
能夠做爲 GC Roots
的對象:緩存
可達性分析中重要的一環就是遍歷整個堆,並標記其中的存活對象。一種經常使用的標記算法是 三色標記法tri-color marking
:服務器
每一個對象可能爲如下 3 種顏色之一:數據結構
標記算法從 GC Roots 出發遍歷堆,可達對象先標記 gray,而後再標記 爲 black。併發
遍歷完成以後全部可達對象都是 black 的,此時全部標記爲 white 的對象都是能夠回收的。oracle
當實現併發標記算法時,必須防止 white 對象被漏標,不然可能致使不應回收的對象被回收。
傳統垃圾收集器將堆分紅三個部分:年輕代YoungGen = Eden + Survivor
,老年代OldGen
和永久代PermGen
,每一個區域內存連續且大小固定。
將堆內存進行劃分後,能夠按照對象生命週期長短,在不一樣區域使用不一樣的回收算法,提升 GC 的效率。
標記-清除
用一個空閒列表free-list
記錄失效對象佔用的內存區域,方便後續從新分配給新對象。
標記-整理
將全部存活對象移動到內存區域的開頭,剩餘的連續內存區域都是可用的空閒空間。
標記-複製
將內存劃分爲活動區間與空閒區間,前者用於動態分配對象,後者用於容納 GC 存活對象。
GC 時只需將存活對象從前者複製到後者,而後交換二者的角色便可。
CMS Concurrent Mark-Sweep
是一個採用 標記-清除 算法的老年代收集器。
它經過與應用程序線程併發執行大多數垃圾回收工做,來最大程度地減小因爲 GC 致使的暫停。
一般狀況下,CMS 收集器不會複製或壓縮活動對象,這意味着無需移動活動對象便可完成垃圾回收。
然而過多的內存碎片可能形成分配失敗,最終致使 FullGC。能夠經過分配更大的堆來規避這一問題。
CMS 對老年代的回收能夠分爲如下幾個步驟:
Initial Mark (STW) 初始標記
Concurrent Mark 併發標記
GC 線程遍歷 Initial Mark 階段標記出來存活的老年代對象,而後遞歸標記這些可達的對象。
該階段與應用線程併發運行,期間會發生新生代對象晉升、老年代對象引用關係更新,須要對這些對象進行從新標記,避免發生遺漏。
CMS 用一個card-table
管理老年代,併發標記過程當中,某個對象的引用關係發生了變化,則將對象所在的內存塊標記爲 Dirty Card。
CMS 使用增量更新incremental update
解決併發修改致使的漏標問題:把 black 對象從新標記爲 grey,下次從新掃描其引用。
Preclean 預清理
這一階段主要是處理 Concurrent Mark 階段中引用關係改變,致使沒有標記到的存活對象的。經過併發地從新掃描這些對象,預清理階段能夠減小 Remark 階段的 STW。
這個階段會處理前一個階段被標記爲 Dirty Card 的部分,將其中變化了的對象做爲 GC Root 再進行掃描並從新標記。
Abortable Preclean 可終止的預清理
這個階段做用與 Preclean 相似,但能夠經過設置 掃描時長(默認5秒)或 Eden 區使用佔比(默認50%)控制本階段的結束時機。
增長這一階段的緣由,是期待這期間能發生一次 YoungGC 清理無效的年輕代對象,減小 Remark 階段掃描年輕代的時間。
Remark (STW) 從新標記
:
這個階段同時掃描 YoungGen 與 OldGen,從新標記整個老年代中全部存活對象。
因爲以前的 Concurrent Mark 與 Preclean 階段是與用戶線程併發執行的,年輕代對老年代的引用可能已經發生了改變,Remark 要花不少時間處理這些改變,會致使長時間的 STW。
此外,即便新生代的對象已經不可達了,CMS 也會使用這些不可達的對象當作的 GC Roots 來掃描老年代,致使部分失效的老年代對象沒法被及時回收。
能夠加入參數 -XX:+CMSScavengeBeforeRemark,在從新標記以前,先執行一次 YoungGC,回收掉年輕代的對象無用的對象。這樣進行年輕代掃描時,只須要掃描 Survivor 區的對象便可,通常 Survivor 區很是小,這大大減小了掃描時間。
Concurrent Sweep 併發清理
至此,老年代全部存活的對象已經被標記完成。這個階段主要是清除那些沒有標記的對象而且回收空間。
被回收的空間會被添加到 空閒列表中,以供之後分配。這一過程可能會對空閒空間進行合併,可是不會移動存活對象。
因爲該階段是與應用線程併發運行的,天然就還會有新的垃圾不斷產生,這一部分垃圾出如今標記過程以後,沒法在當次收集中處理掉它們。只好留待下一次GC時再清理掉。這一部分垃圾就稱爲 浮動垃圾。
Resetting 重置
清除數據結構,並重置定時器,爲下一輪 GC 作準備。
G1 Garbage-First
是一種服務器端的垃圾收集器:
G1 可以在大內存的多處理器計算機上,保證 GC 暫停時間可控,並實現高吞吐量。
其最終目的是取代 CMS 成爲服務端 GC 更好的解決方案:
incremental collecting
算法,其 GC 暫停時間比 CMS 更具可預測性,並容許用戶指按期望的暫停時間。G1 將堆劃分爲一組大小相等的且連續的堆區域Region
:
G1 中新生代與老年代再也不連續,每一個區域能夠在 Eden、Survivor 與 Old 之間切換角色。此外,還有一類被稱爲 Humongous 的巨型區域,用於容納體積 ≥ 標準區域大小的50%的對象。JVM 一般會將內存劃分爲 2000個區域,每一個大小從 1 到 32Mb 不等,由 JVM 在啓動時經過 -XX:G1HeapRegionSize 指定。
每一個區域會被進一步細分紅多個卡片Card
,每一個大小爲 512Kb,用於實現細粒度的引用統計。
分區設計能夠避免一次收集整個堆,每次 GC 只收集區域的一個子集 CSetcollection set
,其中必然包含全部 Young 區域,同時可能包括部分 Old 區域:
根據回收區域的不一樣,能夠將 GC 分爲:
G1 根據存活對象的字節數統計每一個區域的 活躍度liveness
,而後根據指望停頓時間來肯定該 CSet 的大小,並保證那些垃圾多(活躍度低)的區域會被優先回收,故此得名 垃圾優先。
G1 的執行過程能夠表示爲由 3 個階段組成的循環:
堆中一開始只有 YoungGen,所以只會觸發 YoungGC,將 Eden 與 Survivor 區域中的活動對象複製到另外一個空閒的 Survivor 區域。
G1 中將 將存活對象複製到其餘區域 的過程稱爲 疏散Evacuation
。爲了減小停頓時間,疏散工做由多個 GC 線程並行完成。
YoungGC 過程當中會根據預期目標停頓時間 -XX:MaxGCPauseMillis 動態調整新生代的大小,經過 -XX:G1NewSizePercent 參數能夠人爲干預這一過程,但會讓預期停頓時間參數失效。
當堆的總體佔用空間足夠大時(超過45%),就會進入 Concurrent Marking 階段。經過 -XX:InitiatingHeapOccupancyPercent 選項能夠配置這一行爲。
與 CMS 相似,G1 中的併發標記包括多個階段,其中一些階段是併發的,另外一些階段則會 STW。
Initial Mark (STW) 初始標記
掃描並標記 GC Root 對象直接可達的老年代存活對象。
Initial Mark 並無獨立的執行階段,而是嵌入 YoungGC 中執行的,其停頓時間會被分攤,所以實際的開銷很是低。
Root Region Scan 掃描根區域
掃描 Root Region 並標記全部可達的老年代存活對象。
此處的 Root Region 就是先前 YoungGC 中生成的 Survivor 區域,其包含的對象都會被視爲 GC Root。
爲了不移動對象對標記產生影響,該過程必須在下次 YongGC 啓動前完成。
Concurrent Mark 併發標記
啓動併發標記線程,掃描並標記整個堆中的存活對象(線程數能夠經過 -XX:ConcGCThread 進行配置)。
爲了不重複標記,G1 使用 SATBsnapshot-at-the-beginning
算法解決漏標問題:
該約束是經過預寫屏障pre-write barrier
實現:
log buffers
中,而後交由併發標記線程處理。
爲了不移動對象對標記產生影響,該過程必須在下次 YoungGC 啓動前完成。全部的標記任務必須在堆滿前完成,若是堆滿前沒有完成標記任務,則會觸發擔保機制,經歷一次長時間的串行 FullGC。
Remark (STW) 從新標記
啓動並行標記線程,完成對整個堆中存活對象的標記(線程數能夠經過 -XX:ParallelGCThread 進行配置)。
該階段會暫停全部應用線程,避免發生引用更新,並完成對SATB 日誌緩衝區中剩餘對象的標記,找出全部未被訪問的存活對象。
該階段還執行一些額外的清理操做,例如:
Cleanup 清理垃圾
整理統計信息並識別出高收益的老年代分區,爲 MixedGC 作準備。
主要工做有:
此外還會執行一些清理工做,爲下一次 Concurrent Marking 作好準備。
MixedGC 主要流程與 YoungGC 相似,不一樣的地方在於 CSet 中包含了 Old 區域。
須要注意的是,Concurrent Marking 結束後,並不必定會當即觸發 MixedGC,中間可能會穿插屢次的 YoungGC。
當收集某個區域時,咱們必須知道是否有來自非收集區域引用,來肯定它們的活動性:
但查找整個堆很是耗時,同時也失去了增量收集的優點。爲了解決這一問題,G1 爲每一個區域維護了一個 RSetremembered set
,用於記憶從其餘區域指向本身的引用。
在執行收集時,RSet 中引用信息會扮演局部 GC Roots 的角色,避免耗時的引用查找,保證每一個區域的 GC 可以獨立進行:
注意,象若是 Old 區域中對在 Concurrent Marking 階段被肯定爲垃圾,即便有外部引用,該對象也會被做爲垃圾回收。
接下來發生的事情與其餘收集器所作的相同:多個並行GC線程找出哪些對象是活動的,哪些對象是垃圾:
最後,釋放空閒區域,將活動對象移到 Survivor 區域,並在必要時建立新對象:
爲了維護 RSet,在應用線程對字段執行寫操做時,會觸發寫後屏障post-write barrier
:
爲了減小寫屏障帶來的開銷,該過程是異步的:
Dirty Card Queue
,而後由 Refine 線程將其拾取並將信息傳播到被引用區域的 RSet。
若是應用線程插入速度過快,會致使 Refine 線程來不及處理,那麼應用線程將接管 RSet 更新的任務,從而致使性能降低。
併發標記 與 增量收集 是 G1 實現高性能與可預測回收的關鍵。
對於 CPU 資源充足且對延遲敏感的服務端應用來講,G1 算法可以在大堆上提供良好的響應速度。
做爲代價,額外的寫屏障與更活躍GC線程,會對應用的吞吐量產生負面影響。