本文轉自:http://www.importnew.com/11336.html
另外付JVM參數介紹:http://www.cnblogs.com/langtianya/p/3898760.html
高性能應用構成了現代網絡的支柱。LinkedIn有許多內部高吞吐量服務來滿足每秒數千次的用戶請求。要優化用戶體驗,低延遲地響應這些請求非常重要。
比如說,用戶經常用到的一個功能是瞭解動態信息——不斷更新的專業活動和內容的列表。動態信息在LinkedIn隨處可見,包括公司頁面,學校頁面以及最重要的主頁。基礎動態信息數據平臺爲我們的經濟圖譜(會員,公司,羣組等等)中各種實體的更新建立索引,它必須高吞吐低延遲地實現相關的更新。
這些高吞吐低延遲的Java應用轉變爲產品,開發人員必須確保應用開發週期的每個階段一致的性能。確定優化垃圾回收(Garbage Collection,GC)的設置對達到這些指標非常關鍵。
本文章通過一系列步驟來明確需求並優化GC,目標讀者是爲實現應用的高吞吐低延遲,對使用系統方法優化GC感興趣的開發人員。文章中的方法來自於LinkedIn構建下一代動態信息數據平臺過程。這些方法包括但不侷限於以下幾點:併發標記清除(Concurrent Mark Sweep,CMS)和G1垃圾回收器的CPU和內存開銷,避免長期存活對象引起的持續GC週期,優化GC線程任務分配使性能提升,以及GC停頓時間可預測所需的OS設置。
優化GC的正確時機?
GC運行隨着代碼級的優化和工作負載而發生變化。因此在一個已實施性能優化的接近完成的代碼庫上調整GC非常重要。但是在端到端的基本原型上進行初步分析也很有必要,該原型系統使用存根代碼並模擬了可代表產品環境的工作負載。這樣可以捕捉該架構延遲和吞吐量的真實邊界,進而決定是否縱向或橫向擴展。
在下一代動態信息數據平臺的原型階段,幾乎實現了所有端到端的功能,並且模擬了當前產品基礎架構所服務的查詢負載。從中我們獲得了多種用來衡量應用性能的工作負載特徵和足夠長時間運行情況下的GC特徵。
優化GC的步驟
下面是爲滿足高吞吐,低延遲需求優化GC的總體步驟。也包括在動態信息數據平臺原型實施的具體細節。可以看到在ParNew/CMS有最好的性能,但我們也實驗了G1垃圾回收器。
1.理解GC基礎知識
理解GC工作機制非常重要,因爲需要調整大量的參數。Oracle的Hotspot JVM 內存管理白皮書是開始學習Hotspot JVM GC算法非常好的資料。瞭解G1垃圾回收器,請查看該論文。
2. 仔細考量GC需求
爲降低應用性能的GC開銷,可以優化GC的一些特徵。吞吐量、延遲等這些GC特徵應該長時間測試運行觀察,確保特徵數據來自於應用程序的處理對象數量發生變化的多個GC週期。
- Stop-the-world回收器回收垃圾時會暫停應用線程。停頓的時長和頻率不應該對應用遵守SLA產生不利的影響。
- 併發GC算法與應用線程競爭CPU週期。這個開銷不應該影響應用吞吐量。
- 不壓縮GC算法會引起堆碎片化,導致full GC長時間Stop-the-world停頓。
- 垃圾回收工作需要佔用內存。一些GC算法產生更高的內存佔用。如果應用程序需要較大的堆空間,要確保GC的內存開銷不能太大。
- 清晰地瞭解GC日誌和常用的JVM參數對簡單調整GC運行很有必要。GC運行隨着代碼複雜度增長或者工作特性變化而改變。
我們使用Linux OS的Hotspot Java7u51,32GB堆內存,6GB新生代(young generation)和-XX:CMSInitiatingOccupancyFraction
值爲70(老年代GC觸發時其空間佔用率)開始實驗。設置較大的堆內存用來維持長期存活對象的對象緩存。一旦這個緩存被填充,提升到老年代的對象比例顯著下降。
使用初始的GC配置,每三秒發生一次80ms的新生代GC停頓,超過百分之99.9的應用延遲100ms。這樣的GC很可能適合於SLA不太嚴格要求延遲的許多應用。然而,我們的目標是儘可能降低百分之99.9應用的延遲,爲此GC優化是必不可少的。
3.理解GC指標
優化之前要先衡量。瞭解GC日誌的詳細細節(使用這些選項:-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime
)可以對該應用的GC特徵有總體的把握。
LinkedIn的內部監控和報表系統,inGraphs和Naarad,生成了各種有用的指標可視化圖形,比如GC停頓時間百分比,一次停頓最大持續時間,長時間內GC頻率。除了Naarad,有很多開源工具比如gclogviewer可以從GC日誌創建可視化圖形。
在這個階段,需要確定GC頻率和停頓時長是否影響應用滿足延遲性需求的能力。
4.降低GC頻率
在分代GC算法中,降低迴收頻率可以通過:(1)降低對象分配/提升率;(2)增加代空間的大小。
在Hotspot JVM中,新生代GC停頓時間取決於一次垃圾回收後對象的數量,而不是新生代自身的大小。增加新生代大小對於應用性能的影響需要仔細評估:
- 如果更多的數據存活而且被複制到survivor區域,或者每次垃圾回收更多的數據提升到老年代,增加新生代大小可能導致更長的新生代GC停頓。
- 另一方面,如果每次垃圾回收後存活對象數量不會大幅增加,停頓時間可能不會延長。在這種情況下,減少GC頻率可能使應用總體延遲降低和(或)吞吐量增加。
對於大部分爲短期存活對象的應用,僅僅需要控制前面所說的參數。對於創建長期存活對象的應用,就需要注意,被提升的對象可能很長時間都不能被老年代GC週期回收。如果老年代GC觸發閾值(老年代空間佔用率百分比)比較低,應用將陷入不斷的GC週期。設置高的GC觸發閾值可避免這一問題。
由於我們的應用在堆中維持了長期存活對象的較大緩存,將老年代GC觸發閾值設置爲-XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly
。我們也試圖增加新生代大小來減少新生代回收頻率,但是並沒有採用,因爲這增加了應用延遲。
5.縮短GC停頓時間
減少新生代大小可以縮短新生代GC停頓時間,因爲這樣被複制到survivor區域或者被提升的數據更少。但是,正如前面提到的,我們要觀察減少新生代大小和由此導致的GC頻率增加對於整體應用吞吐量和延遲的影響。新生代GC停頓時間也依賴於tenuring threshold(提升閾值)和空間大小(見第6步)。
使用CMS嘗試最小化堆碎片和與之關聯的老年代垃圾回收full GC停頓時間。通過控制對象提升比例和減小-XX:CMSInitiatingOccupancyFraction
的值使老年代GC在低閾值時觸發。所有選項的細節調整和他們相關的權衡,請查看Web Services的Java 垃圾回收和Java 垃圾回收精粹。
我們觀察到Eden區域的大部分新生代被回收,幾乎沒有對象在survivor區域死亡,所以我們將tenuring threshold從8降低到2(使用選項:-XX:MaxTenuringThreshold=2
),爲的是縮短新生代垃圾回收消耗在數據複製上的時間。
我們也注意到新生代回收停頓時間隨着老年代空間佔用率上升而延長。這意味着來自老年代的壓力使得對象提升花費更多的時間。爲解決這個問題,將總的堆內存大小增加到40GB,減小-XX:CMSInitiatingOccupancyFraction
的值到80,更快地開始老年代回收。儘管-XX:CMSInitiatingOccupancyFraction
的值減小了,增大堆內存可以避免不斷的老年代GC。在本階段,我們獲得了70ms新生代回收停頓和百分之99.9延遲80ms。
6.優化GC工作線程的任務分配
進一步縮短新生代停頓時間,我們決定研究優化與GC線程綁定任務的選項。
-XX:ParGCCardsPerStrideChunk
選項控制GC工作線程的任務粒度,可以幫助不使用補丁而獲得最佳性能,這個補丁用來優化新生代垃圾回收的卡表掃描時間。有趣的是新生代GC時間隨着老年代空間的增加而延長。將這個選項值設爲32678,新生代回收停頓時間降低到平均50ms。此時百分之99.9應用延遲60ms。
也有其他選項將任務映射到GC線程,如果OS允許的話,-XX:+BindGCTaskThreadsToCPUs
選項綁定GC線程到個別的CPU核。-XX:+UseGCTaskAffinity
使用affinity參數將任務分配給GC工作線程。然而,我們的應用並沒有從這些選項發現任何益處。實際上,一些調查顯示這些選項在Linux系統不起作用[1,2]。
7.瞭解GC的CPU和內存開銷
併發GC通常會增加CPU的使用。我們觀察了運行良好的CMS默認設置,併發GC和G1垃圾回收器共同工作引起的CPU使用增加顯著降低了應用的吞吐量和延遲。與CMS相比,G1可能佔用了應用更多的內存開銷。對於低吞吐量的非計算密集型應用,GC的高CPU使用率可能不需要擔心。
8.爲GC優化系統內存和I/O管理
通常來說,GC停頓發生在(1)低用戶時間,高系統時間和高時鐘時間和(2)低用戶時間,低系統時間和高時鐘時間。這意味着基礎的進程/OS設置存在問題。情況(1)可能說明Linux從JVM偷頁,情況(2)可能說明清除磁盤緩存時Linux啓動GC線程,等待I/O時線程陷入內核。在這些情況下如何設置參數可以參考該PPT。
爲避免運行時性能損失,啓動應用時使用JVM選項-XX:+AlwaysPreTouch
訪問和清零頁面。設置vm.swappiness
爲零,除非在絕對必要時,OS不會交換頁面。
可能你會使用mlock
將JVM頁pin在內存中,使OS不換出頁面。但是,如果系統用盡了所有的內存和交換空間,OS通過kill進程來回收內存。通常情況下,Linux內核會選擇高駐留內存佔用但還沒有長時間運行的進程(OOM情況下killing進程的工作流)。對我們而言,這個進程很有可能就是我們的應用程序。一個服務具備優雅降級(適度退化)的特點會更好,服務突然故障預示着不太好的可操作性——因此,我們沒有使用mlock
而是vm.swappiness
避免可能的交換懲罰。
LinkedIn動態信息數據平臺的GC優化
對於該平臺原型系統,我們使用Hotspot JVM的兩個算法優化垃圾回收:
- 新生代垃圾回收使用ParNew,老年代垃圾回收使用CMS。
- 新生代和老年代使用G1。G1用來解決堆大小爲6GB或者更大時存在的低於0.5秒穩定的、可預測停頓時間的問題。在我們用G1實驗過程中,儘管調整了各種參數,但沒有得到像ParNew/CMS一樣的GC性能或停頓時間的可預測值。我們查詢了使用G1發生內存泄漏相關的一個bug[3],但還不能確定根本原因。
使用ParNew/CMS,應用每三秒40-60ms的新生代停頓和每小時一個CMS週期。JVM選項如下:
// JVM sizing options
-server -Xms40g -Xmx40g -XX:MaxDirectMemorySize=4096m -XX:PermSize=256m -XX:MaxPermSize=256m
// Young generation options
-XX:NewSize=6g -XX:MaxNewSize=6g -XX:+UseParNewGC -XX:MaxTenuringThreshold=2 -XX:SurvivorRatio=8 -XX:+UnlockDiagnosticVMOptions -XX:ParGCCardsPerStrideChunk=32768
// Old generation options
-XX:+UseConcMarkSweepGC -XX:CMSParallelRemarkEnabled -XX:+ParallelRefProcEnabled -XX:+CMSClassUnloadingEnabled -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly
// Other options
-XX:+AlwaysPreTouch -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:-OmitStackTraceInFastThrow
使用這些選項,對於幾千次讀請求的吞吐量,應用百分之99.9的延遲降低到60ms。
參考:
[1] -XX:+BindGCTaskThreadsToCPUs
似乎在Linux系統上不起作用,因爲hotspot/src/os/linux/vm/os_linux.cpp
的distribute_processes
方法在JDK7或JDK8沒有實現。
[2] -XX:+UseGCTaskAffinity
選項在JDK7和JDK8的所有平臺似乎都不起作用,因爲任務的affinity屬性永遠被設置爲sentinel_worker = (uint) -1
。源碼見hotspot/src/share/vm/gc_implementation/parallelScavenge/{gcTaskManager.cpp,gcTaskThread.cpp, gcTaskManager.cpp}
。
[3] G1存在一些內存泄露的bug,可能Java7u51沒有修改。這個bug僅在Java 8修正了。
目前服務器配置如下:
-server -Xms1536m -Xmx1536m -XX:MaxDirectMemorySize=128m -XX:PermSize=256m -Xss256k -XX:MaxPermSize=256m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:+UseParNewGC -XX:MaxTenuringThreshold=2 -XX:SurvivorRatio=8 -XX:+UnlockDiagnosticVMOptions -XX:+UseConcMarkSweepGC -XX:+ParallelRefProcEnabled -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:CMSMaxAbortablePrecleanTime=500 -XX:+UseCompressedOops -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+UseCMSInitiatingOccupancyOnly -XX:+AlwaysPreTouch -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:-OmitStackTraceInFastThrow -verbose:gc -Xloggc:/log/gc/amazon-gc.log
另外付幾個參考配置:
http://developer.51cto.com/art/201201/312020.htm
http://blog.csdn.net/madun/article/details/7913043
http://www.tuicool.com/articles/6Vj63qy
http://www.tuicool.com/articles/zQbmae
http://www.open-open.com/lib/view/open1399988244301.html
http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html
http://blog.sae.sina.com.cn/archives/4141
-Xmx3550m:設置JVM最大堆內存 爲3550M。
-Xms3550m:設置JVM初始堆內存 爲3550M。此值可以設置與-Xmx相同,以避免每次垃圾回收完成後JVM重新分配內存。
-Xss128k: 設置每個線程的棧 大小。JDK5.0以後每個線程棧大小爲1M,之前每個線程棧大小爲256K。應當根據應用的線程所需內存大小進行調整。在相同物理內存下,減小這個值能 生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。
-Xmn2g:設置堆內存年輕代 大小爲2G。整個堆內存大小 = 年輕代大小 + 年老代大小 + 持久代大小 。持久代一般固定大小爲64m,所以增大年輕代後,將會減小年老代大小。此值對系統性能影響較大,Sun官方推薦配置爲整個堆的3/8。
-XX:PermSize=256M:設置堆內存持久代 初始值爲256M。(貌似是Eclipse等IDE的初始化參數)
-XX:MaxNewSize=size:新生成的對象能佔用內存的最大值。
-XX:MaxPermSize=512M:設置持久代最大值爲512M。
-XX:NewRatio=4:設置堆內存年輕代(包括Eden和兩個Survivor區)與堆內存年老代的比值(除去持久代) 。設置爲4,則年輕代所佔與年老代所佔的比值爲1:4。
-XX:SurvivorRatio=4: 設置堆內存年輕代中Eden區與Survivor區大小的比值 。設置爲4,則兩個Survivor區(JVM堆內存年輕代中默認有2個Survivor區)與一個Eden區的比值爲2:4,一個Survivor區佔 整個年輕代的1/6。
-XX:MaxTenuringThreshold=7:表示一個對象如果在救助空間(Survivor區)移動7次還沒有被回收就放入年老代。
如果設置爲0的話,則年輕代對象不經過Survivor區,直接進入年老代,對於年老代比較多的應用,這樣做可以提高效率。
如果將此值設置爲一個較大值,則年輕代對象會在Survivor區進行多次複製,這樣可以增加對象在年輕代存活時間,增加對象在年輕代即被回收的概率。
回收器選擇
JVM給了三種選擇:串行收集器、並行收集器、併發收集器,但是串行收集器只適用於小數據量的情況,所以這裏的選擇主要針對並行收集器和併發收集器。
-XX:+UseSerialGC:設置串行收集器
並行收集器(吞吐量優先)
-XX:+UseParallelGC:選擇垃圾收集器爲並行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用併發收集,而年老代仍舊使用串行收集。
-XX:ParallelGCThreads=20:配置並行收集器的線程數,即:同時多少個線程一起進行垃圾回收。此值最好配置與處理器數目相等。
-XX:+UseParallelOldGC:配置年老代垃圾收集方式爲並行收集。JDK6.0支持對年老代並行收集。
-XX:MaxGCPauseMillis=100:設置每次年輕代垃圾回收的最長時間(單位毫秒),如果無法滿足此時間,JVM會自動調整年輕代大小,以滿足此值。
-XX:+UseAdaptiveSizePolicy:設置此選項後,並行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目標系統規定的最低響應時間或者收集頻率等。
此參數建議使用並行收集器時,一直打開。
併發收集器(響應時間優先)
-XX:+UseParNewGC:設置年輕代爲併發收集。可與CMS收集同時使用。JDK5.0以上,JVM會根據系統配置自行設置,所以無需再設置此值。
CMS, 全稱Concurrent Low Pause Collector,是jdk1.4後期版本開始引入的新gc算法,在jdk5和jdk6中得到了進一步改進,它的主要適合場景是對響應時間的重要性需求 大於對吞吐量的要求,能夠承受垃圾回收線程和應用線程共享處理器資源,並且應用中存在比較多的長生命週期的對象的應用。CMS是用於對tenured generation的回收,也就是年老代的回收,目標是儘量減少應用的暫停時間,減少FullGC發生的機率,利用和應用程序線程併發的垃圾回收線程來 標記清除年老代。
-XX:+UseConcMarkSweepGC:設置年老代爲併發收集。測試中配置這個以後,-XX:NewRatio=4的配置失效了。所以,此時年輕代大小最好用-Xmn設置。
-XX:CMSFullGCsBeforeCompaction=:由於併發收集器不對內存空間進行壓縮、整理,所以運行一段時間以後會產生“碎片”,使得運行效率降低。此參數設置運行次FullGC以後對內存空間進行壓縮、整理。
-XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮。可能會影響性能,但是可以消除內存碎片。
-XX:+CMSIncrementalMode:設置爲增量收集模式。一般適用於單CPU情況。
-XX:CMSInitiatingOccupancyFraction=70:表示年老代空間到70%時就開始執行CMS,確保年老代有足夠的空間接納來自年輕代的對象。
注:如果使用 throughput collector 和 concurrent low pause collector 這兩種垃圾收集器,需要適當的挺高內存大小,爲多線程做準備。
-XX:+ScavengeBeforeFullGC:新生代GC優先於Full GC執行。
-XX:-DisableExplicitGC:禁止調用System.gc(),但JVM的gc仍然有效。
-XX:+MaxFDLimit:最大化文件描述符的數量限制。
-XX:+UseThreadPriorities:啓用本地線程優先級API,即使 java.lang.Thread.setPriority() 生效,反之無效。
-XX:SoftRefLRUPolicyMSPerMB=0:“軟引用”的對象在最後一次被訪問後能存活0毫秒(默認爲1秒)。
-XX:TargetSurvivorRatio=90:允許90%的Survivor空間被佔用(默認爲50%)。提高對於Survivor的使用率——超過就會嘗試垃圾回收。
輔助信息
-XX:-CITime:打印消耗在JIT編譯的時間
-XX:ErrorFile=./hs_err_pid.log:保存錯誤日誌或者數據到指定文件中
-XX:-ExtendedDTraceProbes:開啓solaris特有的dtrace探針
-XX:HeapDumpPath=./java_pid.hprof:指定導出堆信息時的路徑或文件名
-XX:-HeapDumpOnOutOfMemoryError:當首次遭遇內存溢出時導出此時堆中相關信息
-XX:OnError=";":出現致命ERROR之後運行自定義命令
-XX:OnOutOfMemoryError=";":當首次遭遇內存溢出時執行自定義命令
-XX:-PrintClassHistogram:遇到Ctrl-Break後打印類實例的柱狀信息,與jmap -histo功能相同
-XX:-PrintConcurrentLocks:遇到Ctrl-Break後打印併發鎖的相關信息,與jstack -l功能相同
-XX:-PrintCommandLineFlags:打印在命令行中出現過的標記
-XX:-PrintCompilation:當一個方法被編譯時打印相關信息
-XX:-PrintGC:每次GC時打印相關信息
-XX:-PrintGC Details:每次GC時打印詳細信息
-XX:-PrintGCTimeStamps:打印每次GC的時間戳
-XX:-TraceClassLoading:跟蹤類的加載信息
-XX:-TraceClassLoadingPreorder:跟蹤被引用到的所有類的加載信息
-XX:-TraceClassResolution:跟蹤常量池
-XX:-TraceClassUnloading:跟蹤類的卸載信息
-XX:-TraceLoaderConstraints:跟蹤類加載器約束的相關信息
JVM服務調優實戰
服務器:8 cup, 8G mem
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-Xms5g:設置JVM初始內存爲5G。此值可以設置與-Xmx相同,以避免每次垃圾回收完成後JVM重新分配內存。
-Xmn2g:設置年輕代大小爲2G。整個堆內存大小 = 年輕代大小 + 年老代大小 + 持久代大小 。持久代一般固定大小爲64m,所以增大年輕代後,將會減小年老代大小。此值對系統性能影響較大,Sun官方推薦配置爲整個堆的3/8。
-XX:+UseParNewGC:設置年輕代爲並行收集。可與CMS收集同時使用。JDK5.0以上,JVM會根據系統配置自行設置,所以無需再設置此值。
-XX:ParallelGCThreads=8:配置並行收集器的線程數,即:同時多少個線程一起進行垃圾回收。此值最好配置與處理器數目相等。
-XX:SurvivorRatio=6:設置年輕代中Eden區與Survivor區的大小比值。根據經驗設置爲6,則兩個Survivor區與一個Eden區的比值爲2:6,一個Survivor區佔整個年輕代的1/8。
-XX:MaxTenuringThreshold=30: 設置垃圾最大年齡(次數)。如果設置爲0的話,則年輕代對象不經過Survivor區直接進入年老代。對於年老代比較多的應用,可以提高效率。如果將此值 設置爲一個較大值,則年輕代對象會在Survivor區進行多次複製,這樣可以增加對象再年輕代的存活時間,增加在年輕代即被回收的概率。設置爲30表示 一個對象如果在Survivor空間移動30次還沒有被回收就放入年老代。
-XX:+UseConcMarkSweepGC:設置年老代爲併發收集。測試配置這個參數以後,參數-XX:NewRatio=4就失效了,所以,此時年輕代大小最好用-Xmn設置,因此這個參數不建議使用。
參考資料 - JVM堆內存的分代
虛 擬機的堆內存共劃分爲三個代:年輕代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java類的類信息,與垃圾收集器要收集的Java對象關係不大。所以,年輕代和年老代的劃分纔是對垃圾 收集影響比較大的。
所有新生成的對象首先都是放在年輕代的。年輕代的目標就是儘可能快速的收集掉那些生命週期短的對象。年輕代分三個區。一個Eden區,兩個 Survivor區(一般而言)。
在年輕代中經歷了N(可配置)次垃圾回收後仍然存活的對象,就會被放到年老代中。因此,可以認爲年老代中存放的都是一些生命週期較長的對象。
用於存放靜態數據,如 Java Class, Method 等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些Class,例如 Hibernate 等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中動態增加的類型。持久代大小通過 -XX:MaxPermSize= 進行設置。
CMSInitiatingOccupancyFraction值與Xmn的關係公式
上面介紹了promontion faild產生的原因是EDEN空間不足的情況下將EDEN與From survivor中的存活對象存入To survivor區時,To survivor區的空間不足,再次晉升到old gen區,而old gen區內存也不夠的情況下產生了promontion faild從而導致full gc.那可以推斷出:eden+from survivor < old gen區剩餘內存時,不會出現promontion faild的情況,即:
(Xmx-Xmn)*(1-CMSInitiatingOccupancyFraction/100)>=(Xmn-Xmn/(SurvivorRatior+2)) 進而推斷出:
CMSInitiatingOccupancyFraction <=((Xmx-Xmn)-(Xmn-Xmn/(SurvivorRatior+2)))/(Xmx-Xmn)*100
例如:
當xmx=128 xmn=36 SurvivorRatior=1時 CMSInitiatingOccupancyFraction<=((128.0-36)-(36-36/(1+2)))/(128-36)*100 =73.913
當xmx=128 xmn=24 SurvivorRatior=1時 CMSInitiatingOccupancyFraction<=((128.0-24)-(24-24/(1+2)))/(128-24)*100=84.615…
當xmx=3000 xmn=600 SurvivorRatior=1時 CMSInitiatingOccupancyFraction<=((3000.0-600)-(600-600/(1+2)))/(3000-600)*100=83.33
CMSInitiatingOccupancyFraction低於70% 需要調整xmn或SurvivorRatior值。
一:理解GC日誌格式,讀GC日誌的方法
1:開啓日誌
-verbose:gc
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/path/gc.log
-XX:+UseGCLogFileRotation 啓用GC日誌文件的自動轉儲 (Since Java)
-XX:NumberOfGClogFiles=1 GC日誌文件的循環數目 (Since Java)
-XX:GCLogFileSize=1M 控制GC日誌文件的大小 (Since Java)
-XX:+PrintGC包含-verbose:gc
-XX:+PrintGCDetails //包含-XX:+PrintGC
只要設置-XX:+PrintGCDetails 就會自動帶上-verbose:gc和-XX:+PrintGC
-XX:+PrintGCDateStamps/-XX:+PrintGCTimeStamps 輸出gc的觸發時間
2:新生代(Young GC)gc日誌分析
- 2014-02-28T11:59:00.638+0800: 766.537:[GC2014-02-28T11:59:00.638+0800: 766.537:
- [ParNew: 1770882K->212916K(1835008K), 0.0834220 secs]
- 5240418K->3814487K(24903680K), 0.0837310 secs] [Times: user=1.12 sys=0.02, real=0.08 secs]
2014-02-28T11:59:00 ...(時間戳):[GC(Young GC)(時間戳):[ParNew(使用ParNew作爲年輕代的垃圾回收期):
1770882K(年輕代垃圾回收前的大小)->212916K(年輕代垃圾回收以後的大小)(1835008K)(年輕代的capacity), 0.0834220 secs(回收時間)]
5240418K(整個heap垃圾回收前的大小)->3814487K(整個heap垃圾回收後的大小)(24903680K)(heap的capacity), 0.0837310secs(回收時間)]
[Times: user=1.12(Young GC用戶耗時) sys=0.02(Young GC系統耗時), real=0.08 secs(Young GC實際耗時)]
其中 Young GC回收了1770882-212916=1557966K內存
Heap通過這次回收總共減少了 5240418-3814487=1425931 K的內存。1557966-1425931=132035K說明這次Young GC有約128M的內存被移動到了Old Gen,
提示:進代量(Young->Old)需要重點觀察,預防promotion failed.
3:老年代(CMS old gc)分析
- 2014-02-28T23:58:42.314+0800: 25789.661: [GC [1 CMS-initial-mark: 17303356K(23068672K)] 18642315K(24903680K), 1.0400410 secs] [Times: user=1.04 sys=0.00, real=1.04 secs]
- 2014-02-28T23:58:43.354+0800: 25790.701: [CMS-concurrent-mark-start]
- 2014-02-28T23:58:43.717+0800: 25791.064: [CMS-concurrent-mark: 0.315/0.363 secs] [Times: user=1.64 sys=0.02, real=0.37 secs]
- 2014-02-28T23:58:43.717+0800: 25791.064: [CMS-concurrent-preclean-start]
- 2014-02-28T23:58:43.907+0800: 25791.254: [CMS-concurrent-preclean: 0.181/0.190 secs] [Times: user=0.20 sys=0.01, real=0.19 secs]
- 2014-02-28T23:58:43.907+0800: 25791.254: [CMS-concurrent-abortable-preclean-start]
- CMS: abort preclean due to time 2014-02-28T23:58:49.082+0800: 25796.429: [CMS-concurrent-abortable-preclean: 5.165/5.174 secs] [Times: user=5.40 sys=0.04, real=5.17 secs]
- 2014-02-28T23:58:49.083+0800: 25796.430: [GC[YG occupancy: 1365142 K (1835008 K)]2014-02-28T23:58:49.083+0800: 25796.430: [Rescan (parallel) , 0.9690640 secs]2014-02-28T23:58:50.052+0800: 25797.399: [weak refs processing, 0.0006190 secs]2014-02-28T23:58:50.053+0800: 25797.400: [scrub string table, 0.0006290 secs] [1 CMS-remark: 17355150K(23068672K)] 18720292K(24903680K), 0.9706650 secs] [Times: user=16.49 sys=0.06, real=0.97 secs]
- 2014-02-28T23:58:50.054+0800: 25797.401: [CMS-concurrent-sweep-start]
- 2014-02-28T23:58:51.940+0800: 25799.287: [CMS-concurrent-sweep: 1.875/1.887 secs] [Times: user=2.03 sys=0.03, real=1.89 secs]
- 2014-02-28T23:58:51.941+0800: 25799.288: [CMS-concurrent-reset-start]
- 2014-02-28T23:58:52.067+0800: 25799.414: [CMS-concurrent-reset: 0.127/0.127 secs] [Times: user=0.13 sys=0.00, real=0.13 secs]
- 2014-03-01T00:00:36.293+0800: 25903.640: [GC2014-03-01T00:00:36.293+0800: 25903.640: [ParNew: 1805234K->226801K(1835008K), 0.1020510 secs] 10902912K->9434796K(24903680K), 0.1023150 secs] [Times: user=1.35 sys=0.02, real=0.10 secs]
- 2014-03-01T00:07:13.559+0800: 26300.906: [GC2014-03-01T00:07:13.559+0800: 26300.906: [ParNew: 1799665K->248991K(1835008K), 0.0876870 secs] 14086673K->12612462K(24903680K), 0.0879620 secs] [Times: user=1.24 sys=0.01, real=0.09 secs]
CMS的gc日誌分爲一下幾個步驟,重點關注initial-mark和remark這兩個階段,因爲這兩個階段會stop進程。
初始標記(init mark):收集根引用,這是一個stop-the-world階段。
併發標記(concurrent mark):這個階段可以和用戶應用併發進行。遍歷老年代的對象圖,標記出活着的對象。
併發預清理(concurrent preclean):這同樣是一個併發的階段。主要的用途也是用來標記,用來標記那些在前面標記之後,發生變化的引用。主要是爲了縮短remark階段的stop-the-world的時間。
重新標記(remark):這是一個stop-the-world的操作。暫停各個應用,統計那些在發生變化的標記。
併發清理(concurrent sweep):併發掃描整個老年代,回收一些在對象圖中不可達對象所佔用的空間。
併發重置(concurrent reset):重置某些數據結果,以備下一個回收週期
提示:紅色爲全部暫停階段重點關注.
- [GC [1 CMS-initial-mark: 17303356K(23068672K)] 18642315K(24903680K), 1.0400410 secs] [Times: user=1.04 sys=0.00, real=1.04 secs]
其中數字依表示標記前後old區的所有對象佔內存大小和old的capacity,整個JavaHeap(不包括perm)所有對象佔內存總的大小和JavaHeap的capacity。
- [GC[YG occupancy: 1365142 K (1835008 K)]2014-02-28T23:58:49.083+0800: 25796.430:
- [Rescan (parallel) , 0.9690640 secs]2014-02-28T23:58:50.052+0800: 25797.399:
- [weak refs processing, 0.0006190 secs]2014-02-28T23:58:50.053+0800: 25797.400: [scrub string table, 0.0006290 secs]
- [1 CMS-remark: 17355150K(23068672K)] 18720292K(24903680K), 0.9706650 secs] [Times: user=16.49 sys=0.06, real=0.97 secs]
Rescan (parallel)表示的是多線程處理young區和多線程掃描old+perm的總時間, parallel 表示多GC線程並行。
weak refs processing 處理old區的弱引用的總時間,用於回收native memory.等等
參考資料:
https://blogs.oracle.com/jonthecollector/entry/the_unspoken_cms_and_printgcdetails
https://blogs.oracle.com/poonam/entry/understanding_cms_gc_logs
4:老年代(CMS old GC ) concurrent mode failure日誌
- 2014-03-03T09:38:26.457+0800: 233373.804: [GC [1 CMS-initial-mark: 17319615K(23068672K)] 17351070K(24903680K), 0.0419440 secs]
- [Times: user=0.04 sys=0.00, real=0.04 secs]
- 2014-03-03T09:38:26.499+0800: 233373.846: [CMS-concurrent-mark-start]
- 2014-03-03T09:38:28.175+0800: 233375.522: [GC2014-03-03T09:38:28.175+0800: 233375.522: [CMS2014-03-03T09:38:28.887+0800: 233376.234:
- [CMS-concurrent-mark: 1.989/2.388 secs] [Times: user=14.37 sys=0.24, real=2.39 secs]
- (concurrent mode failure): 17473174K->8394653K(23068672K), 19.3309170 secs] 18319691K->8394653K(24903680K),
- [CMS Perm : 23157K->23154K(98304K)], 19.3311700 secs] [Times: user=22.18 sys=0.00, real=19.33 secs]
concurrent mode failure一般發生在CMS GC 運行過程中,老年代空間不足,引發MSC(Full GC)
上面的這條發日誌說明CMS運行到CMS-concurrent-mark過程中就出現空間不足,產生併發失敗(17319615K(23068672K)佔77%),
解決思路:降低YGC頻率,降低CMS GC觸發時機,適當降低CMSInitiatingOccupancyFraction.
5:新生代(ParNew YGC)promotion failed日誌
- 2014-02-27T21:19:42.460+0800: 210095.040: [GC 210095.040: [ParNew (promotion failed): 1887487K->1887488K(1887488K), 0.4818790 secs]210095.522: [CMS: 13706434K->7942818K(23068672K), 9.7152990 secs] 15358303K->7942818K(24956160K), [CMS Perm : 27424K->27373K(98304K)], 10.1974110 secs] [Times: user=12.06 sys=0.01, real=10.20 secs]
promotion failed一般發生在新生代晉升老年代時,老年代空間不夠或連續空間不夠卻還沒達到old區的觸發值,引發Full Gc.
解決思路:由於heap碎片,YGC晉升對象過大,過長.(mid/long Time Object),調整-XX:PretenureSizeThreshold=65535,-XX:MaxTenuringThreshold=6
6:system.gc()產生的Full GC日誌
- <strong>2014-01-21T17:44:01.554+0800: 50212.568: [Full GC (System) 50212.568:
- [CMS: 943772K220K(2596864K), 2.3424070 secs] 1477000K->220K(4061184K), [CMS Perm : 3361K->3361K(98304K)], 2.3425410 secs] [Times: user=2.33 sys=0.01, real=2.34 secs]</strong>
Full GC (System)意味着這是個system.gc調用產生的MSC。
“943772K->220K(2596864K), 2.3424070 secs”表示:這次MSC前後old區內總對象大小,old的capacity及這次MSC耗時。
“1477000K->220K(4061184K)”表示:這次MSC前後JavaHeap內總對象大小,JavaHeap的capacity。
“3361K->3361K(98304K)], 2.3425410 secs”表示:這次MSC前後Perm區內總對象大小,Perm區的capacity。
解決:
使用-XX:+DisableExplicitGC參數,System.gc()會變成空調用.
如果應用有地方大量使用direct memory 或 rmi,那麼使用-XX:+DisableExplicitGC要小心。
可以使用-XX:+ExplicitGCInvokesConcurrent替換把 System.gc()從Full GC換成CMS GC.
原因:
DirectByteBuffer沒有finalizer,native memory的清理工作是通過sun.misc.Cleaner自動完成
sun.misc.Cleaner是基於PhantomReference的清理工具,Full GC/Old GC會對old gen做reference processing,同時觸發Cleaner對已死的DirectByteBuffer對象做清理。
如果長時間沒有GC或者只做了young GC的話,不會觸發old區Cleaner的工作,容易產生DirectMemory OOM.
參考:https://gist.github.com/rednaxelafx/1614952
RMI會做的是分佈式GC。Sun JDK的分佈式GC是用純Java實現的,爲RMI服務。
參考:http://docs.oracle.com/javase/6/docs/technotes/guides/rmi/sunrmiproperties.html
7:特殊的Full GC日誌,根據動態計算直接進行MSC
- 2014-02-13T13:48:06.349+0800: 7.092: [GC 7.092: [ParNew: 471872K->471872K(471872K), 0.0000420 secs]7.092: [CMS: 366666K->524287K(524288K), 27.0023450 secs] 838538K->829914K(996160K), [CMS Perm : 3196K->3195K(131072K)], 27.0025170 secs]
ParNew的時間特別短,jvm在minor gc前會首先確認old是不是足夠大,如果不夠大,這次young gc直接返回,進行MSC(Full GC)。
二:參數配置和理解
1:參數分類和說明
jvm參數分固定參數和非固定參數
1):固定參數
如:-Xmx,-Xms,-Xmn,-Xss.
2):非固定參數
如:
-XX:+<option> 啓用選項
-XX:-<option> 不啓用選項
-XX:<option>=<number> 給選項設置一個數字類型值,可跟單位,例如 128k, 2g
-XX:<option>=<string> 給選項設置一個字符串值,例如-XX:HeapDumpPath=./dump.log
2:JVM可設置參數和默認值
1):-XX:+PrintCommandLineFlags
打印出JVM初始化完畢後所有跟最初的默認值不同的參數及它們的值,jdk1.5後支持.
線上建議打開,可以看到自己改了哪些值.
2):-XX:+PrintFlagsFinal
顯示所有可設置的參數及"參數處理"後的默認值。參數本身只從JDK6 U21後支持
可是查看不同版本默認值,以及是否設置成功.輸出的信息中"="表示使用的是初始默認值,而":="表示使用的不是初始默認值
如:jdk6/7 -XX:+MaxTenuringThreshold 默認值都是15,但是在使用CMS收集器後,jdk6默認4 , jdk7默認6.
- [hbase96 logs]# java -version
- java version "1.6.0_27-ea"
- [hbase96 logs]# java -XX:+PrintFlagsInitial | grep MaxTenuringThreshold
- intx MaxTenuringThreshold = 15 {product}
- [hbase96 logs]# java -XX:+PrintFlagsFinal -XX:+UseConcMarkSweepGC | grep MaxTenuringThreshold
- intx MaxTenuringThreshold := 4 {product}
- [zw-34-71 logs]# java -version
- java version "1.7.0_45"
- [zw-34-71 logs]# java -XX:+PrintFlagsInitial | grep MaxTenuringThreshold
- intx MaxTenuringThreshold = 15 {product}
- [zw-34-71 logs]# java -XX:+PrintFlagsFinal -XX:+UseConcMarkSweepGC | grep MaxTenuringThreshold
- intx MaxTenuringThreshold := 6 {product}
3):-XX:+PrintFlagsInitial
在"參數處理"之前所有可設置的參數及它們的值,然後直接退出程序.
這裏的"參數處理"指: 檢查參數之間是否有衝突,通過ergonomics調整某些參數的值等.
- [hbase96 logs]# java -version
- java version "1.6.0_27-ea"
- [hbase96 logs]# java -XX:+PrintFlagsInitial | grep UseCompressedOops
- bool UseCompressedOops = false {lp64_product}
- [hbase96 logs]# java -XX:+PrintFlagsFinal | grep UseCompressedOops
- bool UseCompressedOops := true {lp64_product}
4)CMSInitiatingOccupancyFraction 默認值是多少
jdk6/7:
- #java -server -XX:+UseConcMarkSweepGC -XX:+PrintFlagsFinal | grep -P "CMSInitiatingOccupancyFraction|CMSTriggerRatio|MinHeapFreeRatio"
- intx CMSInitiatingOccupancyFraction = -1 {product}
- intx CMSTriggerRatio = 80 {product}
- uintx MinHeapFreeRatio = 40 {product}
計算公式:
CMSInitiatingOccupancyFraction = (100 - MinHeapFreeRatio) + (CMSTriggerRatio * MinHeapFreeRatio / 100)
最終結果: 在jdk6/7中 CMSInitiatingOccupancyFraction默認值是92% .不是網上傳的68%; 都這麼傳,是因爲 "深入理解Java虛擬機"一書中是68%,但它用的是jdk5 , jdk5的CMSTriggerRatio默認值是20,坑爹...
三:JVM內存區域理解和相關參數
一圖勝千言,直接上圖
1):物理分代圖.
物理分代是除G1之外的JVM 內存分配方式,jvm 通過-Xmx,-Xmn/newRatio等參數將jvm heap劃分成物理固定大小,對於不同場景比例應該設置成多少很考驗經驗.
一篇JVM CMS優化講解的非常好的文章: how-tame-java-gc-pauses
2) 邏輯分代圖(G1)
邏輯分代是以後的趨勢(PS:jkd8連perm都不區分了。), 不需要使用者在糾結Xms/Xmn,SurvivorRatio等比例問題,採用動態算法調整分代大小。
本文轉自:http://hot66hot.iteye.com/blog/2075819