和Web應(yīng)用程序一樣,Tomcat作為一個(gè)Java程序也跑在JVM中淮悼,因此如果要對(duì)Tomcat進(jìn)行調(diào)優(yōu)煞额,需要先了解JVM調(diào)優(yōu)的原理思恐。而對(duì)于JVM調(diào)優(yōu)來說,主要是JVM垃圾收集的優(yōu)化膊毁,一般來說是因?yàn)橛袉栴}才需要優(yōu)化胀莹,所以對(duì)于JVM GC來說,如果觀察到Tomcat進(jìn)程的CPU使用率比較高婚温,并且在GC日志中發(fā)現(xiàn)GC次數(shù)比較頻繁描焰、GC停頓時(shí)間長,這表明需要對(duì)GC進(jìn)行優(yōu)化了栅螟。
在對(duì)GC調(diào)優(yōu)的過程中荆秦,不僅需要知道GC的原理,更重要的是要熟練使用各種監(jiān)控和分析工具力图,具備GC調(diào)優(yōu)的實(shí)戰(zhàn)能力步绸。CMS和G1是時(shí)下使用率比較高的兩款垃圾收集器,從Java 9開始吃媒,采用G1作為默認(rèn)垃圾收集器瓤介,而G1的目標(biāo)也是逐步取代CMS。
1.CMS vs G1
CMS收集器將Java堆分為年輕代(Young)或年老代(Old)赘那。這主要是因?yàn)橛醒芯勘砻餍躺#^90%的對(duì)象在第一次GC時(shí)就被回收掉,但是少數(shù)對(duì)象往往會(huì)存活較長的時(shí)間募舟。
CMS還將年輕代內(nèi)存空間分為幸存者空間(Survivor)和伊甸園空間(Eden)祠斧。新的對(duì)象始終在Eden空間上創(chuàng)建。一旦一個(gè)對(duì)象在一次垃圾收集后還幸存拱礁,就會(huì)被移動(dòng)到幸存者空間琢锋。當(dāng)一個(gè)對(duì)象在多次垃圾收集之后還存活時(shí)辕漂,它會(huì)移動(dòng)到年老代。這樣做的目的是在年輕代和年老代采用不同的收集算法吴超,以達(dá)到較高的收集效率钮热,比如在年輕代采用復(fù)制-整理算法,在年老代采用標(biāo)記-清理算法烛芬。因此CMS將Java堆分成如下區(qū)域:
與CMS相比隧期,G1收集器有兩大特點(diǎn):
- G1可以并發(fā)完成大部分GC的工作,這期間不會(huì)“Stop-The-World”赘娄。
- G1使用非連續(xù)空間仆潮,這使G1能夠有效地處理非常大的堆。此外遣臼,G1可以同時(shí)收集年輕代和年老代性置。G1并沒有將Java堆分成三個(gè)空間(Eden、Survivor和Old)揍堰,而是將堆分成許多(通常是幾百個(gè))非常小的區(qū)域鹏浅。這些區(qū)域是固定大小的(默認(rèn)情況下大約為2MB)。每個(gè)區(qū)域都分配給一個(gè)空間屏歹。 G1收集器的Java堆如下圖所示:
圖上的U表示“未分配”區(qū)域隐砸。G1將堆拆分成小的區(qū)域,一個(gè)最大的好處是可以做局部區(qū)域的垃圾回收蝙眶,而不需要每次都回收整個(gè)區(qū)域比如年輕代和年老代季希,這樣回收的停頓時(shí)間會(huì)比較短。具體的收集過程是:
- 將所有存活的對(duì)象將從收集的區(qū)域復(fù)制到未分配的區(qū)域幽纷,比如收集的區(qū)域是Eden空間式塌,把Eden中的存活對(duì)象復(fù)制到未分配區(qū)域,這個(gè)未分配區(qū)域就成了Survivor空間友浸。理想情況下峰尝,如果一個(gè)區(qū)域全是垃圾(意味著一個(gè)存活的對(duì)象都沒有),則可以直接將該區(qū)域聲明為“未分配”收恢。
- 為了優(yōu)化收集時(shí)間武学,G1總是優(yōu)先選擇垃圾最多的區(qū)域,從而最大限度地減少后續(xù)分配和釋放堆空間所需的工作量派诬。這也是G1收集器名字的由來——Garbage-First劳淆。
2.GC調(diào)優(yōu)原則
GC是有代價(jià)的链沼,因此調(diào)優(yōu)的根本原則是每一次GC都回收盡可能多的對(duì)象默赂,也就是減少無用功。因此在做具體調(diào)優(yōu)的時(shí)候括勺,針對(duì)CMS和G1兩種垃圾收集器缆八,分別有一些相應(yīng)的策略曲掰。
CMS收集器
對(duì)于CMS收集器來說,最重要的是合理地設(shè)置年輕代和年老代的大小奈辰。年輕代太小的話栏妖,會(huì)導(dǎo)致頻繁的Minor GC,并且很有可能存活期短的對(duì)象也不能被回收奖恰,GC的效率就不高吊趾。而年老代太小的話,容納不下從年輕代過來的新對(duì)象瑟啃,會(huì)頻繁觸發(fā)單線程Full GC论泛,導(dǎo)致較長時(shí)間的GC暫停,影響Web應(yīng)用的響應(yīng)時(shí)間蛹屿。
G1收集器
對(duì)于G1收集器來說屁奏,不推薦直接設(shè)置年輕代的大小,這一點(diǎn)跟CMS收集器不一樣错负,這是因?yàn)镚1收集器會(huì)根據(jù)算法動(dòng)態(tài)決定年輕代和年老代的大小坟瓢。因此對(duì)于G1收集器,需要關(guān)心的是Java堆的總大杏倘觥(-Xmx
)折联。
此外G1還有一個(gè)較關(guān)鍵的參數(shù)是-XX:MaxGCPauseMillis = n
,這個(gè)參數(shù)是用來限制最大的GC暫停時(shí)間识颊,目的是盡量不影響請(qǐng)求處理的響應(yīng)時(shí)間崭庸。G1將根據(jù)先前收集的信息以及檢測到的垃圾量,估計(jì)它可以立即收集的最大區(qū)域數(shù)量谊囚,從而盡量保證GC時(shí)間不會(huì)超出這個(gè)限制怕享。因此G1相對(duì)來說更加“智能”,使用起來更加簡單镰踏。
3.內(nèi)存調(diào)優(yōu)實(shí)戰(zhàn)
下面通過一個(gè)例子實(shí)戰(zhàn)一下Java堆設(shè)置得過小函筋,導(dǎo)致頻繁的GC,將通過GC日志分析工具來觀察GC活動(dòng)并定位問題奠伪。
1.首先我們建立一個(gè)Spring Boot程序跌帐,作為調(diào)優(yōu)對(duì)象,代碼如下:
@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)建了一個(gè)對(duì)象池绊率,當(dāng)對(duì)象池中的對(duì)象數(shù)到達(dá)200000時(shí)才清空一次谨敛,用來模擬年老代對(duì)象。
2.用下面的命令啟動(dòng)測試程序:
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日志,請(qǐng)注意這里使用的版本是Java 12炊甲,默認(rèn)的垃圾收集器是G1泥彤。
3.使用JMeter壓測工具向程序發(fā)送測試請(qǐng)求,訪問的路徑是/greeting
卿啡。
4.使用GCViewer工具打開GC日志吟吝,可以看到這樣的圖:
解釋一下這張圖:
- 圖中上部的藍(lán)線表示已使用堆的大小,看到它周期的上下震蕩颈娜,這是因?yàn)閷?duì)象池要擴(kuò)展到200000才會(huì)清空剑逃。
- 圖底部的綠線表示年輕代GC活動(dòng),從圖上看到當(dāng)堆的使用率上去了官辽,會(huì)觸發(fā)頻繁的GC活動(dòng)炕贵。
- 圖中的豎線表示Full GC,從圖上看到野崇,伴隨著Full GC称开,藍(lán)線會(huì)下降,這說明Full GC收集了年老代中的對(duì)象乓梨。
基于上面的分析鳖轰,可以得出一個(gè)結(jié)論,那就是Java堆的大小不夠扶镀。解釋一下為什么得出這個(gè)結(jié)論:
- GC活動(dòng)頻繁:年輕代GC(綠色線)和年老代GC(黑色線)都比較密集蕴侣。這說明內(nèi)存空間不夠,也就是Java堆的大小不夠臭觉。
- Java的堆中對(duì)象在GC之后能夠被回收昆雀,說明不是內(nèi)存泄漏。
通過GCViewer還發(fā)現(xiàn)累計(jì)GC暫停時(shí)間有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也沒有那么頻繁了挽封,并且累計(jì)GC暫停時(shí)間只有3.05秒。
總結(jié)
首先回顧了CMS和G1兩種垃圾收集器背后的設(shè)計(jì)思路以及它們的區(qū)別臣镣,接著分析了GC調(diào)優(yōu)的總體原則辅愿。
對(duì)于CMS來說,要合理設(shè)置年輕代和年老代的大小忆某。該如何確定它們的大小呢点待?這是一個(gè)迭代的過程,可以先采用JVM的默認(rèn)值弃舒,然后通過壓測分析GC日志癞埠。
如果看年輕代的內(nèi)存使用率處在高位,導(dǎo)致頻繁的Minor GC,而頻繁GC的效率又不高燕差,說明對(duì)象沒那么快能被回收,這時(shí)年輕代可以適當(dāng)調(diào)大一點(diǎn)坝冕。
如果看年老代的內(nèi)存使用率處在高位徒探,導(dǎo)致頻繁的Full GC,這樣分兩種情況:如果每次Full GC后年老代的內(nèi)存占用率沒有下來喂窟,可以懷疑是內(nèi)存泄漏测暗;如果Full GC后年老代的內(nèi)存占用率下來了,說明不是內(nèi)存泄漏磨澡,要考慮調(diào)大年老代碗啄。
對(duì)于G1收集器來說,可以適當(dāng)調(diào)大Java堆稳摄,因?yàn)镚1收集器采用了局部區(qū)域收集策略稚字,單次垃圾收集的時(shí)間可控,可以管理較大的Java堆厦酬。