gcview對gc文件的分析教程

gc垃圾回收可是個有優(yōu)化空間的一個重要的工作,下面來介紹一個常用的gc文件分析工具,可以讓jvm調(diào)優(yōu)可視化。老王還是遵循一貫作風(fēng),不廢話,直接開整!

想分析GC文件,首先要設(shè)置項目啟動時的gc日志的配置

java進(jìn)程的啟動腳本如下

nohup java -jar -server -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/project/log/dump/dump.hprof -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/opt/doc-mgr/log/dump/heap_trace.txt -Dspring.profiles.active=prod /opt/project/你的項目jar名字.jar > /dev/null 2>&1 &
這里使用的是G1垃圾回收器,做了個簡單的配置

項目啟動后,可以簡單做個項目的信息查看

jps獲取進(jìn)程id


然后用 jmap 來看看具體的信息

jmap -heap 1273315





可以看出我的項目還是很健康的,只是不直觀,還是需要用個可視化的工具進(jìn)行分心下比較直觀

讓項目運行一段時間后,就會有g(shù)c文件生成,然后下載下來,用gcview來分析即可

gcview的下載和使用
gcview下載地址:https://github.com/chewiebug/GCViewer ,我這里下載的是 gcviewer-1.36.jar

在gcviewer-1.36.jar文件夾路徑下編寫一個啟動腳本來啟動這個jar,就會彈出操作界面

啟動腳本start.bat

java -jar gcviewer-1.36.jar
雙擊start.bat就可以啟動



這就是啟動界面,然后把下載的gc日志拖拽到這個界面中即可


我們先了解一下各個顏色代表的含義:



下面是gcview的參數(shù)介紹和使用的說明,這里以一個大佬的分析數(shù)據(jù)來展開介紹,我這個項目沒什么人訪問,就沒什么看點

使用GCViewer調(diào)優(yōu)GC
在對 GC 調(diào)優(yōu)的過程中,我們不僅需要知道 GC 的原理,更重要的是要熟練使用各種監(jiān)控和分析工具,具備 GC 調(diào)優(yōu)的實戰(zhàn)能力。CMS 和 G1 是時下使用率比較高的兩款垃圾收集器,從 Java 9 開始,采用 G1 作為默認(rèn)垃圾收集器,而 G1 的目標(biāo)也是逐步取代 CMS。所以今天我們先來簡單回顧一下兩種垃圾收集器 CMS 和 G1 的區(qū)別,接著通過一個例子幫你提高 GC 調(diào)優(yōu)的實戰(zhàn)能力。

CMS VS G1
CMS 收集器將 Java 堆分為年輕代(Young)或年老代(Old)。這主要是因為有研究表明,超過 90%的對象在第一次 GC 時就被回收掉,但是少數(shù)對象往往會存活較長的時間。

CMS 還將年輕代內(nèi)存空間分為幸存者空間(Survivor)和伊甸園空間(Eden)。新的對象始終在 Eden 空間上創(chuàng)建。一旦一個對象在一次垃圾收集后還幸存,就會被移動到幸存者空間。當(dāng)一個對象在多次垃圾收集之后還存活時,它會移動到年老代。這樣做的目的是在年輕代和年老代采用不同的收集算法,以達(dá)到較高的收集效率,比如在年輕代采用復(fù)制 - 整理算法,在年老代采用標(biāo)記 - 清理算法。因此 CMS 將 Java 堆分成如下區(qū)域:



與 CMS 相比,G1 收集器有兩大特點:

G1 可以并發(fā)完成大部分 GC 的工作,這期間不會“Stop-The-World”。

G1 使用非連續(xù)空間,這使 G1 能夠有效地處理非常大的堆。此外,G1 可以同時收集年輕代和年老代。G1 并沒有將 Java 堆分成三個空間(Eden、Survivor 和 Old),而是將堆分成許多(通常是幾百個)非常小的區(qū)域。這些區(qū)域是固定大小的(默認(rèn)情況下大約為 2MB)。每個區(qū)域都分配給一個空間。G1 收集器的 Java 堆如下圖所示:



圖上的 U 表示“未分配”區(qū)域。G1 將堆拆分成小的區(qū)域,一個最大的好處是可以做局部區(qū)域的垃圾回收,而不需要每次都回收整個區(qū)域比如年輕代和年老代,這樣回收的停頓時間會比較短。具體的收集過程是:
將所有存活的對象將從收集的區(qū)域復(fù)制到未分配的區(qū)域,比如收集的區(qū)域是 Eden 空間,把 Eden 中的存活對象復(fù)制到未分配區(qū)域,這個未分配區(qū)域就成了 Survivor 空間。理想情況下,如果一個區(qū)域全是垃圾(意味著一個存活的對象都沒有),則可以直接將該區(qū)域聲明為“未分配”。

為了優(yōu)化收集時間,G1 總是優(yōu)先選擇垃圾最多的區(qū)域,從而最大限度地減少后續(xù)分配和釋放堆空間所需的工作量。這也是 G1 收集器名字的由來——Garbage-First。






GC 調(diào)優(yōu)原則
GC 是有代價的,因此我們調(diào)優(yōu)的根本原則是每一次 GC 都回收盡可能多的對象,也就是減少無用功。因此我們在做具體調(diào)優(yōu)的時候,針對 CMS 和 G1 兩種垃圾收集器,分別有一些相應(yīng)的策略。

CMS收集器
對于 CMS 收集器來說,最重要的是合理地設(shè)置年輕代和年老代的大小。年輕代太小的話,會導(dǎo)致頻繁的 Minor GC,并且很有可能存活期短的對象也不能被回收,GC 的效率就不高。而年老代太小的話,容納不下從年輕代過來的新對象,會頻繁觸發(fā)單線程 Full GC,導(dǎo)致較長時間的 GC 暫停,影響 Web 應(yīng)用的響應(yīng)時間。

