JVM調(diào)優(yōu)的幾種策略
一疯溺、JVM內(nèi)存模型及垃圾收集算法
1.根據(jù)Java虛擬機(jī)規(guī)范,JVM將內(nèi)存劃分為:
New(年輕代)
Tenured(年老代)
永久代(Perm)
其中New和Tenured屬于堆內(nèi)存朴下,堆內(nèi)存會(huì)從JVM啟動(dòng)參數(shù)(-Xmx:3G)指定的內(nèi)存中分配,Perm不屬于堆內(nèi)存苦蒿,有虛擬機(jī)直接分配殴胧,但可以通過(guò)-XX:PermSize -XX:MaxPermSize 等參數(shù)調(diào)整其大小。
年輕代(New):年輕代用來(lái)存放JVM剛分配的Java對(duì)象
年老代(Tenured):年輕代中經(jīng)過(guò)垃圾回收沒(méi)有回收掉的對(duì)象將被Copy到年老代
永久代(Perm):永久代存放Class佩迟、Method元信息团滥,其大小跟項(xiàng)目的規(guī)模、類(lèi)报强、方法的量有關(guān)灸姊,一般設(shè)置為128M就足夠,設(shè)置原則是預(yù)留30%的空間秉溉。
New又分為幾個(gè)部分:
Eden:Eden用來(lái)存放JVM剛分配的對(duì)象
Survivor1
Survivro2:兩個(gè)Survivor空間一樣大厨钻,當(dāng)Eden中的對(duì)象經(jīng)過(guò)垃圾回收沒(méi)有被回收掉時(shí),會(huì)在兩個(gè)Survivor之間來(lái)回Copy坚嗜,當(dāng)滿(mǎn)足某個(gè)條件夯膀,比如Copy次數(shù),就會(huì)被Copy到Tenured苍蔬。顯然诱建,Survivor只是增加了對(duì)象在年輕代中的逗留時(shí)間,增加了被垃圾回收的可能性碟绑。
2.垃圾回收算法
垃圾回收算法可以分為三類(lèi)俺猿,都基于標(biāo)記-清除(復(fù)制)算法:
Serial算法(單線程)
并行算法
并發(fā)算法
JVM會(huì)根據(jù)機(jī)器的硬件配置對(duì)每個(gè)內(nèi)存代選擇適合的回收算法,比如格仲,如果機(jī)器多于1個(gè)核押袍,會(huì)對(duì)年輕代選擇并行算法,關(guān)于選擇細(xì)節(jié)請(qǐng)參考JVM調(diào)優(yōu)文檔凯肋。
稍微解釋下的是谊惭,并行算法是用多線程進(jìn)行垃圾回收,回收期間會(huì)暫停程序的執(zhí)行侮东,而并發(fā)算法圈盔,也是多線程回收,但期間不停止應(yīng)用執(zhí)行悄雅。所以驱敲,并發(fā)算法適用于交互性高的一些程序。經(jīng)過(guò)觀察宽闲,并發(fā)算法會(huì)減少年輕代的大小众眨,其實(shí)就是使用了一個(gè)大的年老代握牧,這反過(guò)來(lái)跟并行算法相比吞吐量相對(duì)較低。
還有一個(gè)問(wèn)題是娩梨,垃圾回收動(dòng)作何時(shí)執(zhí)行我碟?
當(dāng)年輕代內(nèi)存滿(mǎn)時(shí),會(huì)引發(fā)一次普通GC姚建,該GC僅回收年輕代矫俺。需要強(qiáng)調(diào)的時(shí),年輕代滿(mǎn)是指Eden代滿(mǎn)掸冤,Survivor滿(mǎn)不會(huì)引發(fā)GC
當(dāng)年老代滿(mǎn)時(shí)會(huì)引發(fā)Full GC厘托,F(xiàn)ull GC將會(huì)同時(shí)回收年輕代、年老代
當(dāng)永久代滿(mǎn)時(shí)也會(huì)引發(fā)Full GC稿湿,會(huì)導(dǎo)致Class铅匹、Method元信息的卸載
另一個(gè)問(wèn)題是,何時(shí)會(huì)拋出OutOfMemoryException饺藤,并不是內(nèi)存被耗空的時(shí)候才拋出
JVM98%的時(shí)間都花費(fèi)在內(nèi)存回收
每次回收的內(nèi)存小于2%
滿(mǎn)足這兩個(gè)條件將觸發(fā)OutOfMemoryException包斑,這將會(huì)留給系統(tǒng)一個(gè)微小的間隙以做一些Down之前的操作,比如手動(dòng)打印Heap Dump涕俗。
二罗丰、內(nèi)存泄漏及解決方法
1.系統(tǒng)崩潰前的一些現(xiàn)象:
每次垃圾回收的時(shí)間越來(lái)越長(zhǎng),由之前的10ms延長(zhǎng)到50ms左右再姑,F(xiàn)ullGC的時(shí)間也有之前的0.5s延長(zhǎng)到4哨免、5s
FullGC的次數(shù)越來(lái)越多族沃,最頻繁時(shí)隔不到1分鐘就進(jìn)行一次FullGC
年老代的內(nèi)存越來(lái)越大并且每次FullGC后年老代沒(méi)有內(nèi)存被釋放
之后系統(tǒng)會(huì)無(wú)法響應(yīng)新的請(qǐng)求畦粮,逐漸到達(dá)OutOfMemoryError的臨界值掌测。
2.生成堆的dump文件
通過(guò)JMX的MBean生成當(dāng)前的Heap信息,大小為一個(gè)3G(整個(gè)堆的大衅芤伞)的hprof文件讨永,如果沒(méi)有啟動(dòng)JMX可以通過(guò)Java的jmap命令來(lái)生成該文件。
3.分析dump文件
下面要考慮的是如何打開(kāi)這個(gè)3G的堆信息文件遇革,顯然一般的Window系統(tǒng)沒(méi)有這么大的內(nèi)存卿闹,必須借助高配置的Linux。當(dāng)然我們可以借助X-Window把Linux上的圖形導(dǎo)入到Window澳淑。我們考慮用下面幾種工具打開(kāi)該文件:
Visual VM
IBM HeapAnalyzer
JDK 自帶的Hprof工具
使用這些工具時(shí)為了確保加載速度比原,建議設(shè)置最大內(nèi)存為6G。使用后發(fā)現(xiàn)杠巡,這些工具都無(wú)法直觀地觀察到內(nèi)存泄漏,Visual VM雖能觀察到對(duì)象大小雇寇,但看不到調(diào)用堆棧氢拥;HeapAnalyzer雖然能看到調(diào)用堆棧蚌铜,卻無(wú)法正確打開(kāi)一個(gè)3G的文件。因此嫩海,我們又選用了Eclipse專(zhuān)門(mén)的靜態(tài)內(nèi)存分析工具:Mat冬殃。
4.分析內(nèi)存泄漏
通過(guò)Mat我們能清楚地看到,哪些對(duì)象被懷疑為內(nèi)存泄漏叁怪,哪些對(duì)象占的空間最大及對(duì)象的調(diào)用關(guān)系审葬。針對(duì)本案,在ThreadLocal中有很多的JbpmContext實(shí)例奕谭,經(jīng)過(guò)調(diào)查是JBPM的Context沒(méi)有關(guān)閉所致涣觉。
另,通過(guò)Mat或JMX我們還可以分析線程狀態(tài)血柳,可以觀察到線程被阻塞在哪個(gè)對(duì)象上官册,從而判斷系統(tǒng)的瓶頸。
5.回歸問(wèn)題
Q:為什么崩潰前垃圾回收的時(shí)間越來(lái)越長(zhǎng)难捌?
A:根據(jù)內(nèi)存模型和垃圾回收算法膝宁,垃圾回收分兩部分:內(nèi)存標(biāo)記、清除(復(fù)制)根吁,標(biāo)記部分只要內(nèi)存大小固定時(shí)間是不變的员淫,變的是復(fù)制部分,因?yàn)槊看卫厥斩加幸恍┗厥詹坏舻膬?nèi)存击敌,所以增加了復(fù)制量满粗,導(dǎo)致時(shí)間延長(zhǎng)。所以愚争,垃圾回收的時(shí)間也可以作為判斷內(nèi)存泄漏的依據(jù)
Q:為什么Full GC的次數(shù)越來(lái)越多映皆?
A:因此內(nèi)存的積累,逐漸耗盡了年老代的內(nèi)存轰枝,導(dǎo)致新對(duì)象分配沒(méi)有更多的空間捅彻,從而導(dǎo)致頻繁的垃圾回收
Q:為什么年老代占用的內(nèi)存越來(lái)越大?
A:因?yàn)槟贻p代的內(nèi)存無(wú)法被回收鞍陨,越來(lái)越多地被Copy到年老代
三步淹、性能調(diào)優(yōu)
除了上述內(nèi)存泄漏外,我們還發(fā)現(xiàn)CPU長(zhǎng)期不足3%诚撵,系統(tǒng)吞吐量不夠缭裆,針對(duì)8core×16G、64bit的Linux服務(wù)器來(lái)說(shuō)寿烟,是嚴(yán)重的資源浪費(fèi)澈驼。
在CPU負(fù)載不足的同時(shí),偶爾會(huì)有用戶(hù)反映請(qǐng)求的時(shí)間過(guò)長(zhǎng)筛武,我們意識(shí)到必須對(duì)程序及JVM進(jìn)行調(diào)優(yōu)缝其。從以下幾個(gè)方面進(jìn)行:
線程池:解決用戶(hù)響應(yīng)時(shí)間長(zhǎng)的問(wèn)題
連接池
JVM啟動(dòng)參數(shù):調(diào)整各代的內(nèi)存比例和垃圾回收算法挎塌,提高吞吐量
程序算法:改進(jìn)程序邏輯算法提高性能
1.Java線程池(java.util.concurrent.ThreadPoolExecutor)
大多數(shù)JVM6上的應(yīng)用采用的線程池都是JDK自帶的線程池,之所以把成熟的Java線程池進(jìn)行羅嗦說(shuō)明内边,是因?yàn)樵摼€程池的行為與我們想象的有點(diǎn)出入榴都。Java線程池有幾個(gè)重要的配置參數(shù):
corePoolSize:核心線程數(shù)(最新線程數(shù))
maximumPoolSize:最大線程數(shù),超過(guò)這個(gè)數(shù)量的任務(wù)會(huì)被拒絕漠其,用戶(hù)可以通過(guò)RejectedExecutionHandler接口自定義處理方式
keepAliveTime:線程保持活動(dòng)的時(shí)間
workQueue:工作隊(duì)列嘴高,存放執(zhí)行的任務(wù)
Java線程池需要傳入一個(gè)Queue參數(shù)(workQueue)用來(lái)存放執(zhí)行的任務(wù),而對(duì)Queue的不同選擇和屎,線程池有完全不同的行為:
SynchronousQueue:?一個(gè)無(wú)容量的等待隊(duì)列拴驮,一個(gè)線程的insert操作必須等待另一線程的remove操作,采用這個(gè)Queue線程池將會(huì)為每個(gè)任務(wù)分配一個(gè)新線程
LinkedBlockingQueue :?無(wú)界隊(duì)列眶俩,采用該Queue莹汤,線程池將忽略maximumPoolSize參數(shù),僅用corePoolSize的線程處理所有的任務(wù)颠印,未處理的任務(wù)便在LinkedBlockingQueue中排隊(duì)
ArrayBlockingQueue: 有界隊(duì)列纲岭,在有界隊(duì)列和?maximumPoolSize的作用下,程序?qū)⒑茈y被調(diào)優(yōu):更大的Queue和小的maximumPoolSize將導(dǎo)致CPU的低負(fù)載线罕;小的Queue和大的池止潮,Queue就沒(méi)起動(dòng)應(yīng)有的作用。
其實(shí)我們的要求很簡(jiǎn)單钞楼,希望線程池能跟連接池一樣喇闸,能設(shè)置最小線程數(shù)、最大線程數(shù)询件,當(dāng)最小數(shù)<任務(wù)<最大數(shù)時(shí)燃乍,應(yīng)該分配新的線程處理;當(dāng)任務(wù)>最大數(shù)時(shí)宛琅,應(yīng)該等待有空閑線程再處理該任務(wù)刻蟹。
但線程池的設(shè)計(jì)思路是,任務(wù)應(yīng)該放到Queue中嘿辟,當(dāng)Queue放不下時(shí)再考慮用新線程處理舆瘪,如果Queue滿(mǎn)且無(wú)法派生新線程,就拒絕該任務(wù)红伦。設(shè)計(jì)導(dǎo)致“先放等執(zhí)行”英古、“放不下再執(zhí)行”、“拒絕不等待”昙读。所以召调,根據(jù)不同的Queue參數(shù),要提高吞吐量不能一味地增大maximumPoolSize。
當(dāng)然某残,要達(dá)到我們的目標(biāo)国撵,必須對(duì)線程池進(jìn)行一定的封裝陵吸,幸運(yùn)的是ThreadPoolExecutor中留了足夠的自定義接口以幫助我們達(dá)到目標(biāo)玻墅。我們封裝的方式是:
以SynchronousQueue作為參數(shù),使maximumPoolSize發(fā)揮作用壮虫,以防止線程被無(wú)限制的分配澳厢,同時(shí)可以通過(guò)提高maximumPoolSize來(lái)提高系統(tǒng)吞吐量
自定義一個(gè)RejectedExecutionHandler,當(dāng)線程數(shù)超過(guò)maximumPoolSize時(shí)進(jìn)行處理囚似,處理方式為隔一段時(shí)間檢查線程池是否可以執(zhí)行新Task剩拢,如果可以把拒絕的Task重新放入到線程池,檢查的時(shí)間依賴(lài)keepAliveTime的大小饶唤。
2.連接池(org.apache.commons.dbcp.BasicDataSource)
在使用org.apache.commons.dbcp.BasicDataSource的時(shí)候徐伐,因?yàn)橹安捎昧四J(rèn)配置,所以當(dāng)訪問(wèn)量大時(shí)募狂,通過(guò)JMX觀察到很多Tomcat線程都阻塞在BasicDataSource使用的Apache ObjectPool的鎖上办素,直接原因當(dāng)時(shí)是因?yàn)锽asicDataSource連接池的最大連接數(shù)設(shè)置的太小,默認(rèn)的BasicDataSource配置祸穷,僅使用8個(gè)最大連接性穿。
我還觀察到一個(gè)問(wèn)題,當(dāng)較長(zhǎng)的時(shí)間不訪問(wèn)系統(tǒng)雷滚,比如2天需曾,DB上的Mysql會(huì)斷掉所以的連接,導(dǎo)致連接池中緩存的連接不能用祈远。為了解決這些問(wèn)題呆万,我們充分研究了BasicDataSource,發(fā)現(xiàn)了一些優(yōu)化的點(diǎn):
Mysql默認(rèn)支持100個(gè)鏈接车份,所以每個(gè)連接池的配置要根據(jù)集群中的機(jī)器數(shù)進(jìn)行谋减,如有2臺(tái)服務(wù)器,可每個(gè)設(shè)置為60
initialSize:參數(shù)是一直打開(kāi)的連接數(shù)
minEvictableIdleTimeMillis:該參數(shù)設(shè)置每個(gè)連接的空閑時(shí)間躬充,超過(guò)這個(gè)時(shí)間連接將被關(guān)閉
timeBetweenEvictionRunsMillis:后臺(tái)線程的運(yùn)行周期逃顶,用來(lái)檢測(cè)過(guò)期連接
maxActive:最大能分配的連接數(shù)
maxIdle:最大空閑數(shù),當(dāng)連接使用完畢后發(fā)現(xiàn)連接數(shù)大于maxIdle充甚,連接將被直接關(guān)閉以政。只有initialSize < x < maxIdle的連接將被定期檢測(cè)是否超期。這個(gè)參數(shù)主要用來(lái)在峰值訪問(wèn)時(shí)提高吞吐量伴找。
initialSize是如何保持的盈蛮?經(jīng)過(guò)研究代碼發(fā)現(xiàn),BasicDataSource會(huì)關(guān)閉所有超期的連接技矮,然后再打開(kāi)initialSize數(shù)量的連接抖誉,這個(gè)特性與minEvictableIdleTimeMillis殊轴、timeBetweenEvictionRunsMillis一起保證了所有超期的initialSize連接都會(huì)被重新連接,從而避免了Mysql長(zhǎng)時(shí)間無(wú)動(dòng)作會(huì)斷掉連接的問(wèn)題袒炉。
3.JVM參數(shù)
在JVM啟動(dòng)參數(shù)中旁理,可以設(shè)置跟內(nèi)存、垃圾回收相關(guān)的一些參數(shù)設(shè)置我磁,默認(rèn)情況不做任何設(shè)置JVM會(huì)工作的很好孽文,但對(duì)一些配置很好的Server和具體的應(yīng)用必須仔細(xì)調(diào)優(yōu)才能獲得最佳性能。通過(guò)設(shè)置我們希望達(dá)到一些目標(biāo):
GC的時(shí)間足夠的小
GC的次數(shù)足夠的少
發(fā)生Full GC的周期足夠的長(zhǎng)
前兩個(gè)目前是相悖的夺艰,要想GC時(shí)間小必須要一個(gè)更小的堆芋哭,要保證GC次數(shù)足夠少,必須保證一個(gè)更大的堆郁副,我們只能取其平衡减牺。
(1)針對(duì)JVM堆的設(shè)置,一般可以通過(guò)-Xms -Xmx限定其最小存谎、最大值拔疚,為了防止垃圾收集器在最小、最大之間收縮堆而產(chǎn)生額外的時(shí)間愕贡,我們通常把最大草雕、最小設(shè)置為相同的值
(2)年輕代和年老代將根據(jù)默認(rèn)的比例(1:2)分配堆內(nèi)存,可以通過(guò)調(diào)整二者之間的比率NewRadio來(lái)調(diào)整二者之間的大小固以,也可以針對(duì)回收代墩虹,比如年輕代,通過(guò) -XX:newSize -XX:MaxNewSize來(lái)設(shè)置其絕對(duì)大小憨琳。同樣诫钓,為了防止年輕代的堆收縮,我們通常會(huì)把-XX:newSize -XX:MaxNewSize設(shè)置為同樣大小
(3)年輕代和年老代設(shè)置多大才算合理篙螟?這個(gè)我問(wèn)題毫無(wú)疑問(wèn)是沒(méi)有答案的菌湃,否則也就不會(huì)有調(diào)優(yōu)。我們觀察一下二者大小變化有哪些影響
更大的年輕代必然導(dǎo)致更小的年老代遍略,大的年輕代會(huì)延長(zhǎng)普通GC的周期惧所,但會(huì)增加每次GC的時(shí)間;小的年老代會(huì)導(dǎo)致更頻繁的Full GC
更小的年輕代必然導(dǎo)致更大年老代绪杏,小的年輕代會(huì)導(dǎo)致普通GC很頻繁下愈,但每次的GC時(shí)間會(huì)更短;大的年老代會(huì)減少Full GC的頻率
如何選擇應(yīng)該依賴(lài)應(yīng)用程序對(duì)象生命周期的分布情況:如果應(yīng)用存在大量的臨時(shí)對(duì)象蕾久,應(yīng)該選擇更大的年輕代势似;如果存在相對(duì)較多的持久對(duì)象,年老代應(yīng)該適當(dāng)增大。但很多應(yīng)用都沒(méi)有這樣明顯的特性履因,在抉擇時(shí)應(yīng)該根據(jù)以下兩點(diǎn):(A)本著Full GC盡量少的原則障簿,讓年老代盡量緩存常用對(duì)象,JVM的默認(rèn)比例1:2也是這個(gè)道理 (B)通過(guò)觀察應(yīng)用一段時(shí)間栅迄,看其他在峰值時(shí)年老代會(huì)占多少內(nèi)存站故,在不影響Full GC的前提下,根據(jù)實(shí)際情況加大年輕代霞篡,比如可以把比例控制在1:1世蔗。但應(yīng)該給年老代至少預(yù)留1/3的增長(zhǎng)空間
(4)在配置較好的機(jī)器上(比如多核端逼、大內(nèi)存)朗兵,可以為年老代選擇并行收集算法:?-XX:+UseParallelOldGC?,默認(rèn)為Serial收集
(5)線程堆棧的設(shè)置:每個(gè)線程默認(rèn)會(huì)開(kāi)啟1M的堆棧顶滩,用于存放棧幀余掖、調(diào)用參數(shù)、局部變量等礁鲁,對(duì)大多數(shù)應(yīng)用而言這個(gè)默認(rèn)值太了盐欺,一般256K就足用。理論上仅醇,在內(nèi)存不變的情況下冗美,減少每個(gè)線程的堆棧,可以產(chǎn)生更多的線程析二,但這實(shí)際上還受限于操作系統(tǒng)粉洼。
(4)可以通過(guò)下面的參數(shù)打Heap Dump信息
-XX:HeapDumpPath
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/usr/aaa/dump/heap_trace.txt
通過(guò)下面參數(shù)可以控制OutOfMemoryError時(shí)打印堆的信息
-XX:+HeapDumpOnOutOfMemoryError
請(qǐng)看一下一個(gè)時(shí)間的Java參數(shù)配置:(服務(wù)器:Linux 64Bit,8Core×16G)
JAVA_OPTS="$JAVA_OPTS -server -Xms3G -Xmx3G -Xss256k -XX:PermSize=128m -XX:MaxPermSize=128m -XX:+UseParallelOldGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/aaa/dump -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/usr/aaa/dump/heap_trace.txt -XX:NewSize=1G -XX:MaxNewSize=1G"
經(jīng)過(guò)觀察該配置非常穩(wěn)定叶摄,每次普通GC的時(shí)間在10ms左右属韧,F(xiàn)ull GC基本不發(fā)生,或隔很長(zhǎng)很長(zhǎng)的時(shí)間才發(fā)生一次
通過(guò)分析dump文件可以發(fā)現(xiàn)蛤吓,每個(gè)1小時(shí)都會(huì)發(fā)生一次Full GC宵喂,經(jīng)過(guò)多方求證,只要在JVM中開(kāi)啟了JMX服務(wù)会傲,JMX將會(huì)1小時(shí)執(zhí)行一次Full GC以清除引用
調(diào)優(yōu)方法
一切都是為了這一步锅棕,調(diào)優(yōu),在調(diào)優(yōu)之前淌山,我們需要記住下面的原則:
1裸燎、多數(shù)的Java應(yīng)用不需要在服務(wù)器上進(jìn)行GC優(yōu)化;
2艾岂、多數(shù)導(dǎo)致GC問(wèn)題的Java應(yīng)用顺少,都不是因?yàn)槲覀儏?shù)設(shè)置錯(cuò)誤,而是代碼問(wèn)題;
3脆炎、在應(yīng)用上線之前梅猿,先考慮將機(jī)器的JVM參數(shù)設(shè)置到最優(yōu)(最適合);
4秒裕、減少創(chuàng)建對(duì)象的數(shù)量袱蚓;
5、減少使用全局變量和大對(duì)象几蜻;
6喇潘、GC優(yōu)化是到最后不得已才采用的手段;
7梭稚、在實(shí)際使用中颖低,分析GC情況優(yōu)化代碼比優(yōu)化GC參數(shù)要多得多;
GC優(yōu)化的目的有兩個(gè):
1弧烤、將轉(zhuǎn)移到老年代的對(duì)象數(shù)量降低到最谐佬肌;
2暇昂、減少full GC的執(zhí)行時(shí)間莺戒;
為了達(dá)到上面的目的,一般地急波,你需要做的事情有:
1从铲、減少使用全局變量和大對(duì)象;
2澄暮、調(diào)整新生代的大小到最合適名段;
3、設(shè)置老年代的大小為最合適赏寇;
4吉嫩、選擇合適的GC收集器;
在上面的4條方法中嗅定,用了幾個(gè)“合適”自娩,那究竟什么才算合適,一般的渠退,請(qǐng)參考上面“收集器搭配”和“啟動(dòng)內(nèi)存分配”兩節(jié)中的建議忙迁。但這些建議不是萬(wàn)能的,需要根據(jù)您的機(jī)器和應(yīng)用情況進(jìn)行發(fā)展和變化碎乃,實(shí)際操作中姊扔,可以將兩臺(tái)機(jī)器分別設(shè)置成不同的GC參數(shù),并且進(jìn)行對(duì)比梅誓,選用那些確實(shí)提高了性能或減少了GC時(shí)間的參數(shù)恰梢。
真正熟練的使用GC調(diào)優(yōu)佛南,是建立在多次進(jìn)行GC監(jiān)控和調(diào)優(yōu)的實(shí)戰(zhàn)經(jīng)驗(yàn)上的,進(jìn)行監(jiān)控和調(diào)優(yōu)的一般步驟為:
1嵌言,監(jiān)控GC的狀態(tài)
使用各種JVM工具嗅回,查看當(dāng)前日志,分析當(dāng)前JVM參數(shù)設(shè)置摧茴,并且分析當(dāng)前堆內(nèi)存快照和gc日志绵载,根據(jù)實(shí)際的各區(qū)域內(nèi)存劃分和GC執(zhí)行時(shí)間,覺(jué)得是否進(jìn)行優(yōu)化苛白;
2娃豹,分析結(jié)果,判斷是否需要優(yōu)化
如果各項(xiàng)參數(shù)設(shè)置合理购裙,系統(tǒng)沒(méi)有超時(shí)日志出現(xiàn)懂版,GC頻率不高,GC耗時(shí)不高缓窜,那么沒(méi)有必要進(jìn)行GC優(yōu)化定续;如果GC時(shí)間超過(guò)1-3秒,或者頻繁GC禾锤,則必須優(yōu)化;
注:如果滿(mǎn)足下面的指標(biāo)摹察,則一般不需要進(jìn)行GC:
Minor GC執(zhí)行時(shí)間不到50ms恩掷;
Minor GC執(zhí)行不頻繁,約10秒一次供嚎;
Full GC執(zhí)行時(shí)間不到1s黄娘;
Full GC執(zhí)行頻率不算頻繁,不低于10分鐘1次克滴;
3逼争,調(diào)整GC類(lèi)型和內(nèi)存分配
如果內(nèi)存分配過(guò)大或過(guò)小,或者采用的GC收集器比較慢劝赔,則應(yīng)該優(yōu)先調(diào)整這些參數(shù)誓焦,并且先找1臺(tái)或幾臺(tái)機(jī)器進(jìn)行beta,然后比較優(yōu)化過(guò)的機(jī)器和沒(méi)有優(yōu)化的機(jī)器的性能對(duì)比着帽,并有針對(duì)性的做出最后選擇杂伟;
4,不斷的分析和調(diào)整
通過(guò)不斷的試驗(yàn)和試錯(cuò)仍翰,分析并找到最合適的參數(shù)
5赫粥,全面應(yīng)用參數(shù)
如果找到了最合適的參數(shù),則將這些參數(shù)應(yīng)用到所有服務(wù)器予借,并進(jìn)行后續(xù)跟蹤越平。
調(diào)優(yōu)實(shí)例
上面的內(nèi)容都是紙上談兵频蛔,下面我們以一些真實(shí)例子來(lái)進(jìn)行說(shuō)明:
實(shí)例1:
筆者昨日發(fā)現(xiàn)部分開(kāi)發(fā)測(cè)試機(jī)器出現(xiàn)異常:java.lang.OutOfMemoryError: GC overhead limit exceeded,這個(gè)異常代表:
GC為了釋放很小的空間卻耗費(fèi)了太多的時(shí)間秦叛,其原因一般有兩個(gè):1帽驯,堆太小,2书闸,有死循環(huán)或大對(duì)象尼变;
筆者首先排除了第2個(gè)原因,因?yàn)檫@個(gè)應(yīng)用同時(shí)是在線上運(yùn)行的浆劲,如果有問(wèn)題嫌术,早就掛了。所以懷疑是這臺(tái)機(jī)器中堆設(shè)置太信平琛度气;
使用ps -ef |grep "java"查看,發(fā)現(xiàn):
該應(yīng)用的堆區(qū)設(shè)置只有768m膨报,而機(jī)器內(nèi)存有2g磷籍,機(jī)器上只跑這一個(gè)java應(yīng)用,沒(méi)有其他需要占用內(nèi)存的地方现柠。另外院领,這個(gè)應(yīng)用比較大,需要占用的內(nèi)存也比較多够吩;
筆者通過(guò)上面的情況判斷比然,只需要改變堆中各區(qū)域的大小設(shè)置即可,于是改成下面的情況:
跟蹤運(yùn)行情況發(fā)現(xiàn)周循,相關(guān)異常沒(méi)有再出現(xiàn)强法;
實(shí)例2:
一個(gè)服務(wù)系統(tǒng),經(jīng)常出現(xiàn)卡頓湾笛,分析原因饮怯,發(fā)現(xiàn)Full GC時(shí)間太長(zhǎng):
jstat -gcutil:
S0 S1 E O P YGC YGCT FGC FGCT GCT
12.16 0.00 5.18 63.78 20.32 54 2.047 5 6.946 8.993
分析上面的數(shù)據(jù),發(fā)現(xiàn)Young GC執(zhí)行了54次嚎研,耗時(shí)2.047秒蓖墅,每次Young GC耗時(shí)37ms,在正常范圍嘉赎,而Full GC執(zhí)行了5次置媳,耗時(shí)6.946秒,每次平均1.389s公条,數(shù)據(jù)顯示出來(lái)的問(wèn)題是:Full GC耗時(shí)較長(zhǎng)拇囊,分析該系統(tǒng)的是指發(fā)現(xiàn),NewRatio=9靶橱,也就是說(shuō)寥袭,新生代和老生代大小之比為1:9路捧,這就是問(wèn)題的原因:
1,新生代太小传黄,導(dǎo)致對(duì)象提前進(jìn)入老年代杰扫,觸發(fā)老年代發(fā)生Full GC;
2膘掰,老年代較大章姓,進(jìn)行Full GC時(shí)耗時(shí)較大;
優(yōu)化的方法是調(diào)整NewRatio的值识埋,調(diào)整到4凡伊,發(fā)現(xiàn)Full GC沒(méi)有再發(fā)生,只有Young GC在執(zhí)行窒舟。這就是把對(duì)象控制在新生代就清理掉系忙,沒(méi)有進(jìn)入老年代(這種做法對(duì)一些應(yīng)用是很有用的,但并不是對(duì)所有應(yīng)用都要這么做)
實(shí)例3:
一應(yīng)用在性能測(cè)試過(guò)程中惠豺,發(fā)現(xiàn)內(nèi)存占用率很高银还,F(xiàn)ull GC頻繁,使用sudo -u admin -H jmap -dump:format=b,file=文件名.hprof pid 來(lái)dump內(nèi)存洁墙,生成dump文件蛹疯,并使用Eclipse下的mat差距進(jìn)行分析,發(fā)現(xiàn):
從圖中可以看出扫俺,這個(gè)線程存在問(wèn)題苍苞,隊(duì)列LinkedBlockingQueue所引用的大量對(duì)象并未釋放,導(dǎo)致整個(gè)線程占用內(nèi)存高達(dá)378m狼纬,此時(shí)通知開(kāi)發(fā)人員進(jìn)行代碼優(yōu)化,將相關(guān)對(duì)象釋放掉即可骂际。
這里給大家提供一個(gè)學(xué)習(xí)交流的平臺(tái)疗琉,java架構(gòu)師群:671017482
具有1-5工作經(jīng)驗(yàn)的,面對(duì)目前流行的技術(shù)不知從何下手歉铝,需要突破技術(shù)瓶頸的可以加群盈简。
在公司待久了,過(guò)得很安逸太示,但跳槽時(shí)面試碰壁柠贤。需要在短時(shí)間內(nèi)進(jìn)修、跳槽拿高薪的可以加群类缤。
如果沒(méi)有工作經(jīng)驗(yàn)臼勉,但基礎(chǔ)非常扎實(shí),對(duì)java工作機(jī)制餐弱,常用設(shè)計(jì)思想宴霸,常用java開(kāi)發(fā)框架掌握熟練的可以加群囱晴。