1. 程式人生 > >聊聊JVM(一)相對全面的GC總結(轉)

聊聊JVM(一)相對全面的GC總結(轉)

cor war 性能 依靠 blank 知識 flags 要去 內存空間

轉至:http://blog.csdn.net/iter_zc/article/details/41746265

最近時間比較緊張,要寫的東西也有很多,只能想到一點寫一點。關於GC,網上的資料太多,之前對一個系統調優的時候又回顧了一下,找了幾篇廣泛流傳的資料,大部分都是大同小異,這裏總個總結,希望能夠做個相對的全集,並寫出一些新的點,比如Card Marking(卡片標記)等。

首先是大家都要提到的GC的基礎算法:標記清除,標記整理,復制,分代。這些算法的第一步都是做的一件事: 標記(Mark)。

JVM的標記算法采用了根搜索算法(Root Tracing)。根有幾種:

1. JVM棧的Frame裏面的引用

2. 靜態類,常量的引用

3. 本地棧中的引用

4. 本地方法的引用

一般我們能控制的就是JVM棧中的引用和靜態類,常量的引用。標記也分為幾個階段,比如

1. 標記直接和根引用的對象

2. 標記間接和根引用的對象

3. 由於分代算法,被老年代對象所引用的新生代的對象

對於第三種,JVM采用了Card Marking(卡片標記)的方法,避免了在做Minor GC時需要對整個老年代掃描。具體的方法如下:

1. 將老年代的內存分片,1個片默認是512byte

2. 如果老年代的對象發生了修改,就把這個老年代對象所在的片標記為臟 dirty。或者老年代對象指向了新生代對象,那麽它所在的片也會被標記為dirty

3. 沒有標記為臟的老年代片它沒有指向新的新生代對象,所以可以不需要去掃描

4. Minor GC掃描老年代空間時,只需要去掃描臟的卡片的對象,不需要掃描整個老年代空間

技術分享

技術分享

所以做Minor GC時標記的時間 = T(stack_scan) + T(card_scan) + T(old_root_scan).

T(stack_scan): 級聯掃描在JVM棧裏的根的時間

T(card_scan): 級聯掃描卡表中臟卡片的時間

T(old_root_scan): 掃描在老年代中的直接的根的時間。註意是直接的根,不會去級聯掃描老年代的對象。因為掃描都是從根開始的,一開始不知道根到底是在老年代還是新生代

和Card Marking相關的一個重要的JVM參數是-XX:UseCondCardMark 。使用這個參數的原因是在高並發的情況下,Card標記為臟的操作本身就存在著競爭,使用這個參數可以避免卡片被重復標記為臟,從而提高性能。

說完了標記,下面提一下幾種基礎的GC算法,沒有什麽新的點,直接引用網上的圖

標記-清除算法

技術分享

復制算法

技術分享

標記--整理算法

技術分享

分代算法是將對象分為新生代和老年代,然後使用不同的GC策略來進行回收,提高整體的效率。

由於新生代的大部分對象都會在一次Minor GC中死亡,存活的對象很少,所以新生代的GC收集器都采用了復制算法。新生代分為Eden + S0 + S1. S0和S1就是用來實現復制的,在任何一次Minor GC後,S0和S1總是只有一個區域有數據,另一個區域為空,以便於下一次復制使用

當新生代空間不能滿足大對象分配時,老年代空間為它提供了分配擔保,大對象可以直接進入老年代。有兩個JVM參數可以控制新生代進入老年代的門檻:

PretenureSizeThreshold: 單位是B,設置了對象大小的閥值

MaxTenuringThreshold: 設置了進入老年代的年齡的閥值

老年代對象一般都是存活時間久,老年代的空間本來就大,所以沒有更多空間來提供分配擔保,所以老年代一般采用標記--清理或者標記--整理算法。

下面這張圖很好地介紹了JDK6的各種GC收集器以及各自的特點:

技術分享

1. 新生代都采用復制算法

2. CMS采用了標記--清除算法,由於標記清除算法會生成內存碎片,所以JVM提供了參數來使CMS可以在幾次清除後作一次整理

