本文介紹 GC 基礎原理和理論,GC 調優方法思路和方法,基於 Hotspot jdk1.8,學習以後你將瞭解如何對生產系統出現的 GC 問題進行排查解決。git
本文的內容主要以下:github
大多數狀況下對 Java 程序進行 GC 調優,主要關注兩個目標:算法
響應速度(Responsiveness):響應速度指程序或系統對一個請求的響應有多迅速編程
好比,用戶訂單查詢響應時間,對響應速度要求很高的系統,較大的停頓時間是不可接受的。調優的重點是在短的時間內快速響應。後端
吞吐量(Throughput):吞吐量關注在一個特定時間段內應用系統的最大工做量緩存
例如每小時批處理系統能完成的任務數量,在吞吐量方面優化的系統,較長的 GC 停頓時間也是能夠接受的,由於高吞吐量應用更關心的是如何儘量快地完成整個任務,不考慮快速響應用戶請求安全
在 GC 調優中,GC 致使的應用暫停時間影響系統響應速度,GC 處理線程的 CPU 使用率影響系統吞吐量。bash
現代的垃圾收集器基本都是採用分代收集算法,其主要思想: 將 Java 的堆內存邏輯上分紅兩塊:新生代、老年代,針對不一樣存活週期、不一樣大小的對象採起不一樣的垃圾回收策略。服務器
新生代又叫年輕代,大多數對象在新生代中被建立,不少對象的生命週期很短。每次新生代的垃圾回收(又稱 Young GC、Minor GC、YGC)後只有少許對象存活,因此使用複製算法,只需少許的複製操做成本就能夠完成回收。多線程
**新生代內又分三個區:**一個 Eden 區,兩個 Survivor 區(S0、S1,又稱From Survivor、To Survivor),大部分對象在 Eden 區中生成。
當 Eden 區滿時,還存活的對象將被複制到兩個 Survivor 區(中的一個);當這個 Survivor 區滿時,此區的存活且不知足晉升到老年代條件的對象將被複制到另一個 Survivor 區。對象每經歷一次複製,年齡加 1,達到晉升年齡閾值後,轉移到老年代。
在新生代中經歷了 N 次垃圾回收後仍然存活的對象,就會被放到老年代,該區域中對象存活率高。老年代的垃圾回收一般使用「標記-整理」算法。
根據垃圾收集回收的區域不一樣,垃圾收集主要分爲:
新生代內存的垃圾收集事件稱爲 Young GC(又稱 Minor GC),當 JVM 沒法爲新對象分配在新生代內存空間時總會觸發 Young GC。好比 Eden 區佔滿時,新對象分配頻率越高,Young GC 的頻率就越高。
Young GC 每次都會引發全線停頓(Stop-The-World),暫停全部的應用線程,停頓時間相對老年代 GC 形成的停頓,幾乎能夠忽略不計。
Old GC:只清理老年代空間的 GC 事件,只有 CMS 的併發收集是這個模式。
Full GC:清理整個堆的 GC 事件,包括新生代、老年代、元空間等 。
Mixed GC:清理整個新生代以及部分老年代的 GC,只有 G1 有這個模式。
GC 日誌是一個很重要的工具,它準確記錄了每一次的 GC 的執行時間和執行結果,經過分析 GC 日誌能夠調優堆設置和 GC 設置,或者改進應用程序的對象分配模式。
開啓的 JVM 啓動參數以下:
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps
複製代碼
常見的 Young GC、Full GC 日誌含義以下:
免費的 GC 日誌圖形分析工具推薦下面 2 個:
Java 提供的自動內存管理,能夠歸結爲解決了對象的內存分配和回收的問題。前面已經介紹了內存回收,下面介紹幾條最廣泛的內存分配策略:
大多數狀況下,對象在先新生代 Eden 區中分配。當 Eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Young GC。
JVM 提供了一個對象大小閾值參數(-XX:PretenureSizeThreshold,默認值爲 0,表明無論多大都是先在 Eden 中分配內存)。
大於參數設置的閾值值的對象直接在老年代分配,這樣能夠避免對象在 Eden 及兩個 Survivor 直接發生大內存複製。
對象每經歷一次垃圾回收,且沒被回收掉,它的年齡就增長 1,大於年齡閾值參數(-XX:MaxTenuringThreshold,默認 15)的對象,將晉升到老年代中。
當進行 Young GC 以前,JVM 須要預估:老年代是否可以容納 Young GC 後新生代晉升到老年代的存活對象,以肯定是否須要提早觸發 GC 回收老年代空間,基於空間分配擔保策略來計算。
Young GC 以後若是成功(Young GC 後晉升對象能放入老年代),則表明擔保成功,不用再進行 Full GC,提升性能。
若是失敗,則會出現「promotion failed」錯誤,表明擔保失敗,須要進行 Full GC。
新生代對象的年齡可能沒達到閾值(MaxTenuringThreshold 參數指定)就晉升老年代。
若是 Young GC 以後,新生代存活對象達到相同年齡全部對象大小的總和大於任意 Survivor 空間(S0+S1空間)的一半,此時 S0 或者 S1 區即將容納不了存活的新生代對象。
年齡大於或等於該年齡的對象就能夠直接進入老年代,無須等到 MaxTenuringThreshold 中要求的年齡。
另外,若是 Young GC 後 S0 或 S1 區不足以容納:未達到晉升老年代條件的新生代存活對象,會致使這些存活對象直接進入老年代,須要儘可能避免。
用於判斷對象是否存活,基本思想是經過一系列稱爲「GC Root」的對象做爲起點(常見的 GC Root 有系統類加載器、棧中的對象、處於激活狀態的線程等),基於對象引用關係,從 GC Roots 開始向下搜索,所走過的路徑稱爲引用鏈,當一個對象到 GC Root 沒有任何引用鏈相連,證實對象再也不存活。
GC 過程當中分析對象引用關係,爲了保證分析結果的準確性,須要經過停頓全部 Java 執行線程,保證引用關係再也不動態變化,該停頓事件稱爲 Stop The World(STW)。
代碼執行過程當中的一些特殊位置,當線程執行到這些位置的時候,說明虛擬機當前的狀態是安全的,若是有須要 GC,線程能夠在這個位置暫停。
HotSpot 採用主動中斷的方式,讓執行線程在運行期輪詢是否須要暫停的標誌,若須要則中斷掛起。
CMS(Concurrent Mark and Sweep 併發-標記-清除),是一款基於併發、使用標記清除算法的垃圾回收算法,只針對老年代進行垃圾回收。
CMS 收集器工做時,儘量讓 GC 線程和用戶線程併發執行,以達到下降 STW 時間的目的。
經過如下命令行參數,啓用 CMS 垃圾收集器:
-XX:+UseConcMarkSweepGC
複製代碼
值得補充的是,下面介紹到的 CMS GC 是指老年代的 GC,而 Full GC 指的是整個堆的 GC 事件,包括新生代、老年代、元空間等,二者有所區分。
能與 CMS 搭配使用的新生代垃圾收集器有 Serial 收集器和 ParNew 收集器。
這 2 個收集器都採用標記複製算法,都會觸發 STW 事件,中止全部的應用線程。不一樣之處在於,Serial 是單線程執行,ParNew 是多線程執行。
CMS GC 以獲取最小停頓時間爲目的,儘量減小 STW 時間,能夠分爲 7 個階段:
初始標記階段的目標是標記老年代中全部存活的對象, 包括 GC Root 的直接引用, 以及由新生代中存活對象所引用的對象,觸發第一次 STW 事件。
這個過程是支持多線程的(JDK7 以前單線程,JDK8 以後並行,可經過參數 CMSParallelInitialMarkEnabled 調整)。
併發標記階段 GC 線程和應用線程併發執行,遍歷階段 1 初始標記出來的存活對象,而後繼續遞歸標記這些對象可達的對象。
併發預清理階段 GC 線程和應用線程也是併發執行,由於階段 2 是與應用線程併發執行,可能有些引用關係已經發生改變。
經過卡片標記(Card Marking),提早把老年代空間邏輯劃分爲相等大小的區域(Card)。
若是引用關係發生改變,JVM 會將發生改變的區域標記爲 「髒區」(Dirty Card),而後在本階段,這些髒區會被找出來,刷新引用關係,清除「髒區」標記。
併發可取消的預清理階段也不中止應用線程。本階段嘗試在 STW 的最終標記階段(Final Remark)以前儘量地多作一些工做,以減小應用暫停時間。
在該階段不斷循環處理:標記老年代的可達對象、掃描處理 Dirty Card 區域中的對象,循環的終止條件有:
這是 GC 事件中第二次(也是最後一次)STW 階段,目標是完成老年代中全部存活對象的標記,此階段會執行:
併發清除階段與應用程序併發執行,不須要 STW 停頓,根據標記結果清除垃圾對象。
併發重置階段與應用程序併發執行,重置 CMS 算法相關的內部數據, 爲下一次 GC 循環作準備。
CMS 的 GC 停頓時間約 80% 都在最終標記階段(Final Remark),若該階段停頓時間過長,常見緣由是新生代對老年代的無效引用,在上一階段的併發可取消預清理階段中,執行閾值時間內未完成循環,來不及觸發 Young GC,清理這些無效引用。
經過添加參數:-XX:+CMSScavengeBeforeRemark。
在執行最終操做以前先觸發 Young GC,從而減小新生代對老年代的無效引用,下降最終標記階段的停頓。
但若是在上個階段(併發可取消的預清理)已觸發 Young GC,也會重複觸發 Young GC。
併發模式失敗:當 CMS 在執行回收時,新生代發生垃圾回收,同時老年代又沒有足夠的空間容納晉升的對象時,CMS 垃圾回收就會退化成單線程的 Full GC。全部的應用線程都會被暫停,老年代中全部的無效對象都被回收。
晉升失敗:當新生代發生垃圾回收,老年代有足夠的空間能夠容納晉升的對象,可是因爲空閒空間的碎片化,致使晉升失敗,此時會觸發單線程且帶壓縮動做的 Full GC。
併發模式失敗和晉升失敗都會致使長時間的停頓,常看法決思路以下:
下降觸發 CMS GC 的閾值
即參數 -XX:CMSInitiatingOccupancyFraction 的值,讓 CMS GC 儘早執行,以保證有足夠的空間
增長 CMS 線程數,即參數 -XX:ConcGCThreads
增大老年代空間
讓對象儘可能在新生代回收,避免進入老年代
一般 CMS 的 GC 過程基於標記清除算法,不帶壓縮動做,致使愈來愈多的內存碎片須要壓縮。
常見如下場景會觸發內存碎片壓縮:
可經過參數 CMSFullGCsBeforeCompaction 的值,設置多少次 Full GC 觸發一次壓縮。
默認值爲 0,表明每次進入 Full GC 都會觸發壓縮,帶壓縮動做的算法爲上面提到的單線程 Serial Old 算法,暫停時間(STW)時間很是長,須要儘量減小壓縮時間。
G1(Garbage-First)是一款面向服務器的垃圾收集器,支持新生代和老年代空間的垃圾收集,主要針對配備多核處理器及大容量內存的機器。
G1 最主要的設計目標是:實現可預期及可配置的 STW 停頓時間。
爲實現大內存空間的低停頓時間的回收,將劃分爲多個大小相等的 Region。每一個小堆區均可能是 Eden 區,Survivor 區或者 Old 區,可是在同一時刻只能屬於某個代。
在邏輯上, 全部的 Eden 區和 Survivor 區合起來就是新生代,全部的 Old 區合起來就是老年代,且新生代和老年代各自的內存 Region 區域由 G1 自動控制,不斷變更。
當對象大小超過 Region 的一半,則認爲是巨型對象(Humongous Object),直接被分配到老年代的巨型對象區(Humongous Regions)。
這些巨型區域是一個連續的區域集,每個 Region 中最多有一個巨型對象,巨型對象能夠佔多個 Region。
G1 把堆內存劃分紅一個個 Region 的意義在於:
針對新生代和老年代,G1 提供 2 種 GC 模式,Young GC 和 Mixed GC,兩種會致使 Stop The World。
當新生代的空間不足時,G1 觸發 Young GC 回收新生代空間。
Young GC 主要是對 Eden 區進行 GC,它在 Eden 空間耗盡時觸發,基於分代回收思想和複製算法,每次 Young GC 都會選定全部新生代的 Region。
同時計算下次 Young GC 所需的 Eden 區和 Survivor 區的空間,動態調整新生代所佔 Region 個數來控制 Young GC 開銷。
當老年代空間達到閾值會觸發 Mixed GC,選定全部新生代裏的 Region,根據全局併發標記階段(下面介紹到)統計得出收集收益高的若干老年代 Region。
在用戶指定的開銷目標範圍內,儘量選擇收益高的老年代 Region 進行 GC,經過選擇哪些老年代 Region 和選擇多少 Region 來控制 Mixed GC 開銷。
全局併發標記主要是爲 Mixed GC 計算找出回收收益較高的 Region 區域,具體分爲 5 個階段:
暫停全部應用線程(STW),併發地進行標記從 GC Root 開始直接可達的對象(原生棧對象、全局對象、JNI 對象)。
當達到觸發條件時,G1 並不會當即發起併發標記週期,而是等待下一次新生代收集,利用新生代收集的 STW 時間段,完成初始標記,這種方式稱爲借道(Piggybacking)。
在初始標記暫停結束後,新生代收集也完成的對象複製到 Survivor 的工做,應用線程開始活躍起來。
此時爲了保證標記算法的正確性,全部新複製到 Survivor 分區的對象,須要找出哪些對象存在對老年代對象的引用,把這些對象標記成根(Root)。
這個過程稱爲根分區掃描(Root Region Scanning),同時掃描的 Suvivor 分區也被稱爲根分區(Root Region)。
根分區掃描必須在下一次新生代垃圾收集啓動前完成(接下來併發標記的過程當中,可能會被若干次新生代垃圾收集打斷),由於每次 GC 會產生新的存活對象集合。
標記線程與應用程序線程並行執行,標記各個堆中 Region 的存活對象信息,這個步驟可能被新的 Young GC 打斷。
全部的標記任務必須在堆滿前就完成掃描,若是併發標記耗時很長,那麼有可能在併發標記過程當中,又經歷了幾回新生代收集。
和 CMS 相似暫停全部應用線程(STW),以完成標記過程短暫地中止應用線程, 標記在併發標記階段發生變化的對象,和全部未被標記的存活對象,同時完成存活數據計算。
爲即將到來的轉移階段作準備, 此階段也爲下一次標記執行全部必需的整理計算工做:
G1 的正常處理流程中沒有 Full GC,只有在垃圾回收處理不過來(或者主動觸發)時纔會出現,G1 的 Full GC 就是單線程執行的 Serial old gc,會致使很是長的 STW,是調優的重點,須要儘可能避免 Full GC。
常見緣由以下:
相似 CMS,常見的解決是:
巨型對象區中的每一個 Region 中包含一個巨型對象,剩餘空間再也不利用,致使空間碎片化,當 G1 沒有合適空間分配巨型對象時,G1 會啓動串行 Full GC 來釋放空間。
能夠經過增長 -XX:G1HeapRegionSize 來增大 Region 大小,這樣一來,至關一部分的巨型對象就再也不是巨型對象了,而是採用普通的分配方式。
緣由是爲了儘可能知足目標停頓時間,邏輯上的 Young 區會進行動態調整。若是設置了大小,則會覆蓋掉而且會禁用掉對停頓時間的控制。
使用應用的平均響應時間做爲參考來設置 MaxGCPauseMillis,JVM 會盡可能去知足該條件,多是 90% 的請求或者更多的響應時間在這以內, 可是並不表明是全部的請求都能知足,平均響應時間設置太小會致使頻繁 GC。
如何分析系統 JVM GC 運行情況及合理優化?
GC 優化的核心思路在於,儘量讓對象在新生代中分配和回收,儘可能避免過多對象進入老年代,致使對老年代頻繁進行垃圾回收,同時給系統足夠的內存減小新生代垃圾回收次數,進行系統分析和優化也是圍繞着這個思路展開。
分析系統的運行情況:
經常使用工具以下:
jstat 是 JVM 自帶命令行工具,可用於統計內存分配速率、GC 次數,GC 耗時。經常使用命令格式以下:
jstat -gc <pid> <統計間隔時間> <統計次數>
複製代碼
輸出返回值表明含義以下:
例如:jstat -gc 32683 1000 10,統計 pid=32683 的進程,每秒統計 1 次,統計 10 次。
jmap 也是 JVM 自帶命令行工具,可用於瞭解系統運行時的對象分佈。經常使用命令格式以下:
// 命令行輸出類名、類數量數量,類佔用內存大小,
// 按照類佔用內存大小降序排列
jmap -histo <pid>
// 生成堆內存轉儲快照,在當前目錄下導出dump.hrpof的二進制文件,
// 能夠用eclipse的MAT圖形化工具分析
jmap -dump:live,format=b,file=dump.hprof <pid>
複製代碼
用來查看正在運行的 Java 應用程序的擴展參數,包括 Java System 屬性和 JVM 命令行參數。命令格式以下:
jinfo <pid>
複製代碼
平臺主要對用戶在 App 中行爲進行定時分析統計,並支持報表導出,使用 CMS GC 算法。
數據分析師在使用中發現系統頁面打開常常卡頓,經過 jstat 命令發現系統每次 Young GC 後大約有 10% 的存活對象進入老年代。
原來是由於 Survivor 區空間設置太小,每次 Young GC 後存活對象在 Survivor 區域放不下,提早進入老年代。
經過調大 Survivor 區,使得 Survivor 區能夠容納 Young GC 後存活對象,對象在 Survivor 區經歷屢次 Young GC 達到年齡閾值才進入老年代。
調整以後每次 Young GC 後進入老年代的存活對象穩定運行時僅幾百 Kb,Full GC 頻率大大下降。
網關主要消費 Kafka 數據,進行數據處理計算而後轉發到另外的 Kafka 隊列,系統運行幾個小時候出現 OOM,重啓系統幾個小時以後又 OOM。
經過 jmap 導出堆內存,在 eclipse MAT 工具分析才找出緣由:代碼中將某個業務 Kafka 的 topic 數據進行日誌異步打印,該業務數據量較大,大量對象堆積在內存中等待被打印,致使 OOM。
系統對外提供各類帳號鑑權服務,使用時發現系統常常服務不可用,經過 Zabbix 的監控平臺監控發現系統頻繁發生長時間 Full GC,且觸發時老年代的堆內存一般並無佔滿,發現原來是業務代碼中調用了 System.gc()。
GC 問題能夠說沒有捷徑,排查線上的性能問題自己就並不簡單,除了將本文介紹到的原理和工具融會貫通,還須要咱們不斷去積累經驗,真正作到性能最優。
篇幅所限,再也不展開介紹常見 GC 參數的使用,能夠從 GitHub 克隆:
https://github.com/caison/caison-blog-demo
複製代碼
轉載:陳彩華(caison),Akulaku 岩心科技開發工程師,喜歡研究分佈式系統、線上問題排查、架構設計
本賬號持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。