JVM-技術專題-GCViewer調優GC

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"使用GCViewer調優GC"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 在對 GC 調優的過程中,我們不僅需要知道 GC 的原理,更重要的是要熟練使用各種監控和分析工具,具備 GC 調優的實戰能力。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "},{"type":"text","marks":[{"type":"strong"}],"text":"CMS 和 G1 是時下使用率比較高的兩款垃圾收集器"},{"type":"text","text":","},{"type":"text","marks":[{"type":"strong"}],"text":"從 Java 9 開始"},{"type":"text","text":",採用 G1 作爲默認垃圾收集器,而 G1 的目標也是逐步取代 CMS。所以今天我們先來簡單回顧一下兩種垃圾收集器 CMS 和 G1 的區別,接着通過一個例子幫你提高 GC 調優的實戰能力。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"CMS VS G1"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":" CMS 收集器將 Java 堆分爲年輕代(Young)或年老代(Old)。這主要是因爲有研究表明,超過 90%的對象在第一次 GC 時就被回收掉,但是少數對象往往會存活較長的時間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "},{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}},{"type":"strong"}],"text":"CMS 還將年輕代內存空間分爲倖存者空間(Survivor)和伊甸園空間(Eden)。新的對象始終在 Eden 空間上創建。"},{"type":"text","marks":[{"type":"strong"}],"text":"一旦一個對象在一次垃圾收集後還倖存,就會被移動到倖存者空間"},{"type":"text","text":"。當一個對象在多次垃圾收集之後還存活時,它會移動到年老代。"},{"type":"text","marks":[{"type":"strong"}],"text":"這樣做的目的是在年輕代和年老代採用不同的收集算法"},{"type":"text","text":",以達到較高的收集效率,比"},{"type":"text","marks":[{"type":"strong"}],"text":"如在年輕代採用複製 - 整理算法"},{"type":"text","text":",在年老代採用"},{"type":"text","marks":[{"type":"strong"}],"text":"標記 - 清理算法"},{"type":"text","text":"。因此 CMS 將 Java 堆分成如下區域:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/62/622d280d3c4f912744ce08f0a12dde5f.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"與 CMS 相比,G1 收集器有兩大特點:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}},{"type":"strong"}],"text":"G1 可以併發完成大部分 GC 的工作,這期間不會“Stop-The-World”。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#FF827B","name":"pink"}},{"type":"strong"}],"text":"G1 使用非連續空間,這使 G1 能夠有效地處理非常大的堆。此外,G1 可以同時收集年輕代和年老代。"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#9254DE","name":"purple"}},{"type":"strong"}],"text":"G1 並沒有將 Java 堆分成三個空間(Eden、Survivor 和 Old),而是將堆分成許多(通常是幾百個)非常小的區域。這些區域是固定大小的(默認情況下大約爲 2MB)。每個區域都分配給一個空間。 G1 收集器的 Java 堆如下圖所示:"}]}]}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/25/25318a1ddf86ac7fab7cf12098a881b5.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"image.png"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "},{"type":"text","marks":[{"type":"color","attrs":{"color":"#9254DE","name":"purple"}}],"text":"圖上的 U 表示“"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#9254DE","name":"purple"}},{"type":"strong"}],"text":"未分配"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#9254DE","name":"purple"}}],"text":"”區域"},{"type":"text","text":"。G1 將堆拆分成小的區域,一個最大的好處是可以做局部區域的垃圾回收,"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#40A9FF","name":"blue"}},{"type":"strong"}],"text":"而不需要每次都回收整個區域比如年輕代和年老代,這樣回收的停頓時間會比較短。具體的收集過程是:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 將所有存活的對象將從收集的區域複製到未分配的區域,"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#FF7021","name":"orange"}},{"type":"strong"}],"text":"比如收集的區域是 Eden 空間,把 Eden 中的存活對象複製到未分配區域,這個未分配區域就成了 Survivor 空間"},{"type":"text","text":"。"},{"type":"text","marks":[{"type":"strong"}],"text":"理想情況下,如果一個區域全是垃圾(意味着一個存活的對象都沒有),則可以直接將該區域聲明爲“未分配”"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 爲了優化收集時間,"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#73D13D","name":"green"}},{"type":"strong"}],"text":"G1 總是優先選擇垃圾最多的區域,從而最大限度地減少後續分配和釋放堆空間所需的工作量。這也是 G1 收集器名字的由來"},{"type":"text","text":"——"},{"type":"text","marks":[{"type":"strong"}],"text":"Garbage-First"},{"type":"text","text":"。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"GC調優原則"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" GC 是有代價的,因此我們調優的根本原則是每一次 GC 都回收儘可能多的對象,也就是減少無用功。因此我們在做具體調優的時候,針對 CMS 和 G1 兩種垃圾收集器,分別有一些相應的策略。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"CMS收集器"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "},{"type":"text","marks":[{"type":"color","attrs":{"color":"#73D13D","name":"green"}},{"type":"strong"}],"text":"對於 CMS 收集器來說"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#73D13D","name":"green"}}],"text":","},{"type":"text","marks":[{"type":"color","attrs":{"color":"#73D13D","name":"green"}},{"type":"strong"}],"text":"最重要的是合理地設置年輕代和年老代的大小"},{"type":"text","text":"。"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#9254DE","name":"purple"}},{"type":"strong"}],"text":"年輕代太小的話,會導致頻繁的 Minor GC,並且很有可能存活期短的對象也不能被回收,GC 的效率就不高"},{"type":"text","text":"。"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"而年老代太小的話,容納不下從年輕代過來的新對象,會頻繁觸發單線程 Full GC,導致較長時間的 GC 暫停,影響 Web 應用的響應時間。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"G1收集器"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "},{"type":"text","marks":[{"type":"strong"}],"text":"對於 G1 收集器來說,我不推薦直接設置年輕代的大小,這一點跟 CMS 收集器不一樣,這是因爲 G1 收集器會根據算法動態決定年輕代和年老代的大小"},{"type":"text","text":"。"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"因此對於 G1 收集器,我們需要關心的是 Java 堆的總大小(-Xmx)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 此外 G1 還有一個較關鍵的參數是"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":"-XX:MaxGCPauseMillis = n"},{"type":"text","text":",這個參數是用來限制最大的 GC 暫停時間,目的是儘量不影響請求處理的響應時間。G1 將根據先前收集的信息以及檢測到的垃圾量,估計它可以立即收集的最大區域數量,從而儘量保證 GC 時間不會超出這個限制。因此 G1 相對來說更加“智能”,使用起來更加簡單。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"內存調優實戰"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 下面我通過一個例子實戰一下 Java 堆設置得過小,導致頻繁的 GC,我們將通過 GC 日誌分析工具來觀察 GC 活動並定位問題。"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"首先我們建立一個 Spring Boot 程序,作爲我們的調優對象,代碼如下:"}]}]}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"@RestController\npublic class GcTestController {\n\n private Queue objCache = new ConcurrentLinkedDeque<>();\n\n @RequestMapping(\"/greeting\")\n public Greeting greeting() {\n Greeting greeting = new Greeting(\"Hello World!\");\n\n if (objCache.size() >= 200000) {\n objCache.clear();\n } else {\n objCache.add(greeting);\n }\n return greeting;\n }\n}\n\n@Data\n@AllArgsConstructor\nclass Greeting {\n private String message;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的代碼就是創建了一個對象池,當對象池中的對象數到達 200000 時才清空一次,用來模擬年老代對象。"}]},{"type":"numberedlist","attrs":{"start":"2","normalizeStart":"2"},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"用下面的命令啓動測試程序:"}]}]}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"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"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我給程序設置的堆的大小爲 32MB,目的是能讓我們看到 Full GC。除此之外,我還打開了 verbosegc 日誌,請注意這裏我使用的版本是 Java 12,默認的垃圾收集器是 G1。"}]},{"type":"numberedlist","attrs":{"start":"3","normalizeStart":"3"},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"使用 JMeter 壓測工具向程序發送測試請求,訪問的路徑是/greeting。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"使用 GCViewer 工具打開 GC 日誌,我們可以看到這樣的圖:"}]}]}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/33/33b5ca00bd8efe0522f0c727604d0620.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"image.png"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖中上部的藍線表示已使用堆的大小,我們看到它週期的上下震盪,這是我們的對象池要擴展到 200000 纔會清空。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖底部的綠線表示年輕代 GC 活動,從圖上看到當堆的使用率上去了,會觸發頻繁的 GC 活動。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖中的豎線表示 Full GC,從圖上看到,伴隨着 Full GC,藍線會下降,這說明 Full GC 收集了年老代中的對象。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於上面的分析,我們可以得出一個結論,那就是 Java 堆的大小不夠。我來解釋一下爲什麼得出這個結論:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GC 活動頻繁:年輕代 GC(綠色線)和年老代 GC(黑色線)都比較密集。這說明內存空間不夠,也就是 Java 堆的大小不夠。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Java 的堆中對象在 GC 之後能夠被回收,說明不是內存泄漏。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們通過 GCViewer 還發現累計 GC 暫停時間有 55.57 秒,如下圖所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/10/108e09a572984ba036fa425ad56407c3.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"image.png"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此我們的解決方案是調大 Java 堆的大小,像下面這樣:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"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\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"生成的新的 GC log 分析圖如下:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/45/451e5841e5e279915f275e398a61073f.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"image.png"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可以看到,沒有發生 Full GC,並且年輕代 GC 也沒有那麼頻繁了,並且累計 GC 暫停時間只有 3.05 秒。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f1/f14444ce6d7446392b01042103b0828d.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"image.png"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 對於 CMS 來說,我們要合理設置年輕代和年老代的大小。你可能會問該如何確定它們的大小呢?這是一個迭代的過程,可以先採用 JVM 的默認值,然後通過壓測分析 GC 日誌。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 如果我們看年輕代的內存使用率處在高位,導致頻繁的 Minor GC,而頻繁 GC 的效率又不高,說明對象沒那麼快能被回收,這時年輕代可以適當調大一點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#9254DE","name":"purple"}},{"type":"strong"}],"text":" 如果我們看年老代的內存使用率處在高位,導致頻繁的 Full GC,這樣分兩種情況:如果每次 Full GC 後年老代的內存佔用率沒有下來,可以懷疑是內存泄漏;如果 Full GC 後年老代的內存佔用率下來了,說明不是內存泄漏,我們要考慮調大年老代。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong"}],"text":" 對於 G1 收集器來說,我們可以適當調大 Java 堆,因爲 G1 收集器採用了局部區域收集策略,單次垃圾收集的時間可控,可以管理較大的 Java 堆。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章