一蒲祈、簡介
? ? 講到JVM調(diào)優(yōu)甘萧,很多對(duì)JVM接觸不深或者只是在應(yīng)對(duì)面試前強(qiáng)行背一些指令的程序員,會(huì)覺得這個(gè)東西太深入梆掸,加上平時(shí)不太用得到扬卷,不愿意花時(shí)間系統(tǒng)學(xué)習(xí)。
? ? 但其實(shí)可以換一個(gè)角度去想酸钦,Java發(fā)展這么多年怪得,加之現(xiàn)在被Oracle收購后催什,研發(fā)團(tuán)隊(duì)實(shí)力更加雄厚爆哑,那些頂級(jí)大佬們每天都在花時(shí)間研究怎么提升JVM性能殊者,每天都在做各種調(diào)試和優(yōu)化罕邀,追求極致,那么逾礁,我們這些平時(shí)只是使用Java語言做開發(fā)的程序員移宅,為什么還要去調(diào)優(yōu)呢鲫售,又該如何調(diào)優(yōu)呢硝拧。
? ? 接下來將從這兩個(gè)問題出發(fā)径筏,講解JVM調(diào)優(yōu),這里可以先給一個(gè)小結(jié)論:我們可以調(diào)優(yōu)的東西真的不多障陶,相對(duì)固定滋恬,所以,不需要太擔(dān)心過于深入而看不懂抱究。
二恢氯、為什么要調(diào)優(yōu)
? ? 在上一章《淺談Java虛擬機(jī)(三)—垃圾回收》中,我們學(xué)習(xí)了垃圾回收算法媳维,也簡單提到了GC中的STW現(xiàn)象(即Stop The World:GC線程在執(zhí)行清理的過程中會(huì)暫停用戶線程酿雪,完成GC后再恢復(fù)用戶線程),事實(shí)上侄刽,不論哪個(gè)分代的GC,運(yùn)用什么樣的算法朋凉,也不論哪個(gè)廠商實(shí)現(xiàn)的垃圾收集器州丹,都躲不過STW,只是時(shí)間長短問題而已,畢竟枚舉GC Roots怎么也需要停頓的墓毒。
? ? STW導(dǎo)致的停頓時(shí)間吓揪,會(huì)使程序運(yùn)行效率降低,甚至直接影響用戶體驗(yàn)所计,早年間如果玩過JAVA做的游戲柠辞,就會(huì)發(fā)現(xiàn)游戲玩著玩著會(huì)偶發(fā)突然莫名其妙的卡頓,那個(gè)可能就是程序正在GC導(dǎo)致的主胧,雖然現(xiàn)在經(jīng)過這么多年的改良叭首,停頓時(shí)間已經(jīng)大大縮短,甚至有的垃圾收集器已經(jīng)可以做到停頓時(shí)間可控踪栋,但針對(duì)不同程序的應(yīng)用場景焙格,即使短暫的停頓時(shí)間或者停頓太頻繁也是不可接受的,因此圍繞停頓時(shí)間的優(yōu)化不管JVM研發(fā)團(tuán)隊(duì)還是我們普通開發(fā)人員都是需要一直做的夷都。
????縮短STW的時(shí)間自然是由JVM研發(fā)團(tuán)隊(duì)眷唉,更確切的說,應(yīng)該是垃圾收集器研發(fā)團(tuán)隊(duì)來做囤官,而我們需要調(diào)優(yōu)的第一個(gè)方向就是根據(jù)我們程序的應(yīng)用場景選擇合適的垃圾收集器冬阳。另外,雖然只要是GC都不可避免STW党饮,但發(fā)生在年輕代的Minor GC效率很高摩泪,加上年輕代只有老年代的二分之一,相對(duì)來說內(nèi)存區(qū)域也比較小劫谅,因此STW停頓時(shí)間很短见坑,反之Full GC引起的停頓時(shí)間就會(huì)較長,因此捏检,我們第二個(gè)調(diào)優(yōu)方向就是盡可能減少GC發(fā)生的次數(shù),尤其是Full GC(Full GC發(fā)生次數(shù)與我們寫的代碼息息相關(guān)荞驴,這是JVM研發(fā)團(tuán)隊(duì)無論如何也無法掌控的,所以贯城,使用好的框架熊楼,保持良好的編碼習(xí)慣,以及對(duì)項(xiàng)目的監(jiān)控調(diào)優(yōu)都只能是我們才能完成的能犯。)
三鲫骗、如何調(diào)優(yōu)
? ? 3.1 垃圾收集器的選擇
? ? ? ? 選擇收集器的前提是我們得先認(rèn)識(shí)不同的垃圾收集器以及它們的優(yōu)缺點(diǎn),JVM有自己的默認(rèn)垃圾收集器踩晶,但也可以通過啟動(dòng)參數(shù)的配置來選擇自己需要的垃圾收集器执泰。
? ??????Serial收集器(-XX:+UserSerialGC)
????????Serial(串行)收集器是最基本、歷史最悠久的垃圾收集器渡蜻。
????????顧名思義术吝,這是一個(gè)單線程收集器计济,它在進(jìn)行垃圾收集工作的時(shí)候必須暫停其他所有的工作線程( “Stop The World” ),直到它收集結(jié)束排苍。
????????采用復(fù)制算法沦寂,主要用于新生代的垃圾收集。
? ??????Serial收集器的優(yōu)點(diǎn)在于它簡單而高效(與其他收集器的單線程相比)淘衙,由于沒有線程交互的開銷传藏,自然可以獲得很高的單線程收集效率,對(duì)于運(yùn)行在Client模式(桌面應(yīng)用)下的虛擬機(jī)來說是個(gè)不錯(cuò)的選擇彤守,或是在用戶的桌面應(yīng)用場景中毯侦,可用內(nèi)存一般不大(幾十M至一兩百M(fèi)),可以在較短時(shí)間內(nèi)完成垃圾收集(幾十MS至一百多MS)遗增,只要不頻繁發(fā)生叫惊,這也是可以接受的。
? ??????Serial Old收集器(沒有直接設(shè)置這個(gè)收集器的參數(shù)做修,還是用-XX:+UserSerialGC霍狰,可用其他新生代收集器的設(shè)置參數(shù)來替換掉Serial收集器)
????????Serial收集器的老年代版本,使用標(biāo)記-整理算法饰及,它同樣是一個(gè)單線程收集器蔗坯。
????????它主要有兩大用途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作為CMS收集器的后備方案燎含。
? ??????ParNew收集器(XX:+UseParNewGC)
????????ParNew收集器其實(shí)就是Serial收集器的多線程版本宾濒,除了使用多線程進(jìn)行垃圾收集外,其余行為(控制參數(shù)屏箍、收集算法绘梦、回收策略等等)和Serial收集器完全一樣,ParNew 在單核 CPU 環(huán)境并不會(huì)比 Serial 收集器達(dá)到更好的效果赴魁,它默認(rèn)開啟的收集線程數(shù)和 CPU 數(shù)量一致卸奉,可以通過 -XX:ParallelGCThreads 來設(shè)置垃圾收集的線程數(shù)。
? ? ? ? 同樣采用復(fù)制算法颖御,用于新生代垃圾收集榄棵。
? ??????它是許多運(yùn)行在Server模式下的虛擬機(jī)的首要選擇。
? ??????Parallel Scavenge收集器(-XX:+UseParallelGC潘拱,JDK7疹鳄、8默認(rèn)使用)
????????Parallel Scavenge 收集器類似于ParNew 收集器,可以看作?ParNew收集器進(jìn)一步的升級(jí)版芦岂。
????????Parallel Scavenge收集器關(guān)注點(diǎn)是吞吐量(高效率的利用CPU)瘪弓,所謂吞吐量就是CPU中用于運(yùn)行用戶代碼的時(shí)間與CPU總消耗時(shí)間(運(yùn)行用戶代碼時(shí)間+GC時(shí)間)的比值。并非收集時(shí)間越短盔腔,吞吐量就越高杠茬,比如一個(gè)收集器每60秒收集一次月褥,一次花費(fèi)5秒弛随,另一收集器每30秒收集一次瓢喉,每次花費(fèi)3秒,后者單次收集時(shí)間更快舀透,但總體吞吐量卻更低了栓票。
? ??????可以通過 -XX:MaxGCPauseMillis 來設(shè)置收集器盡可能在多長時(shí)間內(nèi)完成內(nèi)存回收,可以通過 -XX:GCTimeRatio 來較精確控制吞吐量愕够。
? ??????使用多線程和復(fù)制算法走贪,用于新生代垃圾收集。
? ??????Parallel Old收集器(-XX:+UseParallelOldGC惑芭,JDK7坠狡、8默認(rèn)使用)
????????Parallel Scavenge收集器的老年代版本。
? ??????使用多線程和標(biāo)記-整理算法遂跟,用于老年代垃圾收集逃沿。
? ? ? ? CMS收集器(XX:+UserConMarkSweepGC)
? ??????CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。它而非常符合在注重用戶體驗(yàn)的應(yīng)用上使用幻锁。
????????CMS(Concurrent Mark Sweep)收集器是HotSpot虛擬機(jī)第一款真正意義上的并發(fā)收集器凯亮,它第一次實(shí)現(xiàn)了讓垃圾收集線程與用戶線程(基本上)同時(shí)工作。
? ? ? ? 這里補(bǔ)充解釋一下前面的并行處理器和這里的并發(fā)處理器概念上的區(qū)別:
? ? ? ? 并行(Parallel) :指多條垃圾收集線程并行工作哄尔,但此時(shí)用戶線程仍然處于等待狀態(tài)假消。
? ??????并發(fā)(Concurrent):指用戶線程與垃圾收集線程同時(shí)執(zhí)行(可能是多CPU并行處理,也可能是單CPU快速切換時(shí)間片交替執(zhí)行)岭接。
????????從該收集器名字中的Mark Sweep這兩個(gè)詞可以看出富拗,CMS收集器是一種 “標(biāo)記-清除”算法實(shí)現(xiàn)的,它的運(yùn)作過程相比于前面幾種垃圾收集器來說更加復(fù)雜一些鸣戴。整個(gè)過程分為四個(gè)步驟:
? ? ? ? 1.初始標(biāo)記(CMS initial mark): 暫停所有的其他線程啃沪,并記錄下直接與root相連的對(duì)象,速度很快葵擎,單線程執(zhí)行谅阿。
? ? ? ? 2.并發(fā)標(biāo)記(CMS concurrent mark): 同時(shí)開啟GC和用戶線程,用一個(gè)閉包結(jié)構(gòu)去記錄可達(dá)對(duì)象酬滤。但在這個(gè)階段結(jié)束签餐,這個(gè)閉包結(jié)構(gòu)并不能保證包含當(dāng)前所有的可達(dá)對(duì)象。因?yàn)橛脩艟€程可能會(huì)不斷的更新引用域盯串,所以GC線程無法保證可達(dá)性分析的實(shí)時(shí)性氯檐。所以這個(gè)算法里會(huì)跟蹤記錄這些發(fā)生引用更新的地方。
? ? ? ? 3.重新標(biāo)記(CMS remark): 重新標(biāo)記階段就是為了修正并發(fā)標(biāo)記期間因?yàn)橛脩舫绦蚶^續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄体捏,這個(gè)階段的停頓時(shí)間一般會(huì)比初始標(biāo)記階段的時(shí)間稍長冠摄,遠(yuǎn)遠(yuǎn)比并發(fā)標(biāo)記階段時(shí)間短
? ? ? ? 4.并發(fā)清除(CMS concurrent sweep): 開啟用戶線程糯崎,同時(shí)GC線程開始對(duì)為標(biāo)記的區(qū)域做清掃。
? ??????使用多線程和標(biāo)記-清除算法河泳,用于老年代垃圾收集沃呢。
? ??????CMS主要優(yōu)點(diǎn):并發(fā)收集、低停頓拆挥。但是它有下面三個(gè)明顯的缺點(diǎn):
? ??????對(duì)CPU資源敏感薄霜,隨著 CPU 數(shù)量下降,占用 CPU 資源就越多纸兔,吞吐量也越卸韫稀(換言之,CMS收集器更適配于多核處理器)汉矿;
? ??????無法處理浮動(dòng)垃圾(即并發(fā)過程中崎坊,由于用戶線程未停止,會(huì)不斷產(chǎn)生新對(duì)象洲拇,也可能使之前未標(biāo)記的對(duì)象成為無引用的可回收對(duì)象奈揍,這部分稱為浮動(dòng)垃圾);
? ??????它使用的回收算法“標(biāo)記-清除”算法會(huì)導(dǎo)致收集結(jié)束時(shí)會(huì)有大量空間碎片產(chǎn)生呻待。
? ? ? ? G1收集器(-XX:+UseG1GC打月,JDK9默認(rèn)使用)
? ??????G1 (Garbage-First)是一款面向服務(wù)器的垃圾收集器,主要針對(duì)配備多核處理器及大容量內(nèi)存的機(jī)器,以極高概率滿足GC停頓時(shí)間要求的同時(shí),還具備高吞吐量性能特征。
? ? ? ? 前面的收集器都是針對(duì)于分代收集算法的新生代或老年代蚕捉,而G1收集器直接作用于整個(gè)堆奏篙。
????????G1將整個(gè)Java堆劃分為多個(gè)大小固定的獨(dú)立區(qū)域(Region),但仍然保留了分代收集算法中的新生代和老年代劃分迫淹,只不過每一個(gè)Region都可能是新生代和老年代秘通,還有一個(gè)Humongous分區(qū),專門用來放大對(duì)象敛熬,這些區(qū)域的劃分也是為了G1方便統(tǒng)計(jì)和清理肺稀。
? ? ? ? 從整個(gè)回收過程而言,它同樣分為4步应民,與CMS一樣话原,不同在于最后一步的清理過程(G1將這一步命名為篩選回收),G1會(huì)跟蹤所有Region的垃圾堆積面積诲锹,在后臺(tái)維護(hù)一個(gè)優(yōu)先級(jí)列表繁仁,每次根據(jù)用戶設(shè)置的允許收集時(shí)間,優(yōu)先回收垃圾最多的區(qū)域(這也是Garbage-First名字的由來)归园,這樣保證了G1收集器在有限的時(shí)間內(nèi)可以獲得最高的收集效率黄虱。
它與前面講的 CMS 垃圾收集器相比,有兩個(gè)顯著的改進(jìn):
? ? ? ?1.采用 標(biāo)記-整理 的回收算法
對(duì)于每一個(gè)Region局部而言庸诱,是采用復(fù)制算法捻浦,但從整體而言晤揣,采用標(biāo)記整理算法,這樣不會(huì)產(chǎn)生空間碎片朱灿。
? ? ? ?2.可以精確的控制停頓時(shí)間
使用參數(shù)-XX:MaxGCPauseMillis能讓使用者明確指定消耗在垃圾回收上的時(shí)間不超過指定毫秒數(shù)昧识。
ZGC是JDK11使用的最新的收集器,可以一次收集最大為T為單位的垃圾母剥,十分強(qiáng)大滞诺,但由于筆者暫時(shí)對(duì)其并不了解形导,所以這里暫且不談环疼。
? ? ? ? 如何選擇垃圾收集器
? ? ? ? 其實(shí)上面介紹各個(gè)收集器優(yōu)缺點(diǎn)時(shí)或多或少提到過其應(yīng)用場景,這里簡單做一個(gè)總結(jié):
? ??????如果內(nèi)存較小的服務(wù)器(小于100m)或單核并且沒有停頓時(shí)間的要求朵耕,可使用串行收集器(Serial)炫隶;
? ? ? ? 如果不是很要求停頓時(shí)間,且比較注重吞吐量阎曹,高效利用 CPU伪阶,需要高效運(yùn)算且不需要太多交互,選擇并行(?Parallel Scavenge)处嫌;
? ? ? ? 如果響應(yīng)時(shí)間最重要栅贴,并且不能超過1秒,使用并發(fā)收集器(CMS G1)熏迹;
? ? ? ? 這里需要額外注意的一點(diǎn)是CMS作為老年代收集器檐薯,但卻無法與JDK1.4已經(jīng)存在的新生代收集器Parallel Scavenge配合工作, 因?yàn)镻arallel Scavenge(以及G1)都沒有使用傳統(tǒng)的GC收集器代碼框架注暗,而是另外獨(dú)立實(shí)現(xiàn)坛缕,其余幾種收集器則共用了部分的框架代碼。因此在搭配垃圾收集器時(shí)捆昏,要注意赚楚,能與CMS搭配的只有ParNew和Serial收集器。
? ? ? ? 綜合來說骗卜,官方推薦G1宠页,其性能各方面都脫穎而出,當(dāng)然最終還是要根據(jù)服務(wù)器和應(yīng)用情況來定寇仓。
? ? 3.2 GC調(diào)優(yōu)
? ? ? ? 從上面各垃圾收集器的特點(diǎn)以及綜合發(fā)展歷程來說举户,也可以看出對(duì)于JVM調(diào)優(yōu),主要就是調(diào)整兩個(gè)指標(biāo):停頓時(shí)間和吞吐量焚刺。我們減少GC的次數(shù)最終目的亦是如此敛摘。
? ? ? ? 我們要從GC日志中來分析GC發(fā)生的時(shí)間和原因,因此首先加上啟動(dòng)參數(shù)-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log乳愉,四個(gè)參數(shù)分別是打印GC詳細(xì)日志兄淫,打印GC時(shí)間戳(自JVM啟動(dòng)以后以秒計(jì)量)屯远,打印GC發(fā)生的系統(tǒng)時(shí)間,GC日志存放位置(我這里放在了項(xiàng)目根目錄)捕虽。
? ? ? ? 筆者啟動(dòng)了一個(gè)基于spring boot的后臺(tái)管理系統(tǒng)慨丐,并未做其他操作,直接到項(xiàng)目根路徑找到gc日志并打開泄私,內(nèi)容如下(這里筆者沒有指定收集器房揭,使用的JDK8,默認(rèn)收集器為Parallel Scavenge晌端,因此以下是Parallel Scavenge的GC日志格式捅暴,如果是CMS或者G1,格式會(huì)跟下面內(nèi)容不同咧纠,更重要的是學(xué)習(xí)調(diào)優(yōu)的方法):
Java HotSpot(TM) 64-Bit Server VM (25.201-b09) for windows-amd64 JRE (1.8.0_201-b09), built on Dec 15 2018 18:36:39 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8303556k(2147924k free), swap 19497824k(6824340k free)
CommandLine flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:InitialHeapSize=132856896 -XX:+ManagementServer -XX:MaxHeapSize=2125710336 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
2020-12-14T16:56:58.160+0800: 1.783: [GC (Allocation Failure) [PSYoungGen: 33280K->4161K(38400K)] 33280K->4169K(125952K), 0.1588282 secs] [Times: user=0.00 sys=0.00, real=0.16 secs]
2020-12-14T16:56:58.618+0800: 2.149: [GC (Allocation Failure) [PSYoungGen: 37424K->4640K(38400K)] 37432K->4656K(125952K), 0.0096581 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-14T16:56:58.996+0800: 2.527: [GC (Allocation Failure) [PSYoungGen: 37920K->4553K(38400K)] 37936K->4577K(125952K), 0.0141848 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
2020-12-14T16:56:59.432+0800: 2.964: [GC (Allocation Failure) [PSYoungGen: 37833K->5113K(71680K)] 37857K->5518K(159232K), 0.0067484 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-14T16:56:59.863+0800: 3.395: [GC (Allocation Failure) [PSYoungGen: 71673K->5092K(71680K)] 72078K->7683K(159232K), 0.0136604 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-14T16:57:00.755+0800: 4.286: [GC (Metadata GC Threshold) [PSYoungGen: 68897K->7159K(138240K)] 71489K->11699K(225792K), 0.0123722 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-14T16:57:00.768+0800: 4.299: [Full GC (Metadata GC Threshold) [PSYoungGen: 7159K->0K(138240K)] [ParOldGen: 4540K->8861K(54272K)] 11699K->8861K(192512K), [Metaspace: 20620K->20618K(1067008K)], 0.0353734 secs] [Times: user=0.13 sys=0.00, real=0.04 secs]
2020-12-14T16:57:01.860+0800: 5.392: [GC (Allocation Failure) [PSYoungGen: 131072K->6138K(140288K)] 139933K->15007K(194560K), 0.0078854 secs] [Times: user=0.03 sys=0.02, real=0.01 secs]
2020-12-14T16:57:02.439+0800: 5.970: [GC (Allocation Failure) [PSYoungGen: 137210K->8156K(226816K)] 146079K->17097K(281088K), 0.0080187 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]
2020-12-14T16:57:03.950+0800: 7.481: [GC (Allocation Failure) [PSYoungGen: 226268K->9716K(227840K)] 235209K->25743K(282112K), 0.0166100 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]
2020-12-14T16:57:04.336+0800: 7.867: [GC (Metadata GC Threshold) [PSYoungGen: 55486K->8346K(315904K)] 71513K->29205K(370176K), 0.0193028 secs] [Times: user=0.05 sys=0.00, real=0.02 secs]
2020-12-14T16:57:04.356+0800: 7.887: [Full GC (Metadata GC Threshold) [PSYoungGen: 8346K->0K(315904K)] [ParOldGen: 20859K->18807K(86016K)] 29205K->18807K(401920K), [Metaspace: 33912K->33912K(1079296K)], 0.0916556 secs] [Times: user=0.27 sys=0.00, real=0.09 secs]
2020-12-14T16:57:08.468+0800: 12.000: [GC (Allocation Failure) [PSYoungGen: 302592K->13300K(315904K)] 321399K->34767K(401920K), 0.0135112 secs] [Times: user=0.05 sys=0.02, real=0.01 secs]
2020-12-14T16:57:10.831+0800: 14.363: [GC (Metadata GC Threshold) [PSYoungGen: 234414K->14848K(405504K)] 255881K->40860K(491520K), 0.0169806 secs] [Times: user=0.06 sys=0.01, real=0.02 secs]
2020-12-14T16:57:10.848+0800: 14.380: [Full GC (Metadata GC Threshold) [PSYoungGen: 14848K->0K(405504K)] [ParOldGen: 26011K->35887K(125952K)] 40860K->35887K(531456K), [Metaspace: 56473K->56466K(1099776K)], 0.2215918 secs] [Times: user=0.61 sys=0.02, real=0.22 secs]
2020-12-14T16:57:13.807+0800: 17.339: [GC (Allocation Failure) [PSYoungGen: 390144K->15517K(411648K)] 426031K->51413K(537600K), 0.0222413 secs] [Times: user=0.03 sys=0.02, real=0.02 secs]
? ? ? ? 在上一章中已經(jīng)介紹過如何讀GC日志蓬痒,詳細(xì)解釋這里不再重復(fù),只是簡單回顧一下:
? ??????2020-12-14T16:56:58.160+0800: 1.783: [GC (Allocation Failure) [PSYoungGen: 33280K->4161K(38400K)] 33280K->4169K(125952K), 0.1588282 secs] [Times: user=0.00 sys=0.00, real=0.16 secs]
? ? ? ? 這是第一條真正關(guān)于GC的信息漆羔,前面的時(shí)間忽略梧奢,從時(shí)間后開始讀,這里發(fā)生的是普通GC演痒,發(fā)生原因?yàn)锳llocation Failure亲轨,即內(nèi)存分配失敗,程序剛啟動(dòng)鸟顺,大量創(chuàng)建對(duì)象惦蚊,觸發(fā)這個(gè)很正常,后面為發(fā)生這次Young GC前后年輕代的內(nèi)存大小變化(如果發(fā)生的Full GC就還會(huì)有老年代诊沪、元空間內(nèi)存大小變化)养筒。
? ? ? ? 好了,基于這個(gè)簡單回顧端姚,接下來我們首先關(guān)注第一次發(fā)生Full GC的日志信息:
? ??????2020-12-14T16:57:00.768+0800: 4.299: [Full GC (Metadata GC Threshold) [PSYoungGen: 7159K->0K(138240K)] [ParOldGen: 4540K->8861K(54272K)] 11699K->8861K(192512K), [Metaspace: 20620K->20618K(1067008K)], 0.0353734 secs] [Times: user=0.13 sys=0.00, real=0.04 secs]
? ? ? ? 有了前面對(duì)GC日志的認(rèn)識(shí)晕粪,這里就簡單了:這次觸發(fā)Full GC的原因是Metadata GC Threshold,即元空間達(dá)到閾值渐裸。并且從后面?[Metaspace: 20620K->20618K(1067008K)]可以看出GC前后巫湘,元空間的內(nèi)存占用并無變化,其實(shí)原因也很容易想到昏鹃,程序剛啟動(dòng)尚氛,大量的Class元數(shù)據(jù)信息被加載進(jìn)元空間,這個(gè)時(shí)候的清理自然沒什么效果洞渤,我們馬上找到下一條Full GC信息:
????????2020-12-14T16:57:04.356+0800: 7.887: [Full GC (Metadata GC Threshold) [PSYoungGen: 8346K->0K(315904K)] [ParOldGen: 20859K->18807K(86016K)] 29205K->18807K(401920K), [Metaspace: 33912K->33912K(1079296K)], 0.0916556 secs] [Times: user=0.27 sys=0.00, real=0.09 secs]
? ? ? ? 仍然是元空間觸發(fā)的Full GC阅嘶,這條還可以看出另外一點(diǎn),元空間的閾值比上一次Full GC時(shí)變大了(?[Metaspace: 33912K->33912K(1079296K)]),也側(cè)面印證了元空間是動(dòng)態(tài)擴(kuò)展的讯柔。
? ? ? ? 那么解決此次Full GC的方案就很明了了抡蛙,使用-XX:MetaspaceSize=128M 增大初始元空間大小(元空間的默認(rèn)初始大小是20.75MB魂迄,我這里將其設(shè)置為128M)粗截,再看更改設(shè)置后重新啟動(dòng)項(xiàng)目的GC日志:
Java HotSpot(TM) 64-Bit Server VM (25.201-b09) for windows-amd64 JRE (1.8.0_201-b09), built on Dec 15 2018 18:36:39 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8303556k(2495208k free),swap 18265028k(10087868k free)
CommandLine flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:InitialHeapSize=132856896 -XX:+ManagementServer -XX:MaxHeapSize=2125710336 -XX:MetaspaceSize=134217728 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
2020-12-15T14:45:45.254+0800: 1.277: [GC (GCLocker Initiated GC) [PSYoungGen: 33280K->4130K(38400K)] 33280K->4146K(125952K), 0.0054048 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-15T14:45:45.375+0800: 1.399: [GC (Allocation Failure) [PSYoungGen: 37398K->4463K(38400K)] 40738K->7811K(125952K), 0.0087424 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
2020-12-15T14:45:45.687+0800: 1.711: [GC (Allocation Failure) [PSYoungGen: 37743K->4781K(38400K)] 41091K->8137K(125952K), 0.0060392 secs] [Times: user=0.03 sys=0.01, real=0.01 secs]
2020-12-15T14:45:45.930+0800: 1.954: [GC (Allocation Failure) [PSYoungGen: 38061K->5108K(71680K)] 41417K->8910K(159232K), 0.0074853 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-15T14:45:46.225+0800: 2.249: [GC (Allocation Failure) [PSYoungGen: 71668K->5107K(71680K)] 75470K->11170K(159232K), 0.0097313 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-15T14:45:46.619+0800: 2.643: [GC (Allocation Failure) [PSYoungGen: 71667K->7679K(138752K)] 77730K->15449K(226304K), 0.0112721 secs] [Times: user=0.03 sys=0.03, real=0.01 secs]
2020-12-15T14:45:47.333+0800: 3.358: [GC (Allocation Failure) [PSYoungGen: 138751K->8713K(140800K)] 146521K->19498K(228352K), 0.0116370 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-15T14:45:47.881+0800: 3.905: [GC (Allocation Failure) [PSYoungGen: 139785K->9793K(270848K)] 150570K->21575K(358400K), 0.0089494 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-15T14:45:49.568+0800: 5.592: [GC (Allocation Failure) [PSYoungGen: 270401K->11771K(272384K)] 282183K->34080K(359936K), 0.0197679 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]
2020-12-15T14:45:50.989+0800: 7.013: [GC (Allocation Failure) [PSYoungGen: 272379K->17405K(429568K)] 294688K->47770K(517120K), 0.0207186 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
2020-12-15T14:45:53.296+0800: 9.320: [GC (Allocation Failure) [PSYoungGen: 429565K->21845K(434688K)] 459930K->62361K(522240K), 0.0283819 secs] [Times: user=0.13 sys=0.00, real=0.03 secs]
????????可以明顯看出捣炬,整個(gè)啟動(dòng)過程中熊昌,沒有發(fā)生Full GC(修改前是三次,根據(jù)實(shí)際情況調(diào)整合適的元空間初始值湿酸,筆者這里為了效果一次性調(diào)得比較大)婿屹,那么我們的此次調(diào)優(yōu)目的就達(dá)到了,事實(shí)上稿械,絕大多數(shù)調(diào)優(yōu)的第一步都是增大元空間大醒⌒骸(項(xiàng)目比較大的時(shí)候,如果是十分微型的項(xiàng)目美莫,可能不需要這步調(diào)優(yōu))。
? ? ? ? 基于對(duì)Full GC的調(diào)優(yōu)梯捕,我們對(duì)新生代也可以如法炮制厢呵,增大新生代動(dòng)態(tài)擴(kuò)容增量(默認(rèn)是20%),可以減少Young GC:-XX:YoungGenerationSizeIncrement=30傀顾,效果這里不再展示襟铭。
????????可能看GC日志比較枯燥,這里筆者再推薦一個(gè)很好用的可視化在線GC日志分析工具:gceasy.io(不需要翻墻)短曾,我們再用這個(gè)工具來展示上面調(diào)優(yōu)前后的效果寒砖。
? ? ? ? 打開網(wǎng)站上傳GC日志,點(diǎn)擊分析按鈕:
????????分析完成會(huì)跳轉(zhuǎn)頁面嫉拐,然后找到這里:
????????這次上傳的是調(diào)優(yōu)前的日志哩都,可以看到,標(biāo)注的幾個(gè)關(guān)鍵指標(biāo)分別就是吞吐量:95.886%婉徘,平均GC停頓時(shí)間35毫秒漠嵌,最大單次GC停頓時(shí)間為200毫秒,再往下翻可以看到GC次數(shù)統(tǒng)計(jì):
? ? ? ? 可以看到盖呼,Minor GC共13次儒鹿,Full GC共3次,當(dāng)然几晤,這些數(shù)據(jù)都可以從GC日志中自行計(jì)算约炎,但有好的工具輔助我們調(diào)優(yōu),何樂不為呢蟹瘾,加上如果GC日志非常長圾浅,人工統(tǒng)計(jì)難免繁瑣易錯(cuò)墙贱。
? ? ? ? 好了,廢話不多說贱傀,重新上傳調(diào)優(yōu)后的日志再看數(shù)據(jù):
? ? ? ? 可以看到惨撇,調(diào)優(yōu)后,吞吐量達(dá)到了98.21%府寒,平均GC停頓時(shí)間15.5毫秒魁衙,最大單次GC停頓時(shí)間40毫秒,Minor GC11次株搔,F(xiàn)ull GC0次剖淀,這里再給大家做一個(gè)表格,更直觀看出調(diào)優(yōu)前后數(shù)據(jù)對(duì)比:
????????這兩步調(diào)優(yōu)能解決大多數(shù)項(xiàng)目啟動(dòng)慢的問題纤房,至于項(xiàng)目運(yùn)行中的情況纵隔,可能會(huì)更復(fù)雜,也因項(xiàng)目而異炮姨,但調(diào)優(yōu)的方法都差不多:分析GC產(chǎn)生的原因捌刮,針對(duì)原因設(shè)置JVM啟動(dòng)參數(shù),其實(shí)上面給出的這個(gè)工具中也有GC原因統(tǒng)計(jì):
? ? ? ? 調(diào)優(yōu)的指令也有很多舒岸,這里不可能全部模擬出來并使用绅作,只能列舉出來,讀者在平時(shí)調(diào)優(yōu)過程中可能會(huì)用到:
堆棧設(shè)置
-Xss:每個(gè)線程的棧大小
-Xms:初始堆大小蛾派,默認(rèn)物理內(nèi)存的1/64
-Xmx:最大堆大小俄认,默認(rèn)物理內(nèi)存的1/4
-Xmn:新生代大小
-XX:NewSize:設(shè)置新生代初始大小
-XX:NewRatio:默認(rèn)2,表示新生代占年老代的1/2洪乍,占整個(gè)堆內(nèi)存的1/3眯杏。
-XX:SurvivorRatio:默認(rèn)8,表示一個(gè)survivor區(qū)占用1/8的Eden內(nèi)存壳澳,即1/10的新生代內(nèi)存岂贩。
-XX:YoungGenerationSizeIncrement:?設(shè)置新生代動(dòng)態(tài)擴(kuò)容增量
-XX:MetaspaceSize:設(shè)置元空間初始大小
-XX:MaxMetaspaceSize:設(shè)置元空間最大允許大小,默認(rèn)不受限制钾埂,JVM Metaspace會(huì)進(jìn)行動(dòng)態(tài)擴(kuò)展河闰。
收集器設(shè)置
-XX:+UseSerialGC:設(shè)置串行收集器
-XX:+UseParallelGC:設(shè)置并行收集器
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:+UseParNewGC:在新生代使用并行收集器
-XX:+UseParalledlOldGC:設(shè)置并行老年代收集器
-XX:+UseConcMarkSweepGC:設(shè)置CMS并發(fā)收集器
-XX:+UseG1GC:設(shè)置G1收集器
-XX:ParallelGCThreads:設(shè)置用于垃圾回收的線程數(shù)
并行收集器設(shè)置
-XX:ParallelGCThreads:設(shè)置并行收集器收集時(shí)使用的CPU數(shù)。并行收集線程數(shù)褥紫。
-XX:MaxGCPauseMillis:設(shè)置并行收集最大暫停時(shí)間
-XX:GCTimeRatio:設(shè)置垃圾回收時(shí)間占程序運(yùn)行時(shí)間的百分比姜性。公式為1/(1+n)
CMS收集器設(shè)置
-XX:+UseConcMarkSweepGC:設(shè)置CMS并發(fā)收集器
-XX:+CMSIncrementalMode:設(shè)置為增量模式。適用于單CPU情況髓考。
-XX:ParallelGCThreads:設(shè)置并發(fā)收集器新生代收集方式為并行收集時(shí)部念,使用的CPU數(shù)。并行收集線程數(shù)。
-XX:CMSFullGCsBeforeCompaction:設(shè)定進(jìn)行多少次CMS垃圾回收后儡炼,進(jìn)行一次內(nèi)存壓縮
-XX:+CMSClassUnloadingEnabled:允許對(duì)類元數(shù)據(jù)進(jìn)行回收
-XX:UseCMSInitiatingOccupancyOnly:表示只在到達(dá)閥值的時(shí)候妓湘,才進(jìn)行CMS回收
-XX:ParallelCMSThreads:設(shè)定CMS的線程數(shù)量
-XX:CMSInitiatingOccupancyFraction:設(shè)置CMS收集器在老年代空間被使用多少后觸發(fā)
-XX:+UseCMSCompactAtFullCollection:設(shè)置CMS收集器在完成垃圾收集后是否要進(jìn)行一次內(nèi)存碎片的整理
G1收集器設(shè)置
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的線程數(shù)量
-XX:G1HeapRegionSize:指定分區(qū)大小(1MB~32MB,且必須是2的冪)乌询,默認(rèn)將整堆劃分為2048個(gè)分區(qū)
-XX:GCTimeRatio:吞吐量大小榜贴,0-100的整數(shù)(默認(rèn)9),值為n則系統(tǒng)將花費(fèi)不超過1/(1+n)的時(shí)間用于垃圾收集
-XX:MaxGCPauseMillis:最大停頓時(shí)間(默認(rèn)200ms)
-XX:G1NewSizePercent:新生代內(nèi)存初始空間(默認(rèn)整堆5%)
-XX:G1MaxNewSizePercent:新生代內(nèi)存最大空間
-XX:TargetSurvivorRatio:Survivor填充容量(默認(rèn)50%)
-XX:MaxTenuringThreshold:最大任期閾值(默認(rèn)15)
-XX:InitiatingHeapOccupancyPercen:老年代占用空間超過整堆比IHOP閾值(默認(rèn)45%),超過則執(zhí)行混合收集
-XX:G1HeapWastePercent:堆廢物百分比(默認(rèn)5%)
-XX:G1MixedGCCountTarget:參數(shù)混合周期的最大總次數(shù)(默認(rèn)8)
四、總結(jié)
? ? 本章講解了JVM調(diào)優(yōu),如何選擇垃圾收集器刑枝,如何減少GC,調(diào)優(yōu)的步驟和方法其實(shí)很簡單驶拱,也很單調(diào),但JVM調(diào)優(yōu)講究的更多是經(jīng)驗(yàn)累積晶衷,讀者可以下來后用自己的項(xiàng)目進(jìn)行調(diào)優(yōu)實(shí)戰(zhàn)蓝纲,熟能生巧。?
? ? 本章并未講解JMap晌纫、JStat這種JVM監(jiān)控命令税迷,還有JConsole、JVisualvm這種可視化監(jiān)控工具缸匪,主要是使用這些的目的更多是監(jiān)控項(xiàng)目運(yùn)行情況和定位如內(nèi)存溢出這種問題翁狐,個(gè)人理解其并不在調(diào)優(yōu)范疇,感興趣的讀者可以搜索其他作者關(guān)于JVM監(jiān)控和分析專題的文章凌蔬。
? ? JVM的內(nèi)容也遠(yuǎn)不止這些,筆者“淺談Java虛擬機(jī)系列”主要一則是分享自己的學(xué)習(xí)歷程闯冷,二者是引領(lǐng)對(duì)JVM不了解的Java學(xué)習(xí)者們從認(rèn)識(shí)虛擬機(jī)到進(jìn)行很多同行都比較“害怕”的JVM調(diào)優(yōu)砂心,每一篇文章都層層遞進(jìn),從了解到上手自此打開JVM這扇大門蛇耀,后續(xù)的學(xué)習(xí)筆者與讀者共勉辩诞。
? ? 那么本系列就到此結(jié)束,后續(xù)會(huì)更新其他Java系列的文章纺涤,學(xué)無止境译暂,一起加油。
《淺談Java虛擬機(jī)(一)—什么是Java虛擬機(jī)》
《淺談Java虛擬機(jī)(二)—運(yùn)行時(shí)數(shù)據(jù)區(qū)域》
《淺談Java虛擬機(jī)(四)—JVM調(diào)優(yōu)》
本系列文章參考文檔:《深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐》--?周志明