Java性能調優筆記

Java性能調優筆記(http://blog.csdn.net/yang_net/article/details/5830820#comments
調優步驟:衡量系統現狀、設定調優目標、尋找性能瓶頸、性能調優、衡量是否到達目標(如果未到達目標,需重新尋找性能瓶頸)、性能調優結束。
一、尋找性能瓶頸
性能瓶頸的表象:資源消耗過多、外部處理系統的性能不足、資源消耗不多但程序的響應速度卻仍達不到要求。
資源消耗:CPU、文件IO、網絡IO、內存。
外部處理系統的性能不足:所調用的其他系統提供的功能或數據庫操作的響應速度不夠。
資源消耗不多但程序的響應速度卻仍達不到要求:程序代碼運行效率不夠高、未充分使用資源、程序結構不合理。
CPU消耗分析
CPU主要用於中斷、內核、用戶進程的任務處理,優先級爲中斷>內核>用戶進程。
上下文切換:
每個線程分配一定的執行時間,當到達執行時間、線程中有IO阻塞或高優先級線程要執行時,將切換執行的線程。在切換時要存儲目前線程的執行狀態,並恢復要執行的線程的狀態。
對於Java應用,典型的是在進行文件IO操作、網絡IO操作、鎖等待、線程Sleep時,當前線程會進入阻塞或休眠狀態,從而觸發上下文切換,上下文切換過多會造成內核佔據較多的CPU的使用。
運行隊列:
每個CPU核都維護一個可運行的線程隊列。系統的load主要由CPU的運行隊列來決定。
運行隊列值越大,就意味着線程會要消耗越長的時間才能執行完成。
利用率:
CPU在用戶進程、內核、中斷處理、IO等待、空閒,這五個部分使用百分比。
文件IO消耗分析
Linux在操作文件時,將數據放入文件緩存區,直到內存不夠或系統要釋放內存給用戶進程使用。所以通常情況下只有寫文件和第一次讀取文件時會產生真正的文件IO。
對於Java應用,造成文件IO消耗高主要是多個線程需要進行大量內容寫入(例如頻繁的日誌寫入)的動作、磁盤設備本身的處理速度慢、文件系統慢、操作的文件本身已經很大。
網絡IO消耗分析
對於分佈式Java應用,網卡中斷是不是均衡分配到各CPU(cat/proc/interrupts查看)。
內存消耗分析(-Xms和-Xmx設爲相同的值,避免運行期JVM堆內存要不斷申請內存)
對於Java應用,內存的消耗主要在Java堆內存上,只有創建線程和使用Direct ByteBuffer纔會操作JVM堆外的內存。
JVM內存消耗過多會導致GC執行頻繁,CPU消耗增加,應用線程的執行速度嚴重下降,甚至造成OutOfMemoryError,最終導致Java進程退出。
JVM堆外的內存
swap的消耗、物理內存的消耗、JVM內存的消耗。
程序執行慢原因分析
鎖競爭激烈:很多線程競爭互斥資源,但資源有限, 造成其他線程都處於等待狀態。
未充分使用硬件資源:線程操作被串行化。
數據量增長:單表數據量太大(如1個億)造成數據庫讀寫速度大幅下降(操作此表)。
調優
JVM調優(最關鍵參數爲:-Xms -Xmx -Xmn -XX:SurvivorRatio -XX:MaxTenuringThreshold)
代大小調優:避免新生代大小設置過小、避免新生代大小設置過大、避免Survivor設置過小或過大、合理設置新生代存活週期。
-Xmn 調整新生代大小,新生代越大通常也意味着更多對象會在minor GC階段被回收,但可能有可能造成舊生代大小,造成頻繁觸發Full GC,甚至是OutOfMemoryError。
-XX:SurvivorRatio調整Eden區與Survivor區的大小,Eden 區越大通常也意味着minor GC發生頻率越低,但可能有可能造成Survivor區太小,導致對象minor GC後就直接進入舊生代,從而更頻繁觸發Full GC。
GC策略的調優:CMS GC多數動作是和應用併發進行的,確實可以減小GC動作給應用造成的暫停時間。對於Web應用非常需要一個對應用造成暫停時間短的GC,再加上Web應用的瓶頸都不在CPU上,在G1還不夠成熟的情況下,CMS GC是不錯的選擇。
(如果系統不是CPU密集型,且從新生代進入舊生代的大部分對象是可以回收的,那麼採用CMS GC可以更好地在舊生代滿之前完成對象的回收,更大程度降低Full GC發生的可能)
在調整了內存管理方面的參數後應通過-XX:PrintGCDetails、-XX:+PrintGCTimeStamps、 -XX:+PrintGCApplicationStoppedTime以及jstat或visualvm等方式觀察調整後的GC狀況。
出內存管理以外的其他方面的調優參數:-XX:CompileThreshold、-XX:+UseFastAccessorMethods、 -XX:+UseBaiasedLocking。
程序調優
CPU消耗嚴重的解決方法
CPU us高的解決方法:
CPU us 高的原因主要是執行線程不需要任何掛起動作,且一直執行,導致CPU 沒有機會去調度執行其他的線程。
調優方案: 增加Thread.sleep,以釋放CPU 的執行權,降低CPU 的消耗。以損失單次執行性能爲代價的,但由於其降低了CPU 的消耗,對於多線程的應用而言,反而提高了總體的平均性能。
(在實際的Java應用中類似場景, 對於這種場景最佳方式是改爲採用wait/notify機制)
對於其他類似循環次數過多、正則、計算等造成CPU us過高的狀況, 則需要結合業務調優。
對於GC頻繁,則需要通過JVM調優或程序調優,降低GC的執行次數。
CPU sy高的解決方法:
CPU sy 高的原因主要是線程的運行狀態要經常切換,對於這種情況,常見的一種優化方法是減少線程數。
調優方案: 將線程數降低
這種調優過後有可能會造成CPU us過高,所以合理設置線程數非常關鍵。
對於Java分佈式應用,還有一種典型現象是應用中有較多的網絡IO操作和確實需要一些鎖競爭機制(如數據庫連接池),但爲了能夠支撐搞得併發量,可採用協程(Coroutine)來支撐更高的併發量,避免併發量上漲後造成CPU sy消耗嚴重、系統load迅速上漲和系統性能下降。
在Java中實現協程的框架有Kilim,Kilim執行一項任務創建Task,使用Task的暫停機制,而不是Thread,Kilim承擔了線程調度以及上下切換動作,Task相對於原生Thread而言就輕量級多了,且能更好利用CPU。Kilim帶來的是線程使用率的提升,但同時由於要在JVM堆中保存Task上下文信息,因此在採用Kilim的情況下要消耗更多的內存。(目前JDK 7中也有一個支持協程方式的實現,另外基於JVM的Scala的Actor也可用於在Java使用協程)
文件IO消耗嚴重的解決方法
從程序的角度而言,造成文件IO消耗嚴重的原因主要是多個線程在寫進行大量的數據到同一文件,導致文件很快變得很大,從而寫入速度越來越慢,並造成各線程激烈爭搶文件鎖。
常用調優方法:
異步寫文件
批量讀寫
限流
限制文件大小
網絡IO消耗嚴重的解決方法
從程序的角度而言,造成網絡IO消耗嚴重的原因主要是同時需要發送或接收的包太多。
常用調優方法:
限流,限流通常是限制發送packet的頻率,從而在網絡IO消耗可接受的情況下來發送packget。
內存消耗嚴重的解決方法
釋放不必要的引用:代碼持有了不需要的對象引用,造成這些對象無法被GC,從而佔據了JVM堆內存。(使用ThreadLocal:注意在線程內動作執行完畢時,需執行ThreadLocal.set把對象清除,避免持有不必要的對象引用)
使用對象緩存池:創建對象要消耗一定的CPU以及內存,使用對象緩存池一定程度上可降低JVM堆內存的使用。
採用合理的緩存失效算法:如果放入太多對象在緩存池中,反而會造成內存的嚴重消耗, 同時由於緩存池一直對這些對象持有引用,從而造成Full GC增多,對於這種狀況要合理控制緩存池的大小,避免緩存池的對象數量無限上漲。(經典的緩存失效算法來清除緩存池中的對象:FIFO、LRU、LFU等)
合理使用SoftReference和WeekReference:SoftReference的對象會在內存不夠用的時候回收,WeekReference的對象會在Full GC的時候回收。
資源消耗不多但程序執行慢的情況的解決方法
降低鎖競爭: 多線多了,鎖競爭的狀況會比較明顯,這時候線程很容易處於等待鎖的狀況,從而導致性能下降以及CPU sy上升。
使用併發包中的類:大多數採用了lock-free、nonblocking算法。
使用Treiber算法:基於CAS以及AtomicReference。
使用Michael-Scott非阻塞隊列算法:基於CAS以及AtomicReference,典型ConcurrentLindkedQueue。
(基於CAS和AtomicReference來實現無阻塞是不錯的選擇,但值得注意的是,lock-free算法需不斷的循環比較來保證資源的一致性的,對於衝突較多的應用場景而言,會帶來更高的CPU消耗,因此不一定採用CAS實現無阻塞的就一定比採用lock方式的性能好。 還有一些無阻塞算法的改進:MCAS、WSTM等)
儘可能少用鎖:儘可能只對需要控制的資源做加鎖操作(通常沒有必要對整個方法加鎖,儘可能讓鎖最小化,只對互斥及原子操作的地方加鎖,加鎖時儘可能以保護資源的最小化粒度爲單位--如只對需要保護的資源加鎖而不是this)。
拆分鎖:獨佔鎖拆分爲多把鎖(讀寫鎖拆分、類似ConcurrentHashMap中默認拆分爲16把鎖),很多程度上能提高讀寫的性能,但需要注意在採用拆分鎖後,全局性質的操作會變得比較複雜(如ConcurrentHashMap中size操作)。(拆分鎖太多也會造成副作用,如CPU消耗明顯增加)
去除讀寫操作的互斥:在修改時加鎖,並複製對象進行修改,修改完畢後切換對象的引用,從而讀取時則不加鎖。這種稱爲CopyOnWrite,CopyOnWriteArrayList是典型實現,好處是可以明顯提升讀的性能,適合讀多寫少的場景, 但由於寫操作每次都要複製一份對象,會消耗更多的內存。
充分利用硬件資源(CPU和內存):
充分利用CPU
在能並行處理的場景中未使用足夠的線程(線程增加:CPU資源消耗可接受且不會帶來激烈競爭鎖的場景下), 例如單線程的計算,可以拆分爲多個線程分別計算,最後將結果合併,JDK 7中的fork-join框架。
Amdahl定律公式:1/(F+(1-F)/N)。
充分利用內存
數據的緩存、耗時資源的緩存(數據庫連接創建、網絡連接的創建等)、頁面片段的緩存。
畢竟內存的讀取肯定遠快於硬盤、網絡的讀取, 在內存消耗可接受、GC頻率、以及系統結構(例如集羣環境可能會帶來緩存的同步)可接受情況下,應充分利用內存來緩存數據,提升系統的性能。
總結:
好的調優策略是收益比(調優後提升的效果/調優改動所需付出的代價)最高的,通常來說簡單的系統調優比較好做,因此儘量保持單機上應用的純粹性, 這是大型系統的基本架構原則。
調優的三大有效原則:充分而不過分使用硬件資源、合理調整JVM、合理使用JDK包。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章