-XX:CMSFullGCsBeforeCompaction:由於並發收集器不對內存空間進行壓縮、整理,所以運行一段時間以後會產生“碎片”,使得運行效率降低。此值設置運行多少次GC以後對內存空間進行壓縮、整理。
-XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮。可能會影響性能,但是可以消除碎片

3. Serial Old(MSC)和Parallel Old都采用標記整理算法

4. UseSerialGC默認會在新生代使用Serial收集器,在老年代使用Serial Old收集器,這兩個都是單線程的收集器

5. UseConcMarkSweepGC默認會再新生代使用ParNew收集器,這是個並發的收集器。在老年代會使用CMS + Serial Old收集器,當CMS失敗的時候,會啟用Serial Old做FULL GC

6. UseParallelOldGC默認會在新生的使用Parallel Scavenge收集器,在老年代使用Parallel Old收集器。這兩個收集器都是吞吐量優先,所謂吞吐量優先就是它可以嚴格控制GC的時間,從而保證吞吐量。但是吞吐量提高了,新生代和老年代的空間就是動態調整的,而不是按照初始配置的大小。因為單位時間清除的垃圾量近乎一個常量,既然要保證時間,那麽必須保證垃圾總量,而垃圾總量可以通過新生代和老年代的大小來控制的

7. 對於和用戶有交互的應用,比如Web應用,一個重要的考量是系統的響應時間,要保證系統的響應時間就要保證由GC導致的stop the world次數少,或者讓用戶線程和GC線程一起運行。所以Web應用是使用CMS收集器的一個重要場景。CMS減少了stop the world的次數,不可避免地讓整體GC的時間拉長了

8. 對於計算密集型的應用可能會考慮計算的吞吐量,這時候可以使用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的日誌信息做了說明:

技術分享

關於JVM調優的各種參數設置,網上一抓一大把,這裏不多說了。有一個調優的整體的原則:

1. 先做一個JVM的性能測試,了解當前的狀態

2. 明確調優的目標,比如減少FULL GC的次數,減少GC的總時間,提高吞吐量等

3. 調整參數後再進行多次的測試,分析,最終達到一個較為理想的狀態。各種參數要根據系統的自身情況來確定,沒有統一的解決方案

將各種工具的文章頁很多,這裏從解決問題的角度出發列出幾個。

查看JVM啟動參數

1. jps -v

2. jinfo -flags pid

3. jinfo pid -- 列出JVM啟動參數和system.properties

4. ps -ef | grep Java

查看當前堆的配置

1. jstat -gc pid 1000 3 -- 列出堆的各個區域的大小

2. jstat -gcutil pid 1000 3 -- 列出堆的各個區域使用的比例

3. jmap -heap pid -- 列出當前使用的GC算法,堆的各個區域大小

查看線程的堆棧信息

1. jstack -l pid

dump堆內的對象

1. jmap -dump:live,format=b,file=xxx pid

2. -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=xxx -- 設置JVM參數,當JVM OOM時輸出堆的dump

3. ulimit -c unlimited -- 設置Linux ulimit參數,可以產生coredump且不受大小限制。之前在線上遇到過一個極其詭異的問題,JVM整個進程突然掛了,這時候依靠JVM本身生成dump文件已經不行了,只有依賴linux,讓系統來生成進程掛掉的core dump文件

使用jstack 可以來獲得這個coredump的線程堆棧信息: jstack "$JAVA_HOME/bin/java" core.xxx > core.log

獲得當前系統占用CPU最高的10個進程,線程

ps Hh -eo pid,tid,pcpu,pmem | sort -nk3 |tail > temp.txt

圖形化界面
1. jvisualvm 裏面有很多插件,比如Visual GC,可以可視化地看到各個堆區域時候的狀態,從而可以對整體GC的性能有整體的認識


就說到這吧,有遺漏的後面再補充

參考資料: Understanding GC pauses in JVM, HotSpot‘s minor GC.

《深入理解JVM》

《Think in GC》

聊聊JVM(一)相對全面的GC總結(轉)