本文介紹GC基礎原理和理論,GC調優方法思路和方法,基於Hotspot jdk1.8,學習以後將瞭解如何對生產系統出現的GC問題進行排查解決git
閱讀時長約30分鐘,內容主要以下:github
大多數狀況下對 Java 程序進行GC調優, 主要關注兩個目標:響應速度、吞吐量web
響應速度(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、Old GC、Full GC、Mixed GC
新生代內存的垃圾收集事件稱爲Young GC(又稱Minor GC),當JVM沒法爲新對象分配在新生代內存空間時總會觸發 Young GC,好比 Eden 區佔滿時。新對象分配頻率越高, Young GC 的頻率就越高
Young GC 每次都會引發全線停頓(Stop-The-World),暫停全部的應用線程,停頓時間相對老年代GC的形成的停頓,幾乎能夠忽略不計
Old GC,只清理老年代空間的GC事件,只有CMS的併發收集是這個模式 Full GC,清理整個堆的GC事件,包括新生代、老年代、元空間等
GC日誌是一個很重要的工具,它準確記錄了每一次的GC的執行時間和執行結果,經過分析GC日誌能夠調優堆設置和GC設置,或者改進應用程序的對象分配模式,開啓的JVM啓動參數以下:
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps
複製代碼
常見的Young GC、Full GC日誌含義以下:
免費的GC日誌圖形分析工具推薦下面2個:
Java提供的自動內存管理,能夠歸結爲解決了對象的內存分配和回收的問題,前面已經介紹了內存回收,下面介紹幾條最廣泛的內存分配策略
對象優先在Eden區分配 大多數狀況下,對象在先新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Young GC
大對象之間進入老年代 JVM提供了一個對象大小閾值參數(-XX:PretenureSizeThreshold,默認值爲0,表明無論多大都是先在Eden中分配內存),大於參數設置的閾值值的對象直接在老年代分配,這樣能夠避免對象在Eden及兩個Survivor直接發生大內存複製
長期存活的對象將進入老年代 對象每經歷一次垃圾回收,且沒被回收掉,它的年齡就增長1,大於年齡閾值參數(-XX:MaxTenuringThreshold,默認15)的對象,將晉升到老年代中
空間分配擔保 當進行Young GC以前,JVM須要預估:老年代是否可以容納Young GC後新生代晉升到老年代的存活對象,以肯定是否須要提早觸發GC回收老年代空間,基於空間分配擔保策略來計算:
continueSize:老年代最大可用連續空間
Young GC以後若是成功(Young GC後晉升對象能放入老年代),則表明擔保成功,不用再進行Full GC,提升性能;若是失敗,則會出現「promotion failed」錯誤,表明擔保失敗,須要進行Full GC
另外,若是Young GC後S0或S1區不足以容納:未達到晉升老年代條件的新生代存活對象,會致使這些存活對象直接進入老年代,須要儘可能避免
可達性分析算法:用於判斷對象是否存活,基本思想是經過一系列稱爲「GC Root」的對象做爲起點(常見的GC Root有系統類加載器、棧中的對象、處於激活狀態的線程等),基於對象引用關係,從GC Roots開始向下搜索,所走過的路徑稱爲引用鏈,當一個對象到GC Root沒有任何引用鏈相連,證實對象再也不存活
Stop The World:GC過程當中分析對象引用關係,爲了保證分析結果的準確性,須要經過停頓全部Java執行線程,保證引用關係再也不動態變化,該停頓事件稱爲Stop The World(STW)
Safepoint:代碼執行過程當中的一些特殊位置,當線程執行到這些位置的時候,說明虛擬機當前的狀態是安全的,若是有須要GC,線程能夠在這個位置暫停。HotSpot採用主動中斷的方式,讓執行線程在運行期輪詢是否須要暫停的標誌,若須要則中斷掛起
CMS(Concurrent Mark and Swee 併發-標記-清除),是一款基於併發、使用標記清除算法的垃圾回收算法,只針對老年代進行垃圾回收。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區域中的對象,循環的終止條件有: 1 達到循環次數 2 達到循環執行時間閾值 3 新生代內存使用率達到閾值
這是GC事件中第二次(也是最後一次)STW階段,目標是完成老年代中全部存活對象的標記。在此階段執行: 1 遍歷新生代對象,從新標記 2 根據GC Roots,從新標記 3 遍歷老年代的Dirty Card,從新標記
此階段與應用程序併發執行,不須要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過程基於標記清除算法,不帶壓縮動做,致使愈來愈多的內存碎片須要壓縮,常見如下場景會觸發內存碎片壓縮:
可經過參數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
Young GC 當新生代的空間不足時,G1觸發Young GC回收新生代空間 Young GC主要是對Eden區進行GC,它在Eden空間耗盡時觸發,基於分代回收思想和複製算法,每次Young GC都會選定全部新生代的Region,同時計算下次Young GC所需的Eden區和Survivor區的空間,動態調整新生代所佔Region個數來控制Young GC開銷
Mixed GC 當老年代空間達到閾值會觸發Mixed GC,選定全部新生代裏的Region,根據全局併發標記階段(下面介紹到)統計得出收集收益高的若干老年代 Region。在用戶指定的開銷目標範圍內,儘量選擇收益高的老年代Region進行GC,經過選擇哪些老年代Region和選擇多少Region來控制Mixed GC開銷
全局併發標記主要是爲Mixed GC計算找出回收收益較高的Region區域,具體分爲5個階段
階段 1: 初始標記(Initial Mark) 暫停全部應用線程(STW),併發地進行標記從 GC Root 開始直接可達的對象(原生棧對象、全局對象、JNI 對象),當達到觸發條件時,G1 並不會當即發起併發標記週期,而是等待下一次新生代收集,利用新生代收集的 STW 時間段,完成初始標記,這種方式稱爲借道(Piggybacking)
階段 2: 根區域掃描(Root Region Scan) 在初始標記暫停結束後,新生代收集也完成的對象複製到 Survivor 的工做,應用線程開始活躍起來; 此時爲了保證標記算法的正確性,全部新複製到 Survivor 分區的對象,須要找出哪些對象存在對老年代對象的引用,把這些對象標記成根(Root); 這個過程稱爲根分區掃描(Root Region Scanning),同時掃描的 Suvivor 分區也被稱爲根分區(Root Region); 根分區掃描必須在下一次新生代垃圾收集啓動前完成(接下來併發標記的過程當中,可能會被若干次新生代垃圾收集打斷),由於每次 GC 會產生新的存活對象集合
階段 3: 併發標記(Concurrent Marking) 標記線程與應用程序線程並行執行,標記各個堆中Region的存活對象信息,這個步驟可能被新的 Young GC 打斷 全部的標記任務必須在堆滿前就完成掃描,若是併發標記耗時很長,那麼有可能在併發標記過程當中,又經歷了幾回新生代收集
階段 4: 再次標記(Remark) 和CMS相似暫停全部應用線程(STW),以完成標記過程短暫地中止應用線程, 標記在併發標記階段發生變化的對象,和全部未被標記的存活對象,同時完成存活數據計算
階段 5: 清理(Cleanup) 爲即將到來的轉移階段作準備, 此階段也爲下一次標記執行全部必需的整理計算工做:
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 -gc <pid> <統計間隔時間> <統計次數>
複製代碼
輸出返回值表明含義以下:
例如: jstat -gc 32683 1000 10 ,統計pid=32683的進程,每秒統計1次,統計10次
// 命令行輸出類名、類數量數量,類佔用內存大小,
// 按照類佔用內存大小降序排列
jmap -histo <pid>
// 生成堆內存轉儲快照,在當前目錄下導出dump.hrpof的二進制文件,
// 能夠用eclipse的MAT圖形化工具分析
jmap -dump:live,format=b,file=dump.hprof <pid>
複製代碼
jinfo <pid>
複製代碼
用來查看正在運行的 Java 應用程序的擴展參數,包括Java System屬性和JVM命令行參數
其餘GC工具
平臺主要對用戶在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問題能夠說沒有捷徑,排查線上的性能問題自己就並不簡單,除了將本文介紹到的原理和工具融會貫通,還須要咱們不斷去積累經驗,真正作到性能最優