G1收集器
對于 G1 收集器來說,我不推薦直接設(shè)置年輕代的大小,這一點跟 CMS 收集器不一樣,這是因為 G1 收集器會根據(jù)算法動態(tài)決定年輕代和年老代的大小。因此對于 G1 收集器,我們需要關(guān)心的是 Java 堆的總大?。?Xmx)。

此外 G1 還有一個較關(guān)鍵的參數(shù)是-XX:MaxGCPauseMillis = n,這個參數(shù)是用來限制最大的 GC 暫停時間,目的是盡量不影響請求處理的響應(yīng)時間。G1 將根據(jù)先前收集的信息以及檢測到的垃圾量,估計它可以立即收集的最大區(qū)域數(shù)量,從而盡量保證 GC 時間不會超出這個限制。因此 G1 相對來說更加“智能”,使用起來更加簡單。

內(nèi)存調(diào)優(yōu)實戰(zhàn)
下面我通過一個例子實戰(zhàn)一下 Java 堆設(shè)置得過小,導(dǎo)致頻繁的 GC,我們將通過 GC 日志分析工具來觀察 GC 活動并定位問題。

首先我們建立一個 Spring Boot 程序,作為我們的調(diào)優(yōu)對象,代碼如下:

@RestController
public class GcTestController {

    private Queue<Greeting> objCache =  new ConcurrentLinkedDeque<>();

    @RequestMapping("/greeting")
    public Greeting greeting() {
        Greeting greeting = new Greeting("Hello World!");

        if (objCache.size() >= 200000) {
            objCache.clear();
        } else {
            objCache.add(greeting);
        }
        return greeting;
    }
}

@Data
@AllArgsConstructor
class Greeting {
   private String message;
}
上面的代碼就是創(chuàng)建了一個對象池,當(dāng)對象池中的對象數(shù)到達(dá) 200000 時才清空一次,用來模擬年老代對象。

用下面的命令啟動測試程序:

java -Xmx32m -Xss256k -verbosegc -Xlog:gc*,gc+ref=debug,gc+heap=debug,gc+age=trace:file=gc-%p-%t.log:tags,uptime,time,level:filecount=2,filesize=100m -jar target/demo-0.0.1-SNAPSHOT.jar
我給程序設(shè)置的堆的大小為 32MB,目的是能讓我們看到 Full GC。除此之外,我還打開了 verbosegc 日志,請注意這里我使用的版本是 Java 12,默認(rèn)的垃圾收集器是 G1。

2. 使用 JMeter 壓測工具向程序發(fā)送測試請求,訪問的路徑是/greeting。

3. 使用 GCViewer 工具打開 GC 日志,我們可以看到這樣的圖:



圖中上部的藍(lán)線表示已使用堆的大小,我們看到它周期的上下震蕩,這是我們的對象池要擴(kuò)展到 200000 才會清空。

圖底部的綠線表示年輕代 GC 活動,從圖上看到當(dāng)堆的使用率上去了,會觸發(fā)頻繁的 GC 活動。

圖中的豎線表示 Full GC,從圖上看到,伴隨著 Full GC,藍(lán)線會下降,這說明 Full GC 收集了年老代中的對象。

基于上面的分析,我們可以得出一個結(jié)論,那就是 Java 堆的大小不夠。我來解釋一下為什么得出這個結(jié)論:

GC 活動頻繁:年輕代 GC(綠色線)和年老代 GC(黑色線)都比較密集。這說明內(nèi)存空間不夠,也就是 Java 堆的大小不夠。

Java 的堆中對象在 GC 之后能夠被回收,說明不是內(nèi)存泄漏。
我們通過 GCViewer 還發(fā)現(xiàn)累計 GC 暫停時間有 55.57 秒,如下圖所示:



因此我們的解決方案是調(diào)大 Java 堆的大小,像下面這樣:

java -Xmx2048m -Xss256k -verbosegc -Xlog:gc*,gc+ref=debug,gc+heap=debug,gc+age=trace:file=gc-%p-%t.log:tags,uptime,time,level:filecount=2,filesize=100m -jar target/demo-0.0.1-SNAPSHOT.jar
生成的新的 GC log 分析圖如下:



你可以看到,沒有發(fā)生 Full GC,并且年輕代 GC 也沒有那么頻繁了,并且累計 GC 暫停時間只有 3.05 秒。



總結(jié)
對于 CMS 來說,我們要合理設(shè)置年輕代和年老代的大小。你可能會問該如何確定它們的大小呢?這是一個迭代的過程,可以先采用 JVM 的默認(rèn)值,然后通過壓測分析 GC 日志。

如果我們看年輕代的內(nèi)存使用率處在高位,導(dǎo)致頻繁的 Minor GC,而頻繁 GC 的效率又不高,說明對象沒那么快能被回收,這時年輕代可以適當(dāng)調(diào)大一點。

如果我們看年老代的內(nèi)存使用率處在高位,導(dǎo)致頻繁的 Full GC,這樣分兩種情況:如果每次 Full GC 后年老代的內(nèi)存占用率沒有下來,可以懷疑是內(nèi)存泄漏;如果 Full GC 后年老代的內(nèi)存占用率下來了,說明不是內(nèi)存泄漏,我們要考慮調(diào)大年老代。

對于 G1 收集器來說,我們可以適當(dāng)調(diào)大 Java 堆,因為 G1 收集器采用了局部區(qū)域收集策略,單次垃圾收集的時間可控,可以管理較大的 Java 堆。









作者:老王

歡迎關(guān)注微信公眾號 : IT學(xué)習(xí)道場