最近時間比較緊張,要寫的東西也有很多,只能想到一點寫一點。關(guān)于GC,網(wǎng)上的資料太多,之前對一個系統(tǒng)調(diào)優(yōu)的時候又回顧了一下,找了幾篇廣泛流傳的資料,大部分都是大同小異,這里總個總結(jié),希望能夠做個相對的全集,并寫出一些新的點,比如Card Marking(卡片標記)等。
首先是大家都要提到的GC的基礎(chǔ)算法:標記清除,標記整理,復(fù)制,分代。這些算法的第一步都是做的一件事: 標記(Mark)。
JVM的標記算法采用了根搜索算法(Root Tracing)。根有幾種:
1. JVM棧的Frame里面的引用
2. 靜態(tài)類,常量的引用
3. 本地棧中的引用
4. 本地方法的引用
一般我們能控制的就是JVM棧中的引用和靜態(tài)類,常量的引用。標記也分為幾個階段,比如
1. 標記直接和根引用的對象
2. 標記間接和根引用的對象
3. 由于分代算法,被老年代對象所引用的新生代的對象
對于第三種,JVM采用了Card Marking(卡片標記)的方法,避免了在做Minor GC時需要對整個老年代掃描。具體的方法如下:
1. 將老年代的內(nèi)存分片,1個片默認是512byte
2. 如果老年代的對象發(fā)生了修改,就把這個老年代對象所在的片標記為臟 dirty。或者老年代對象指向了新生代對象,那么它所在的片也會被標記為dirty
3. 沒有標記為臟的老年代片它沒有指向新的新生代對象,所以可以不需要去掃描
4. Minor GC掃描老年代空間時,只需要去掃描臟的卡片的對象,不需要掃描整個老年代空間
所以做Minor GC時標記的時間 = T(stack_scan) + T(card_scan) + T(old_root_scan).
T(stack_scan): 級聯(lián)掃描在JVM棧里的根的時間
T(card_scan): 級聯(lián)掃描卡表中臟卡片的時間
T(old_root_scan): 掃描在老年代中的直接的根的時間。注意是直接的根,不會去級聯(lián)掃描老年代的對象。因為掃描都是從根開始的,一開始不知道根到底是在老年代還是新生代
和Card Marking相關(guān)的一個重要的JVM參數(shù)是-XX:UseCondCardMark 。使用這個參數(shù)的原因是在高并發(fā)的情況下,Card標記為臟的操作本身就存在著競爭,使用這個參數(shù)可以避免卡片被重復(fù)標記為臟,從而提高性能。
說完了標記,下面提一下幾種基礎(chǔ)的GC算法,沒有什么新的點,直接引用網(wǎng)上的圖
標記-清除算法
復(fù)制算法
標記--整理算法
分代算法是將對象分為新生代和老年代,然后使用不同的GC策略來進行回收,提高整體的效率。
由于新生代的大部分對象都會在一次Minor GC中死亡,存活的對象很少,所以新生代的GC收集器都采用了復(fù)制算法。新生代分為Eden + S0 + S1. S0和S1就是用來實現(xiàn)復(fù)制的,在任何一次Minor GC后,S0和S1總是只有一個區(qū)域有數(shù)據(jù),另一個區(qū)域為空,以便于下一次復(fù)制使用
當新生代空間不能滿足大對象分配時,老年代空間為它提供了分配擔保,大對象可以直接進入老年代。有兩個JVM參數(shù)可以控制新生代進入老年代的門檻:
PretenureSizeThreshold: 單位是B,設(shè)置了對象大小的閥值
MaxTenuringThreshold: 設(shè)置了進入老年代的年齡的閥值
老年代對象一般都是存活時間久,老年代的空間本來就大,所以沒有更多空間來提供分配擔保,所以老年代一般采用標記--清理或者標記--整理算法。
下面這張圖很好地介紹了JDK6的各種GC收集器以及各自的特點:
1. 新生代都采用復(fù)制算法
2. CMS采用了標記--清除算法,由于標記清除算法會生成內(nèi)存碎片,所以JVM提供了參數(shù)來使CMS可以在幾次清除后作一次整理
-XX:CMSFullGCsBeforeCompaction:由于并發(fā)收集器不對內(nèi)存空間進行壓縮、整理,所以運行一段時間以后會產(chǎn)生“碎片”,使得運行效率降低。此值設(shè)置運行多少次GC以后對內(nèi)存空間進行壓縮、整理。
-XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮??赡軙绊懶阅?,但是可以消除碎片
3. Serial Old(MSC)和Parallel Old都采用標記整理算法
4. UseSerialGC默認會在新生代使用Serial收集器,在老年代使用Serial Old收集器,這兩個都是單線程的收集器
5. UseConcMarkSweepGC默認會再新生代使用ParNew收集器,這是個并發(fā)的收集器。在老年代會使用CMS + Serial Old收集器,當CMS失敗的時候,會啟用Serial Old做FULL GC
6. UseParallelOldGC默認會在新生的使用Parallel Scavenge收集器,在老年代使用Parallel Old收集器。這兩個收集器都是吞吐量優(yōu)先,所謂吞吐量優(yōu)先就是它可以嚴格控制GC的時間,從而保證吞吐量。但是吞吐量提高了,新生代和老年代的空間就是動態(tài)調(diào)整的,而不是按照初始配置的大小。因為單位時間清除的垃圾量近乎一個常量,既然要保證時間,那么必須保證垃圾總量,而垃圾總量可以通過新生代和老年代的大小來控制的
7. 對于和用戶有交互的應(yīng)用,比如Web應(yīng)用,一個重要的考量是系統(tǒng)的響應(yīng)時間,要保證系統(tǒng)的響應(yīng)時間就要保證由GC導(dǎo)致的stop the world次數(shù)少,或者讓用戶線程和GC線程一起運行。所以Web應(yīng)用是使用CMS收集器的一個重要場景。CMS減少了stop the world的次數(shù),不可避免地讓整體GC的時間拉長了。
8. 對于計算密集型的應(yīng)用可能會考慮計算的吞吐量,這時候可以使用Parallel Scavenge收集器來保證吞吐量
9. Serial, ParNew, Parallel Scanvange, Parallel Old, Serial Old全程都會Stop the world,JVM這時候只運行GC線程,不運行用戶線程
10. CMS主要分為 initial Mark, Concurrent Mark, ReMark, Concurrent Sweep等階段,initial Mark和Remark占整體的時間比較較小,它們會Stop the world. Concurrent Mark和Concurrent Sweep會和用戶線程一起運行。
下面這張圖對GC的日志信息做了說明:
關(guān)于JVM調(diào)優(yōu)的各種參數(shù)設(shè)置,網(wǎng)上一抓一大把,這里不多說了。有一個調(diào)優(yōu)的整體的原則:
1. 先做一個JVM的性能測試,了解當前的狀態(tài)
2. 明確調(diào)優(yōu)的目標,比如減少FULL GC的次數(shù),減少GC的總時間,提高吞吐量等
3. 調(diào)整參數(shù)后再進行多次的測試,分析,最終達到一個較為理想的狀態(tài)。各種參數(shù)要根據(jù)系統(tǒng)的自身情況來確定,沒有統(tǒng)一的解決方案
將各種工具的文章頁很多,這里從解決問題的角度出發(fā)列出幾個。
查看JVM啟動參數(shù)
1. jps -v
2. jinfo -flags pid
3. jinfo pid -- 列出JVM啟動參數(shù)和system.properties
4. ps -ef | grep java
查看當前堆的配置
1. jstat -gc pid 1000 3 -- 列出堆的各個區(qū)域的大小
2. jstat -gcutil pid 1000 3 -- 列出堆的各個區(qū)域使用的比例
3. jmap -heap pid -- 列出當前使用的GC算法,堆的各個區(qū)域大小
查看線程的堆棧信息
1. jstack -l pid
dump堆內(nèi)的對象
1. jmap -dump:live,format=b,file=xxx pid
2. -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=xxx -- 設(shè)置JVM參數(shù),當JVM OOM時輸出堆的dump
3. ulimit -c unlimited -- 設(shè)置Linux ulimit參數(shù),可以產(chǎn)生coredump且不受大小限制。之前在線上遇到過一個極其詭異的問題,JVM整個進程突然掛了,這時候依靠JVM本身生成dump文件已經(jīng)不行了,只有依賴Linux,讓系統(tǒng)來生成進程掛掉的core dump文件
使用jstack 可以來獲得這個coredump的線程堆棧信息: jstack "$JAVA_HOME/bin/java" core.xxx > core.log
獲得當前系統(tǒng)占用CPU最高的10個進程,線程
ps Hh -eo pid,tid,pcpu,pmem | sort -nk3 |tail > temp.txt
圖形化界面
1. jvisualvm 里面有很多插件,比如Visual GC,可以可視化地看到各個堆區(qū)域時候的狀態(tài),從而可以對整體GC的性能有整體的認識
就說到這吧,有遺漏的后面再補充
參考資料: Understanding GC pauses in JVM, HotSpot's minor GC.
《深入理解JVM》
《Think in GC》