JVM性能調優
部落格分類: JVM
JVM垃圾回收與性能調優總結
JVM調優的幾種政策
一、JVM記憶體模型及垃圾收集算法
根據Java虛拟機規範,JVM将記憶體劃分為:
New(年輕代)
Tenured(年老代)
永久代(Perm)
其中New和Tenured屬于堆記憶體,堆記憶體會從JVM啟動參數(-Xmx:G)指定的記憶體中配置設定,Perm不屬于堆記憶體,有虛拟機直接配置設定,但可以通過-XX:PermSize -XX:MaxPermSize 等參數調整其大小。
年輕代(New):年輕代用來存放JVM剛配置設定的Java對象
年老代(Tenured):年輕代中經過垃圾回收沒有回收掉的對象将被Copy到年老代
永久代(Perm):永久代存放Class、Method元資訊,其大小跟項目的規模、類、方法的量有關,一般設定為M就足夠,設定原則是預留%的空間。
New又分為幾個部分:
Eden:Eden用來存放JVM剛配置設定的對象
Survivor1
Survivro2:兩個Survivor空間一樣大,當Eden中的對象經過垃圾回收沒有被回收掉時,會在兩個Survivor之間來回Copy,當滿足某個條件,比如Copy次數,就會被Copy到Tenured。顯然,Survivor隻是增加了對象在年輕代中的逗留時間,增加了被垃圾回收的可能性。
垃圾回收算法
垃圾回收算法可以分為三類,都基于标記-清除(複制)算法:
Serial算法(單線程)
并行算法
并發算法
JVM會根據機器的硬體配置對每個記憶體代選擇适合的回收算法,比如,如果機器多于個核,會對年輕代選擇并行算法,關于選擇細節請參考JVM調優文檔。
稍微解釋下的是,并行算法是用多線程進行垃圾回收,回收期間會暫停程式的執行,而并發算法,也是多線程回收,但期間不停止應用執行。是以,并發算法适用于互動性高的一些程式。經過觀察,并發算法會減少年輕代的大小,其實就是使用了一個大的年老代,這反過來跟并行算法相比吞吐量相對較低。
還有一個問題是,垃圾回收動作何時執行?
當年輕代記憶體滿時,會引發一次普通GC,該GC僅回收年輕代。需要強調的時,年輕代滿是指Eden代滿,Survivor滿不會引發GC
當年老代滿時會引發Full GC,Full GC将會同時回收年輕代、年老代
當永久代滿時也會引發Full GC,會導緻Class、Method元資訊的解除安裝
另一個問題是,何時會抛出OutOfMemoryException,并不是記憶體被耗空的時候才抛出
JVM98%的時間都花費在記憶體回收
每次回收的記憶體小于%
滿足這兩個條件将觸發OutOfMemoryException,這将會留給系統一個微小的間隙以做一些Down之前的操作,比如手動列印Heap Dump。
二、記憶體洩漏及解決方法
系統崩潰前的一些現象:
每次垃圾回收的時間越來越長,由之前的ms延長到ms左右,FullGC的時間也有之前的s延長到、s
FullGC的次數越來越多,最頻繁時隔不到分鐘就進行一次FullGC
年老代的記憶體越來越大并且每次FullGC後年老代沒有記憶體被釋放
之後系統會無法響應新的請求,逐漸到達OutOfMemoryError的臨界值。
生成堆的dump檔案
通過JMX的MBean生成目前的Heap資訊,大小為一個G(整個堆的大小)的hprof檔案,如果沒有啟動JMX可以通過Java的jmap指令來生成該檔案。
分析dump檔案
下面要考慮的是如何打開這個G的堆資訊檔案,顯然一般的Window系統沒有這麼大的記憶體,必須借助高配置的Linux。當然我們可以借助X-Window把Linux上的圖形導入到Window。我們考慮用下面幾種工具打開該檔案:
Visual VM
IBM HeapAnalyzer
JDK 自帶的Hprof工具
使用這些工具時為了確定加載速度,建議設定最大記憶體為G。使用後發現,這些工具都無法直覺地觀察到記憶體洩漏,Visual VM雖能觀察到對象大小,但看不到調用堆棧;HeapAnalyzer雖然能看到調用堆棧,卻無法正确打開一個G的檔案。是以,我們又選用了Eclipse專門的靜态記憶體分析工具:Mat。
分析記憶體洩漏
通過Mat我們能清楚地看到,哪些對象被懷疑為記憶體洩漏,哪些對象占的空間最大及對象的調用關系。針對本案,在ThreadLocal中有很多的JbpmContext執行個體,經過調查是JBPM的Context沒有關閉所緻。
另,通過Mat或JMX我們還可以分析線程狀态,可以觀察到線程被阻塞在哪個對象上,進而判斷系統的瓶頸。
回歸問題
Q:為什麼崩潰前垃圾回收的時間越來越長?
A:根據記憶體模型和垃圾回收算法,垃圾回收分兩部分:記憶體标記、清除(複制),标記部分隻要記憶體大小固定時間是不變的,變的是複制部分,因為每次垃圾回收都有一些回收不掉的記憶體,是以增加了複制量,導緻時間延長。是以,垃圾回收的時間也可以作為判斷記憶體洩漏的依據
Q:為什麼Full GC的次數越來越多?
A:是以記憶體的積累,逐漸耗盡了年老代的記憶體,導緻新對象配置設定沒有更多的空間,進而導緻頻繁的垃圾回收
Q:為什麼年老代占用的記憶體越來越大?
A:因為年輕代的記憶體無法被回收,越來越多地被Copy到年老代
三、性能調優
除了上述記憶體洩漏外,我們還發現CPU長期不足%,系統吞吐量不夠,針對core×G、bit的Linux伺服器來說,是嚴重的資源浪費。
在CPU負載不足的同時,偶爾會有使用者反映請求的時間過長,我們意識到必須對程式及JVM進行調優。從以下幾個方面進行:
線程池:解決使用者響應時間長的問題
連接配接池
JVM啟動參數:調整各代的記憶體比例和垃圾回收算法,提高吞吐量
程式算法:改程序式邏輯算法提高性能
Java線程池(java.util.concurrent.ThreadPoolExecutor)
大多數JVM6上的應用采用的線程池都是JDK自帶的線程池,之是以把成熟的Java線程池進行羅嗦說明,是因為該線程池的行為與我們想象的有點出入。Java線程池有幾個重要的配置參數:
corePoolSize:核心線程數(最新線程數)
maximumPoolSize:最大線程數,超過這個數量的任務會被拒絕,使用者可以通過RejectedExecutionHandler接口自定義處理方式
keepAliveTime:線程保持活動的時間
workQueue:工作隊列,存放執行的任務
Java線程池需要傳入一個Queue參數(workQueue)用來存放執行的任務,而對Queue的不同選擇,線程池有完全不同的行為:
SynchronousQueue: 一個無容量的等待隊列,一個線程的insert操作必須等待另一線程的remove操作,采用這個Queue線程池将會為每個任務配置設定一個新線程
LinkedBlockingQueue : 無界隊列,采用該Queue,線程池将忽略 maximumPoolSize參數,僅用corePoolSize的線程處理所有的任務,未處理的任務便在LinkedBlockingQueue中排隊
ArrayBlockingQueue: 有界隊列,在有界隊列和 maximumPoolSize的作用下,程式将很難被調優:更大的Queue和小的maximumPoolSize将導緻CPU的低負載;小的Queue和大的池,Queue就沒起動應有的作用。
其實我們的要求很簡單,希望線程池能跟連接配接池一樣,能設定最小線程數、最大線程數,當最小數<任務<最大數時,應該配置設定新的線程處理;當任務>最大數時,應該等待有空閑線程再處理該任務。
但線程池的設計思路是,任務應該放到Queue中,當Queue放不下時再考慮用新線程處理,如果Queue滿且無法派生新線程,就拒絕該任務。設計導緻“先放等執行”、“放不下再執行”、“拒絕不等待”。是以,根據不同的Queue參數,要提高吞吐量不能一味地增大maximumPoolSize。
當然,要達到我們的目标,必須對線程池進行一定的封裝,幸運的是ThreadPoolExecutor中留了足夠的自定義接口以幫助我們達到目标。我們封裝的方式是:
以SynchronousQueue作為參數,使maximumPoolSize發揮作用,以防止線程被無限制的配置設定,同時可以通過提高maximumPoolSize來提高系統吞吐量
自定義一個RejectedExecutionHandler,當線程數超過maximumPoolSize時進行處理,處理方式為隔一段時間檢查線程池是否可以執行新Task,如果可以把拒絕的Task重新放入到線程池,檢查的時間依賴keepAliveTime的大小。
連接配接池(org.apache.commons.dbcp.BasicDataSource)
在使用org.apache.commons.dbcp.BasicDataSource的時候,因為之前采用了預設配置,是以當通路量大時,通過JMX觀察到很多Tomcat線程都阻塞在BasicDataSource使用的Apache ObjectPool的鎖上,直接原因當時是因為BasicDataSource連接配接池的最大連接配接數設定的太小,預設的BasicDataSource配置,僅使用個最大連接配接。
我還觀察到一個問題,當較長的時間不通路系統,比如天,DB上的Mysql會斷掉是以的連接配接,導緻連接配接池中緩存的連接配接不能用。為了解決這些問題,我們充分研究了BasicDataSource,發現了一些優化的點:
Mysql預設支援個連結,是以每個連接配接池的配置要根據叢集中的機器數進行,如有台伺服器,可每個設定為
initialSize:參數是一直打開的連接配接數
minEvictableIdleTimeMillis:該參數設定每個連接配接的空閑時間,超過這個時間連接配接将被關閉
timeBetweenEvictionRunsMillis:背景線程的運作周期,用來檢測過期連接配接
maxActive:最大能配置設定的連接配接數
maxIdle:最大空閑數,當連接配接使用完畢後發現連接配接數大于maxIdle,連接配接将被直接關閉。隻有initialSize < x < maxIdle的連接配接将被定期檢測是否超期。這個參數主要用來在峰值通路時提高吞吐量。
initialSize是如何保持的?經過研究代碼發現,BasicDataSource會關閉所有超期的連接配接,然後再打開initialSize數量的連接配接,這個特性與minEvictableIdleTimeMillis、timeBetweenEvictionRunsMillis一起保證了所有超期的initialSize連接配接都會被重新連接配接,進而避免了Mysql長時間無動作會斷掉連接配接的問題。
JVM參數
在JVM啟動參數中,可以設定跟記憶體、垃圾回收相關的一些參數設定,預設情況不做任何設定JVM會工作的很好,但對一些配置很好的Server和具體的應用必須仔細調優才能獲得最佳性能。通過設定我們希望達到一些目标:
GC的時間足夠的小
GC的次數足夠的少
發生Full GC的周期足夠的長
前兩個目前是相悖的,要想GC時間小必須要一個更小的堆,要保證GC次數足夠少,必須保證一個更大的堆,我們隻能取其平衡。
()針對JVM堆的設定,一般可以通過-Xms -Xmx限定其最小、最大值,為了防止垃圾收集器在最小、最大之間收縮堆而産生額外的時間,我們通常把最大、最小設定為相同的值
()年輕代和年老代将根據預設的比例(:)配置設定堆記憶體,可以通過調整二者之間的比率NewRadio來調整二者之間的大小,也可以針對回收代,比如年輕代,通過 -XX:newSize -XX:MaxNewSize來設定其絕對大小。同樣,為了防止年輕代的堆收縮,我們通常會把-XX:newSize -XX:MaxNewSize設定為同樣大小
()年輕代和年老代設定多大才算合理?這個我問題毫無疑問是沒有答案的,否則也就不會有調優。我們觀察一下二者大小變化有哪些影響
更大的年輕代必然導緻更小的年老代,大的年輕代會延長普通GC的周期,但會增加每次GC的時間;小的年老代會導緻更頻繁的Full GC
更小的年輕代必然導緻更大年老代,小的年輕代會導緻普通GC很頻繁,但每次的GC時間會更短;大的年老代會減少Full GC的頻率
如何選擇應該依賴應用程式對象生命周期的分布情況:如果應用存在大量的臨時對象,應該選擇更大的年輕代;如果存在相對較多的持久對象,年老代應該适當增大。但很多應用都沒有這樣明顯的特性,在抉擇時應該根據以下兩點:(A)本着Full GC盡量少的原則,讓年老代盡量緩存常用對象,JVM的預設比例:也是這個道理 (B)通過觀察應用一段時間,看其他在峰值時年老代會占多少記憶體,在不影響Full GC的前提下,根據實際情況加大年輕代,比如可以把比例控制在:。但應該給年老代至少預留/的增長空間
()在配置較好的機器上(比如多核、大記憶體),可以為年老代選擇并行收集算法: -XX:+UseParallelOldGC ,預設為Serial收集
()線程堆棧的設定:每個線程預設會開啟M的堆棧,用于存放棧幀、調用參數、局部變量等,對大多數應用而言這個預設值太了,一般K就足用。理論上,在記憶體不變的情況下,減少每個線程的堆棧,可以産生更多的線程,但這實際上還受限于作業系統。
()可以通過下面的參數打Heap Dump資訊
-XX:HeapDumpPath
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/usr/aaa/dump/heap_trace.txt
通過下面參數可以控制OutOfMemoryError時列印堆的資訊
-XX:+HeapDumpOnOutOfMemoryError
請看一下一個時間的Java參數配置:(伺服器:Linux Bit,Core×G)
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"
經過觀察該配置非常穩定,每次普通GC的時間在ms左右,Full GC基本不發生,或隔很長很長的時間才發生一次
通過分析dump檔案可以發現,每個小時都會發生一次Full GC,經過多方求證,隻要在JVM中開啟了JMX服務,JMX将會小時執行一次Full GC以清除引用,關于這點請參考附件文檔。
程式算法調優:本次不作為重點
參考資料:
http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html
來源:http://blog.csdn.net/chen77716/article/details/5695893
=======================================================================================
調優方法
一切都是為了這一步,調優,在調優之前,我們需要記住下面的原則:
、多數的Java應用不需要在伺服器上進行GC優化;
、多數導緻GC問題的Java應用,都不是因為我們參數設定錯誤,而是代碼問題;
、在應用上線之前,先考慮将機器的JVM參數設定到最優(最适合);
、減少建立對象的數量;
、減少使用全局變量和大對象;
、GC優化是到最後不得已才采用的手段;
、在實際使用中,分析GC情況優化代碼比優化GC參數要多得多;
GC優化的目的有兩個(http://www.360doc.com/content/13/0305/10/15643_269388816.shtml):
、将轉移到老年代的對象數量降低到最小;
、減少full GC的執行時間;
為了達到上面的目的,一般地,你需要做的事情有:
、減少使用全局變量和大對象;
、調整新生代的大小到最合适;
、設定老年代的大小為最合适;
、選擇合适的GC收集器;
在上面的條方法中,用了幾個“合适”,那究竟什麼才算合适,一般的,請參考上面“收集器搭配”和“啟動記憶體配置設定”兩節中的建議。但這些建議不是萬能的,需要根據您的機器和應用情況進行發展和變化,實際操作中,可以将兩台機器分别設定成不同的GC參數,并且進行對比,選用那些确實提高了性能或減少了GC時間的參數。
真正熟練的使用GC調優,是建立在多次進行GC監控和調優的實戰經驗上的,進行監控和調優的一般步驟為:
,監控GC的狀态
使用各種JVM工具,檢視目前日志,分析目前JVM參數設定,并且分析目前堆記憶體快照和gc日志,根據實際的各區域記憶體劃分和GC執行時間,覺得是否進行優化;
,分析結果,判斷是否需要優化
如果各項參數設定合理,系統沒有逾時日志出現,GC頻率不高,GC耗時不高,那麼沒有必要進行GC優化;如果GC時間超過-秒,或者頻繁GC,則必須優化;
注:如果滿足下面的名額,則一般不需要進行GC:
Minor GC執行時間不到ms;
Minor GC執行不頻繁,約秒一次;
Full GC執行時間不到s;
Full GC執行頻率不算頻繁,不低于分鐘次;
,調整GC類型和記憶體配置設定
如果記憶體配置設定過大或過小,或者采用的GC收集器比較慢,則應該優先調整這些參數,并且先找台或幾台機器進行beta,然後比較優化過的機器和沒有優化的機器的性能對比,并有針對性的做出最後選擇;
,不斷的分析和調整
通過不斷的試驗和試錯,分析并找到最合适的參數
,全面應用參數
如果找到了最合适的參數,則将這些參數應用到所有伺服器,并進行後續跟蹤。
調優執行個體
上面的内容都是紙上談兵,下面我們以一些真執行個體子來進行說明:
執行個體:
筆者昨日發現部分開發測試機器出現異常:java.lang.OutOfMemoryError: GC overhead limit exceeded,這個異常代表:
GC為了釋放很小的空間卻耗費了太多的時間,其原因一般有兩個:,堆太小,,有死循環或大對象;
筆者首先排除了第個原因,因為這個應用同時是線上上運作的,如果有問題,早就挂了。是以懷疑是這台機器中堆設定太小;
使用ps -ef |grep "java"檢視,發現:
該應用的堆區設定隻有m,而機器記憶體有g,機器上隻跑這一個java應用,沒有其他需要占用記憶體的地方。另外,這個應用比較大,需要占用的記憶體也比較多;
筆者通過上面的情況判斷,隻需要改變堆中各區域的大小設定即可,于是改成下面的情況:
跟蹤運作情況發現,相關異常沒有再出現;
執行個體:(http://www.360doc.com/content/13/0305/10/15643_269388816.shtml)
一個服務系統,經常出現卡頓,分析原因,發現Full GC時間太長:
jstat -gcutil:
S0 S1 E O P YGC YGCT FGC FGCT GCT
分析上面的資料,發現Young GC執行了次,耗時秒,每次Young GC耗時ms,在正常範圍,而Full GC執行了次,耗時秒,每次平均s,資料顯示出來的問題是:Full GC耗時較長,分析該系統的是指發現,NewRatio=,也就是說,新生代和老生代大小之比為:,這就是問題的原因:
,新生代太小,導緻對象提前進入老年代,觸發老年代發生Full GC;
,老年代較大,進行Full GC時耗時較大;
優化的方法是調整NewRatio的值,調整到,發現Full GC沒有再發生,隻有Young GC在執行。這就是把對象控制在新生代就清理掉,沒有進入老年代(這種做法對一些應用是很有用的,但并不是對所有應用都要這麼做)
執行個體:
一應用在性能測試過程中,發現記憶體占用率很高,Full GC頻繁,使用sudo -u admin -H jmap -dump:format=b,file=檔案名.hprof pid 來dump記憶體,生成dump檔案,并使用Eclipse下的mat差距進行分析,發現:
從圖中可以看出,這個線程存在問題,隊列LinkedBlockingQueue所引用的大量對象并未釋放,導緻整個線程占用記憶體高達m,此時通知開發人員進行代碼優化,将相關對象釋放掉即可。
來源:Java系列筆記() - JVM監控與調優
原文