前言
分布式系統(tǒng)的性能指標(biāo),廣義來(lái)講,可能有多個(gè)方面,比如吞吐量(TPS)、高可用(幾個(gè)9)、低延時(shí)(RT)、客戶體驗(yàn)(PV/UV)、數(shù)據(jù)一致性、可擴(kuò)展性、容錯(cuò)性等。狹義來(lái)講,我們常用業(yè)務(wù)吞吐量(TPS)來(lái)表達(dá)系統(tǒng)性能。即:在滿足一定客戶體驗(yàn)前提下,在一定機(jī)器資源環(huán)境下,系統(tǒng)所能夠承載的最大業(yè)務(wù)處理能力,通常用每秒處理的事務(wù)數(shù)TPS來(lái)表示。

而除了在出現(xiàn)問(wèn)題的時(shí)候去進(jìn)行解決,更多的是防患于未然,在平時(shí)的開(kāi)發(fā)過(guò)程中稍微的注意一下,可能會(huì)避免后期很多的事情
今天文章主要從兩個(gè)方面講解:性能優(yōu)化的50個(gè)細(xì)節(jié)和如何定位性能問(wèn)題
性能優(yōu)化的50個(gè)細(xì)節(jié)
1. 盡量在合適的場(chǎng)合使用單例
使用單例可以減輕加載的負(fù)擔(dān),縮短加載的時(shí)間,提高加載的效率,但并不是所在地方都適用于單例
簡(jiǎn)單來(lái)說(shuō),單例主要適用于以下三個(gè)方面:
控制資源的使用,通過(guò)線程同步來(lái)控制資源的并發(fā)訪問(wèn);
控制實(shí)例的產(chǎn)生,以達(dá)到節(jié)約資源的目的;
控制數(shù)據(jù)共享,在不建立直接關(guān)聯(lián)的條件下,讓多個(gè)不相關(guān)的進(jìn)程或線程之間實(shí)現(xiàn)通信。
2. 盡量避免隨意使用靜態(tài)變量
當(dāng)某個(gè)對(duì)象被定義為static變量所引用,那么GC通常是不會(huì)回收這個(gè)對(duì)象所占有的內(nèi)存,如:
publicclassA{
privatestaticB b =newB();
}
此時(shí)靜態(tài)變量 b 的生命周期與A類同步,如果A類不會(huì)卸載,那么b對(duì)象會(huì)常駐內(nèi)存,直到程序終止。
3. 盡量避免過(guò)多過(guò)常地創(chuàng)建Java對(duì)象
盡量避免在經(jīng)常調(diào)用的方法,循環(huán)中new對(duì)象,由于系統(tǒng)不僅要花費(fèi)時(shí)間來(lái)創(chuàng)建對(duì)象,而且還要花時(shí)間對(duì)這些對(duì)象進(jìn)行垃圾回收和處理
在我們可以控制的范圍內(nèi),最大限度地重用對(duì)象,最好能用基本的數(shù)據(jù)類型或數(shù)組來(lái)替代對(duì)象。
4. 盡量使用final修飾符
帶有final修飾符的類是不可派生的。在JAVA核心API中,有許多應(yīng)用final的例子,例如java、lang、String,為String類指定final防止了使用者覆蓋length()方法。
另外,如果一個(gè)類是final的,則該類所有方法都是final的。java編譯器會(huì)尋找機(jī)會(huì)內(nèi)聯(lián)(inline)所有的final方法(這和具體的編譯器實(shí)現(xiàn)有關(guān)),此舉能夠使性能平均提高50%。
如:讓訪問(wèn)實(shí)例內(nèi)變量的getter/setter方法變成”final:簡(jiǎn)單的getter/setter方法應(yīng)該被置成final,這會(huì)告訴編譯器,這個(gè)方法不會(huì)被重載,所以,可以變成”inlined”,例子:
classMAF{
publicvoidsetSize(intsize){
? _size = size;
}
privateint_size;
}
更正
classDAF_fixed{
finalpublicvoidsetSize(intsize){
? _size = size;
}
privateint_size;
}
5. 盡量使用局部變量
調(diào)用方法時(shí)傳遞的參數(shù)以及在調(diào)用中創(chuàng)建的臨時(shí)變量都保存在棧(Stack)中,速度較快;其他變量,如靜態(tài)變量、實(shí)例變量等,都在堆(Heap)中創(chuàng)建,速度較慢。
6. 盡量處理好包裝類型和基本類型兩者的使用場(chǎng)所
雖然包裝類型和基本類型在使用過(guò)程中是可以相互轉(zhuǎn)換,但它們兩者所產(chǎn)生的內(nèi)存區(qū)域是完全不同的
基本類型數(shù)據(jù)產(chǎn)生和處理都在棧中處理,包裝類型是對(duì)象,是在堆中產(chǎn)生實(shí)例。在集合類對(duì)象,有對(duì)象方面需要的處理適用包裝類型,其他的處理提倡使用基本類型。
7. 慎用synchronized,盡量減小synchronize的方法
都知道,實(shí)現(xiàn)同步是要很大的系統(tǒng)開(kāi)銷作為代價(jià)的,甚至可能造成死鎖,所以盡量避免無(wú)謂的同步控制。
synchronize方法被調(diào)用時(shí),直接會(huì)把當(dāng)前對(duì)象鎖了,在方法執(zhí)行完之前其他線程無(wú)法調(diào)用當(dāng)前對(duì)象的其他方法。
所以,synchronize的方法盡量減小,并且應(yīng)盡量使用方法同步代替代碼塊同步。
9. 盡量不要使用finalize方法
實(shí)際上,將資源清理放在finalize方法中完成是非常不好的選擇
由于GC的工作量很大,尤其是回收Young代內(nèi)存時(shí),大都會(huì)引起應(yīng)用程序暫停,所以再選擇使用finalize方法進(jìn)行資源清理,會(huì)導(dǎo)致GC負(fù)擔(dān)更大,程序運(yùn)行效率更差。
10. 盡量使用基本數(shù)據(jù)類型代替對(duì)象
Stringstr="hello";
上面這種方式會(huì)創(chuàng)建一個(gè)“hello”字符串,而且JVM的字符緩存池還會(huì)緩存這個(gè)字符串;
Stringstr=newString("hello");
此時(shí)程序除創(chuàng)建字符串外,str所引用的String對(duì)象底層還包含一個(gè)char[]數(shù)組,這個(gè)char[]數(shù)組依次存放了h,e,l,l,o
11. 多線程在未發(fā)生線程安全前提下應(yīng)盡量使用HashMap、ArrayList
HashTable、Vector等使用了同步機(jī)制,降低了性能。
12. 盡量合理的創(chuàng)建HashMap
當(dāng)你要?jiǎng)?chuàng)建一個(gè)比較大的hashMap時(shí),充分利用這個(gè)構(gòu)造函數(shù)
public HashMap(int initialCapacity, float loadFactor);
避免HashMap多次進(jìn)行了hash重構(gòu),擴(kuò)容是一件很耗費(fèi)性能的事
在默認(rèn)中initialCapacity只有16,而loadFactor是 0.75,需要多大的容量,你最好能準(zhǔn)確的估計(jì)你所需要的最佳大小,同樣的Hashtable,Vectors也是一樣的道理。
13. 盡量減少對(duì)變量的重復(fù)計(jì)算
如:
for(inti=0;i<list.size();i++)
應(yīng)該改為:
for(inti=0,len=list.size();i<len;i++)
并且在循環(huán)中應(yīng)該避免使用復(fù)雜的表達(dá)式,在循環(huán)中,循環(huán)條件會(huì)被反復(fù)計(jì)算,如果不使用復(fù)雜表達(dá)式,而使循環(huán)條件值不變的話,程序?qū)?huì)運(yùn)行的更快。
14. 盡量避免不必要的創(chuàng)建
如:
A a =newA();
if(i==1){
list.add(a);
}
應(yīng)該改為:
if(i==1){
A a =newA();
list.add(a);
}
15. 盡量在finally塊中釋放資源
程序中使用到的資源應(yīng)當(dāng)被釋放,以避免資源泄漏,這最好在finally塊中去做。不管程序執(zhí)行的結(jié)果如何,finally塊總是會(huì)執(zhí)行的,以確保資源的正確關(guān)閉。
16. 盡量使用移位來(lái)代替'a/b'的操作
"/"是一個(gè)代價(jià)很高的操作,使用移位的操作將會(huì)更快和更有效
如:
intnum = a /4;
intnum = a /8;
應(yīng)該改為:
intnum = a >>2;
intnum = a >>3;
但注意的是使用移位應(yīng)添加注釋,因?yàn)橐莆徊僮鞑恢庇^,比較難理解。
17.盡量使用移位來(lái)代替'a*b'的操作
同樣的,對(duì)于'*'操作,使用移位的操作將會(huì)更快和更有效
如:
intnum = a *4;
intnum = a *8;
應(yīng)該改為:
intnum = a <<2;
intnum = a <<3;
18. 盡量確定StringBuffer的容量
StringBuffer 的構(gòu)造器會(huì)創(chuàng)建一個(gè)默認(rèn)大?。ㄍǔJ?6)的字符數(shù)組。
在使用中,如果超出這個(gè)大小,就會(huì)重新分配內(nèi)存,創(chuàng)建一個(gè)更大的數(shù)組,并將原先的數(shù)組復(fù)制過(guò)來(lái),再丟棄舊的數(shù)組。
在大多數(shù)情況下,你可以在創(chuàng)建 StringBuffer的時(shí)候指定大小,這樣就避免了在容量不夠的時(shí)候自動(dòng)增長(zhǎng),以提高性能。如:
StringBufferbuffer=newStringBuffer(1000);
19. 盡量早釋放無(wú)用對(duì)象的引用
大部分時(shí),方法局部引用變量所引用的對(duì)象會(huì)隨著方法結(jié)束而變成垃圾,因此,大部分時(shí)候程序無(wú)需將局部,引用變量顯式設(shè)為null。例如:
Java代碼
Publicvoidtest(){
Objectobj =newObject();
……
Obj =null;
}
上面這個(gè)就沒(méi)必要了,隨著方法test()的執(zhí)行完成,程序中obj引用變量的作用域就結(jié)束了。
但是如果是改成下面:
Java代碼
Publicvoidtest(){
Objectobj =newObject();
……
Obj =null;
//執(zhí)行耗時(shí),耗內(nèi)存操作;或調(diào)用耗時(shí),耗內(nèi)存的方法
……
}
這時(shí)候就有必要將obj賦值為null,可以盡早的釋放對(duì)Object對(duì)象的引用。
20. 盡量避免使用二維數(shù)組
二維數(shù)據(jù)占用的內(nèi)存空間比一維數(shù)據(jù)多得多,大概10倍以上。
21. 盡量避免使用split
除非是必須的,否則應(yīng)該避免使用split,split由于支持正則表達(dá)式,所以效率比較低
如果是頻繁的幾十,幾百萬(wàn)的調(diào)用將會(huì)耗費(fèi)大量資源,如果確實(shí)需要頻繁的調(diào)用split,可以考慮使用apache的StringUtils.split(string,char),頻繁split的可以緩存結(jié)果
22. ArrayList & LinkedList
一個(gè)是線性表,一個(gè)是鏈表,一句話,隨機(jī)查詢盡量使用ArrayList,ArrayList優(yōu)于LinkedList,LinkedList還要移動(dòng)指針,添加刪除的操作LinkedList優(yōu)于ArrayList,ArrayList還要移動(dòng)數(shù)據(jù)
不過(guò)這是理論性分析,事實(shí)未必如此,重要的是理解好2者的數(shù)據(jù)結(jié)構(gòu),對(duì)癥下藥。
23. 盡量使用System.arraycopy ()代替通過(guò)來(lái)循環(huán)復(fù)制數(shù)組
System.arraycopy() 要比通過(guò)循環(huán)來(lái)復(fù)制數(shù)組快的多。
24. 盡量緩存經(jīng)常使用的對(duì)象
盡可能將經(jīng)常使用的對(duì)象進(jìn)行緩存,可以使用數(shù)組,或HashMap的容器來(lái)進(jìn)行緩存,但這種方式可能導(dǎo)致系統(tǒng)占用過(guò)多的緩存,性能下降
推薦可以使用一些第三方的開(kāi)源工具,如EhCache,Oscache進(jìn)行緩存,他們基本都實(shí)現(xiàn)了FIFO/FLU等緩存算法。
25. 盡量避免非常大的內(nèi)存分配
有時(shí)候問(wèn)題不是由當(dāng)時(shí)的堆狀態(tài)造成的,而是因?yàn)榉峙涫≡斐傻?。分配的?nèi)存塊都必須是連續(xù)的,而隨著堆越來(lái)越滿,找到較大的連續(xù)塊越來(lái)越困難。
26. 慎用異常
當(dāng)創(chuàng)建一個(gè)異常時(shí),需要收集一個(gè)棧跟蹤(stack track),這個(gè)棧跟蹤用于描述異常是在何處創(chuàng)建的。
構(gòu)建這些棧跟蹤時(shí)需要為運(yùn)行時(shí)棧做一份快照,正是這一部分開(kāi)銷很大。
當(dāng)需要?jiǎng)?chuàng)建一個(gè) Exception 時(shí),JVM 不得不說(shuō):先別動(dòng),我想就您現(xiàn)在的樣子存一份快照,所以暫時(shí)停止入棧和出棧操作。棧跟蹤不只包含運(yùn)行時(shí)棧中的一兩個(gè)元素,而是包含這個(gè)棧中的每一個(gè)元素。
如果您創(chuàng)建一個(gè) Exception ,就得付出代價(jià),好在捕獲異常開(kāi)銷不大,因此可以使用 try-catch 將核心內(nèi)容包起來(lái)。
從技術(shù)上講,你甚至可以隨意地拋出異常,而不用花費(fèi)很大的代價(jià)。招致性能損失的并不是 throw 操作——盡管在沒(méi)有預(yù)先創(chuàng)建異常的情況下就拋出異常是有點(diǎn)不尋常。
真正要花代價(jià)的是創(chuàng)建異常,幸運(yùn)的是,好的編程習(xí)慣已教會(huì)我們,不應(yīng)該不管三七二十一就拋出異常。異常是為異常的情況而設(shè)計(jì)的,使用時(shí)也應(yīng)該牢記這一原則。
27. 盡量重用對(duì)象
特別是String對(duì)象的使用中,出現(xiàn)字符串連接情況時(shí)應(yīng)使用StringBuffer代替,由于系統(tǒng)不僅要花時(shí)間生成對(duì)象,以后可能還需要花時(shí)間對(duì)這些對(duì)象進(jìn)行垃圾回收和處理。因此生成過(guò)多的對(duì)象將會(huì)給程序的性能帶來(lái)很大的影響。
28. 不要重復(fù)初始化變量
默認(rèn)情況下,調(diào)用類的構(gòu)造函數(shù)時(shí),java會(huì)把變量初始化成確定的值,所有的對(duì)象被設(shè)置成null,整數(shù)變量設(shè)置成0,float和double變量設(shè)置成0.0,邏輯值設(shè)置成false。
當(dāng)一個(gè)類從另一個(gè)類派生時(shí),這一點(diǎn)尤其應(yīng)該注意,因?yàn)橛胣ew關(guān)鍵字創(chuàng)建一個(gè)對(duì)象時(shí),構(gòu)造函數(shù)鏈中的所有構(gòu)造函數(shù)都會(huì)被自動(dòng)調(diào)用。
這里有個(gè)注意,給成員變量設(shè)置初始值但需要調(diào)用其他方法的時(shí)候,最好放在一個(gè)方法。比如initXXX()中,因?yàn)橹苯诱{(diào)用某方法賦值可能會(huì)因?yàn)轭惿形闯跏蓟鴴伩罩羔槷惓?,如:public int state = this.getState()。
29. 在java+Oracle的應(yīng)用系統(tǒng)開(kāi)發(fā)中,java中內(nèi)嵌的SQL語(yǔ)言應(yīng)盡量使用大寫形式,以減少Oracle解析器的解析負(fù)擔(dān)。
30. 在java編程過(guò)程中,進(jìn)行數(shù)據(jù)庫(kù)連接,I/O流操作,在使用完畢后,及時(shí)關(guān)閉以釋放資源。因?yàn)閷?duì)這些大對(duì)象的操作會(huì)造成系統(tǒng)大的開(kāi)銷。
31. 過(guò)分的創(chuàng)建對(duì)象會(huì)消耗系統(tǒng)的大量?jī)?nèi)存,嚴(yán)重時(shí),會(huì)導(dǎo)致內(nèi)存泄漏
因此,保證過(guò)期的對(duì)象的及時(shí)回收具有重要意義。JVM的GC并非十分智能,因此建議在對(duì)象使用完畢后,手動(dòng)設(shè)置成null。
32. 在使用同步機(jī)制時(shí),應(yīng)盡量使用方法同步代替代碼塊同步。
33. 不要在循環(huán)中使用Try/Catch語(yǔ)句,應(yīng)把Try/Catch放在循環(huán)最外層
Error是獲取系統(tǒng)錯(cuò)誤的類,或者說(shuō)是虛擬機(jī)錯(cuò)誤的類。不是所有的錯(cuò)誤Exception都能獲取到的,虛擬機(jī)報(bào)錯(cuò)Exception就獲取不到,必須用Error獲取。
34. 通過(guò)StringBuffer的構(gòu)造函數(shù)來(lái)設(shè)定它的初始化容量,可以明顯提升性能
StringBuffer的默認(rèn)容量為16,當(dāng)StringBuffer的容量達(dá)到最大容量時(shí),它會(huì)將自身容量增加到當(dāng)前的2倍+2,也就是2*n+2。
無(wú)論何時(shí),只要StringBuffer到達(dá)它的最大容量,它就不得不創(chuàng)建一個(gè)新的對(duì)象數(shù)組,然后復(fù)制舊的對(duì)象數(shù)組,這會(huì)浪費(fèi)很多時(shí)間。
所以給StringBuffer設(shè)置一個(gè)合理的初始化容量值,是很有必要的!
35. 合理使用java.util.Vector
Vector與StringBuffer類似,每次擴(kuò)展容量時(shí),所有現(xiàn)有元素都要賦值到新的存儲(chǔ)空間中。
Vector的默認(rèn)存儲(chǔ)能力為10個(gè)元素,擴(kuò)容加倍
vector.add(index,obj) 這個(gè)方法可以將元素obj插入到index位置,但index以及之后的元素依次都要向下移動(dòng)一個(gè)位置(將其索引加 1)。除非必要,否則對(duì)性能不利。
同樣規(guī)則適用于remove(int index)方法,移除此向量中指定位置的元素。將所有后續(xù)元素左移(將其索引減 1)。返回此向量中移除的元素。
所以刪除vector最后一個(gè)元素要比刪除第1個(gè)元素開(kāi)銷低很多。刪除所有元素最好用removeAllElements()方法。
如果要?jiǎng)h除vector里的一個(gè)元素可以使用 vector.remove(obj);而不必自己檢索元素位置,再刪除,如int index = indexOf(obj);vector.remove(index)。
38. 不用new關(guān)鍵字創(chuàng)建對(duì)象的實(shí)例
用new關(guān)鍵詞創(chuàng)建類的實(shí)例時(shí),構(gòu)造函數(shù)鏈中的所有構(gòu)造函數(shù)都會(huì)被自動(dòng)調(diào)用。
但如果一個(gè)對(duì)象實(shí)現(xiàn)了Cloneable接口,我們可以調(diào)用它的clone()方法。clone()方法不會(huì)調(diào)用任何類構(gòu)造函數(shù)。
下面是Factory模式的一個(gè)典型實(shí)現(xiàn):
publicstaticCreditgetNewCredit(){
returnnewCredit();
}
改進(jìn)后的代碼使用clone()方法:
privatestaticCredit BaseCredit =newCredit();
publicstaticCredit getNewCredit() {
return(Credit)BaseCredit.clone();
}
39. 不要將數(shù)組聲明為:public static final
40. HaspMap的遍歷:
Map<String,String[]> paraMap =newHashMap<String,String[]>();
for(Entry<String,String[]> entry : paraMap.entrySet()) {
StringappFieldDefId = entry.getKey();
String[] values = entry.getValue();
}
利用散列值取出相應(yīng)的Entry做比較得到結(jié)果,取得entry的值之后直接取key和value。
41. array(數(shù)組)和ArrayList的使用
array 數(shù)組效率最高,但容量固定,無(wú)法動(dòng)態(tài)改變,ArrayList容量可以動(dòng)態(tài)增長(zhǎng),但犧牲了效率。
42. 單線程應(yīng)盡量使用 HashMap, ArrayList,除非必要,否則不推薦使用HashTable,Vector,它們使用了同步機(jī)制,而降低了性能。
43. StringBuffer,StringBuilder的區(qū)別在于:java.lang.StringBuffer 線程安全的可變字符序列。一個(gè)類似于String的字符串緩沖區(qū),但不能修改。
StringBuilder與該類相比,通常應(yīng)該優(yōu)先使用StringBuilder類,因?yàn)樗С炙邢嗤牟僮?,但由于它不?zhí)行同步,所以速度更快。
為了獲得更好的性能,在構(gòu)造StringBuffer或StringBuilder時(shí)應(yīng)盡量指定它的容量。當(dāng)然如果不超過(guò)16個(gè)字符時(shí)就不用了。
相同情況下,使用StringBuilder比使用StringBuffer僅能獲得10%~15%的性能提升,但卻要冒多線程不安全的風(fēng)險(xiǎn)。綜合考慮還是建議使用StringBuffer。
44. 盡量使用基本數(shù)據(jù)類型代替對(duì)象。
45. 使用具體類比使用接口效率高,但結(jié)構(gòu)彈性降低了,但現(xiàn)代IDE都可以解決這個(gè)問(wèn)題。
46. 考慮使用靜態(tài)方法,如果你沒(méi)有必要去訪問(wèn)對(duì)象的外部,那么就使你的方法成為靜態(tài)方法。它會(huì)被更快地調(diào)用,因?yàn)樗恍枰粋€(gè)虛擬函數(shù)導(dǎo)向表。
這同時(shí)也是一個(gè)很好的實(shí)踐,因?yàn)樗嬖V你如何區(qū)分方法的性質(zhì),調(diào)用這個(gè)方法不會(huì)改變對(duì)象的狀態(tài)。
47. 應(yīng)盡可能避免使用內(nèi)在的GET,SET方法。
48.避免枚舉,浮點(diǎn)數(shù)的使用。
以下舉幾個(gè)實(shí)用優(yōu)化的例子:
一、避免在循環(huán)條件中使用復(fù)雜表達(dá)式
在不做編譯優(yōu)化的情況下,在循環(huán)中,循環(huán)條件會(huì)被反復(fù)計(jì)算,如果不使用復(fù)雜表達(dá)式,而使循環(huán)條件值不變的話,程序?qū)?huì)運(yùn)行的更快。
例子:
importjava.util.Vector;
classCEL{
voidmethod(Vectorvector){
for(inti =0; i <vector.size (); i++)// Violation
// ...
}
}
更正:
classCEL_fixed{
voidmethod(Vectorvector){
intsize =vector.size ();
for(inti =0; i < size; i++)
// ...
}
}
二、為'Vectors' 和 'Hashtables'定義初始大小
JVM為Vector擴(kuò)充大小的時(shí)候需要重新創(chuàng)建一個(gè)更大的數(shù)組,將原原先數(shù)組中的內(nèi)容復(fù)制過(guò)來(lái),最后,原先的數(shù)組再被回收??梢?jiàn)Vector容量的擴(kuò)大是一個(gè)頗費(fèi)時(shí)間的事。
通常,默認(rèn)的10個(gè)元素大小是不夠的。你最好能準(zhǔn)確的估計(jì)你所需要的最佳大小。例子:
importjava.util.Vector;
publicclassDIC{
publicvoidaddObjects(Object[] o){
? // if length > 10, Vector needs to expand
? for(inti =0; i< o.length;i++) {
? v.add(o);// capacity before it can add more elements.
? }
}
publicVector v =newVector();// no initialCapacity.
}
更正:
自己設(shè)定初始大小。
publicVector v =newVector(20);
publicHashtable hash =newHashtable(10);
三、在finally塊中關(guān)閉Stream
程序中使用到的資源應(yīng)當(dāng)被釋放,以避免資源泄漏。這最好在finally塊中去做。不管程序執(zhí)行的結(jié)果如何,finally塊總是會(huì)執(zhí)行的,以確保資源的正確關(guān)閉。
四、使用'System.arraycopy ()'代替通過(guò)來(lái)循環(huán)復(fù)制數(shù)組
例子:
publicclassIRB{
voidmethod(){
? int[] array1 =newint[100];
? for(inti =0; i < array1.length; i++) {
? array1 [i] = i;
? }
? int[] array2 =newint[100];
? for(inti =0; i < array2.length; i++) {
? array2 [i] = array1 [i];// Violation
? }
}
}
更正:
publicclassIRB{
voidmethod(){
? int[] array1 =newint[100];
? for(inti =0; i < array1.length; i++) {
? array1 [i] = i;
? }
? int[] array2 =newint[100];
? System.arraycopy(array1,0, array2,0,100);
}
}
五、讓訪問(wèn)實(shí)例內(nèi)變量的getter/setter方法變成”final”
簡(jiǎn)單的getter/setter方法應(yīng)該被置成final,這會(huì)告訴編譯器,這個(gè)方法不會(huì)被重載,所以,可以變成”inlined”,例子:
classMAF{
publicvoidsetSize(intsize){
? _size = size;
}
privateint_size;
}
更正:
classDAF_fixed{
finalpublicvoidsetSize(intsize){
? _size = size;
}
privateint_size;
}
六、對(duì)于常量字符串,用'String' 代替 'StringBuffer'
常量字符串并不需要?jiǎng)討B(tài)改變長(zhǎng)度。
例子:
publicclassUSC{
Stringmethod(){
? StringBuffer s =newStringBuffer ("Hello");
? String t = s +"World!";
? returnt;
}
}
更正:把StringBuffer換成String,如果確定這個(gè)String不會(huì)再變的話,這將會(huì)減少運(yùn)行開(kāi)銷提高性能。
七、在字符串相加的時(shí)候,使用 ' ' 代替 " ",如果該字符串只有一個(gè)字符的話
例子:
publicclassSTR{
publicvoidmethod(String s){
? Stringstring= s +"d"http:// violation.
? string="abc"+"d"http:// violation.
}
}

publicclassSTR{
publicvoidmethod(String s){
? Stringstring= s +"d"http:// violation.
? string="abc"+"d"http:// violation.
}
}
更正:
將一個(gè)字符的字符串替換成' '
publicclassSTR{
publicvoidmethod(String s){
? Stringstring= s +'d'
? string="abc"+'d'
}
}
以上僅是Java方面編程時(shí)的性能優(yōu)化,性能優(yōu)化大部分都是在時(shí)間、效率、代碼結(jié)構(gòu)層次等方面的權(quán)衡,各有利弊
如何定位性能問(wèn)題
接下來(lái)就是如何去定位性能問(wèn)題了,只有準(zhǔn)確定位了,才能具體的去進(jìn)行解決啊,但是其實(shí)大家都知道,性能出現(xiàn)問(wèn)題其實(shí)真的不算多,就像一個(gè)讀者說(shuō)的,ThreadLocal出現(xiàn)內(nèi)存泄漏,我至今沒(méi)有遇到過(guò),其實(shí)包括筆者自己遇到的性能問(wèn)題也不多,但是要居安思危,在平時(shí)準(zhǔn)備好,當(dāng)問(wèn)題出現(xiàn)時(shí)才不會(huì)出現(xiàn)問(wèn)題,我們模擬下常見(jiàn)的幾個(gè)Java性能故障,來(lái)學(xué)習(xí)怎么去分析和定位
既然是定位問(wèn)題,肯定是需要借助工具,所以肯定需要提前學(xué)習(xí)一些性能調(diào)優(yōu)的工具
top命令
top命令是我們最常用的Linux命令之一,它可以實(shí)時(shí)的顯示當(dāng)前正在執(zhí)行的進(jìn)程的CPU使用率,內(nèi)存使用率等系統(tǒng)信息。top -Hp pid 可以查看線程的系統(tǒng)資源使用情況。
vmstat命令
vmstat是一個(gè)指定周期和采集次數(shù)的虛擬內(nèi)存檢測(cè)工具,可以統(tǒng)計(jì)內(nèi)存,CPU,swap的使用情況,它還有一個(gè)重要的常用功能,用來(lái)觀察進(jìn)程的上下文切換。字段說(shuō)明如下:
r: 運(yùn)行隊(duì)列中進(jìn)程數(shù)量(當(dāng)數(shù)量大于CPU核數(shù)表示有阻塞的線程)
b: 等待IO的進(jìn)程數(shù)量
swpd: 使用虛擬內(nèi)存大小
free: 空閑物理內(nèi)存大小
buff: 用作緩沖的內(nèi)存大小(內(nèi)存和硬盤的緩沖區(qū))
cache: 用作緩存的內(nèi)存大?。–PU和內(nèi)存之間的緩沖區(qū))
si: 每秒從交換區(qū)寫到內(nèi)存的大小,由磁盤調(diào)入內(nèi)存
so: 每秒寫入交換區(qū)的內(nèi)存大小,由內(nèi)存調(diào)入磁盤
bi: 每秒讀取的塊數(shù)
bo: 每秒寫入的塊數(shù)
in: 每秒中斷數(shù),包括時(shí)鐘中斷。
cs: 每秒上下文切換數(shù)。
us: 用戶進(jìn)程執(zhí)行時(shí)間百分比(user time)
sy: 內(nèi)核系統(tǒng)進(jìn)程執(zhí)行時(shí)間百分比(system time)
wa: IO等待時(shí)間百分比
id: 空閑時(shí)間百分比
pidstat命令
pidstat 是 Sysstat 中的一個(gè)組件,也是一款功能強(qiáng)大的性能監(jiān)測(cè)工具,top 和 vmstat 兩個(gè)命令都是監(jiān)測(cè)進(jìn)程的內(nèi)存、CPU 以及 I/O 使用情況,而 pidstat 命令可以檢測(cè)到線程級(jí)別的。pidstat命令線程切換字段說(shuō)明如下:
UID :被監(jiān)控任務(wù)的真實(shí)用戶ID。
TGID :線程組ID。
TID:線程ID。
cswch/s:主動(dòng)切換上下文次數(shù),這里是因?yàn)橘Y源阻塞而切換線程,比如鎖等待等情況。
nvcswch/s:被動(dòng)切換上下文次數(shù),這里指CPU調(diào)度切換了線程。
jstack命令
jstack是JDK工具命令,它是一種線程堆棧分析工具,最常用的功能就是使用 jstack pid 命令查看線程的堆棧信息,也經(jīng)常用來(lái)排除死鎖情況。
jstat 命令
它可以檢測(cè)Java程序運(yùn)行的實(shí)時(shí)情況,包括堆內(nèi)存信息和垃圾回收信息,我們常常用來(lái)查看程序垃圾回收情況。常用的命令是jstat -gc pid。信息字段說(shuō)明如下:
S0C:年輕代中 To Survivor 的容量(單位 KB);
S1C:年輕代中 From Survivor 的容量(單位 KB);
S0U:年輕代中 To Survivor 目前已使用空間(單位 KB);
S1U:年輕代中 From Survivor 目前已使用空間(單位 KB);
EC:年輕代中 Eden 的容量(單位 KB);
EU:年輕代中 Eden 目前已使用空間(單位 KB);
OC:老年代的容量(單位 KB);
OU:老年代目前已使用空間(單位 KB);
MC:元空間的容量(單位 KB);
MU:元空間目前已使用空間(單位 KB);
YGC:從應(yīng)用程序啟動(dòng)到采樣時(shí)年輕代中 gc 次數(shù);
YGCT:從應(yīng)用程序啟動(dòng)到采樣時(shí)年輕代中 gc 所用時(shí)間 (s);
FGC:從應(yīng)用程序啟動(dòng)到采樣時(shí) 老年代(Full Gc)gc 次數(shù);
FGCT:從應(yīng)用程序啟動(dòng)到采樣時(shí) 老年代代(Full Gc)gc 所用時(shí)間 (s);
GCT:從應(yīng)用程序啟動(dòng)到采樣時(shí) gc 用的總時(shí)間 (s)。
jmap命令
jmap也是JDK工具命令,它可以查看堆內(nèi)存的初始化信息以及堆內(nèi)存的使用情況,還可以生成dump文件來(lái)進(jìn)行詳細(xì)分析。查看堆內(nèi)存情況命令jmap -heap pid。
mat內(nèi)存工具
MAT(Memory Analyzer Tool)工具是eclipse的一個(gè)插件(MAT也可以單獨(dú)使用),它分析大內(nèi)存的dump文件時(shí),可以非常直觀的看到各個(gè)對(duì)象在堆空間中所占用的內(nèi)存大小、類實(shí)例數(shù)量、對(duì)象引用關(guān)系、利用OQL對(duì)象查詢,以及可以很方便的找出對(duì)象GC Roots的相關(guān)信息。下載地址可以點(diǎn)擊這里
模擬環(huán)境準(zhǔn)備
基礎(chǔ)環(huán)境jdk1.8,采用SpringBoot框架來(lái)寫幾個(gè)接口來(lái)觸發(fā)模擬場(chǎng)景,首先是模擬CPU占滿情況
CPU占滿
模擬CPU占滿還是比較簡(jiǎn)單,直接寫一個(gè)死循環(huán)計(jì)算消耗CPU即可。
/**
? ? * 模擬CPU占滿
? ? */
? ? @GetMapping("/cpu/loop")
? ? public void testCPULoop() throws InterruptedException {
? ? ? ? System.out.println("請(qǐng)求cpu死循環(huán)");
? ? ? ? Thread.currentThread().setName("loop-thread-cpu");
? ? ? ? int num = 0;
? ? ? ? while (true) {
? ? ? ? ? ? num++;
? ? ? ? ? ? if (num == Integer.MAX_VALUE) {
? ? ? ? ? ? ? ? System.out.println("reset");
? ? ? ? ? ? }
? ? ? ? ? ? num = 0;
? ? ? ? }
? ? }
復(fù)制代碼
請(qǐng)求接口地址測(cè)試curl localhost:8080/cpu/loop,發(fā)現(xiàn)CPU立馬飆升到100%

通過(guò)執(zhí)行top -Hp 32805 查看Java線程情況

執(zhí)行 printf '%x' 32826 獲取16進(jìn)制的線程id,用于dump信息查詢,結(jié)果為 803a。最后我們執(zhí)行jstack 32805 |grep -A 20 803a來(lái)查看下詳細(xì)的dump信息。

這里dump信息直接定位出了問(wèn)題方法以及代碼行,這就定位出了CPU占滿的問(wèn)題。
內(nèi)存泄露
模擬內(nèi)存泄漏借助了ThreadLocal對(duì)象來(lái)完成,ThreadLocal是一個(gè)線程私有變量,可以綁定到線程上,在整個(gè)線程的生命周期都會(huì)存在,但是由于ThreadLocal的特殊性,ThreadLocal是基于ThreadLocalMap實(shí)現(xiàn)的,ThreadLocalMap的Entry繼承WeakReference,而Entry的Key是WeakReference的封裝,換句話說(shuō)Key就是弱引用,弱引用在下次GC之后就會(huì)被回收,如果ThreadLocal在set之后不進(jìn)行后續(xù)的操作,因?yàn)镚C會(huì)把Key清除掉,但是Value由于線程還在存活,所以Value一直不會(huì)被回收,最后就會(huì)發(fā)生內(nèi)存泄漏。
/**
? ? * 模擬內(nèi)存泄漏
? ? */
? ? @GetMapping(value = "/memory/leak")
? ? public String leak() {
? ? ? ? System.out.println("模擬內(nèi)存泄漏");
? ? ? ? ThreadLocal<Byte[]> localVariable = new ThreadLocal<Byte[]>();
? ? ? ? localVariable.set(new Byte[4096 * 1024]);// 為線程添加變量
? ? ? ? return "ok";
? ? }
復(fù)制代碼
我們給啟動(dòng)加上堆內(nèi)存大小限制,同時(shí)設(shè)置內(nèi)存溢出的時(shí)候輸出堆??煺詹⑤敵鋈罩?。
java -jar -Xms500m -Xmx500m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/heaplog.log analysis-demo-0.0.1-SNAPSHOT.jar
啟動(dòng)成功后我們循環(huán)執(zhí)行100次,for i in {1..500}; do curl localhost:8080/memory/leak;done,還沒(méi)執(zhí)行完畢,系統(tǒng)已經(jīng)返回500錯(cuò)誤了。查看系統(tǒng)日志出現(xiàn)了如下異常:
java.lang.OutOfMemoryError: Java heap space
復(fù)制代碼
我們用jstat -gc pid 命令來(lái)看看程序的GC情況。

很明顯,內(nèi)存溢出了,堆內(nèi)存經(jīng)過(guò)45次 Full Gc 之后都沒(méi)釋放出可用內(nèi)存,這說(shuō)明當(dāng)前堆內(nèi)存中的對(duì)象都是存活的,有GC Roots引用,無(wú)法回收。那是什么原因?qū)е聝?nèi)存溢出呢?是不是我只要加大內(nèi)存就行了呢?如果是普通的內(nèi)存溢出也許擴(kuò)大內(nèi)存就行了,但是如果是內(nèi)存泄漏的話,擴(kuò)大的內(nèi)存不一會(huì)就會(huì)被占滿,所以我們還需要確定是不是內(nèi)存泄漏。我們之前保存了堆 Dump 文件,這個(gè)時(shí)候借助我們的MAT工具來(lái)分析下。導(dǎo)入工具選擇Leak Suspects Report,工具直接就會(huì)給你列出問(wèn)題報(bào)告。

這里已經(jīng)列出了可疑的4個(gè)內(nèi)存泄漏問(wèn)題,我們點(diǎn)擊其中一個(gè)查看詳情。

這里已經(jīng)指出了內(nèi)存被線程占用了接近50M的內(nèi)存,占用的對(duì)象就是ThreadLocal。如果想詳細(xì)的通過(guò)手動(dòng)去分析的話,可以點(diǎn)擊Histogram,查看最大的對(duì)象占用是誰(shuí),然后再分析它的引用關(guān)系,即可確定是誰(shuí)導(dǎo)致的內(nèi)存溢出。

上圖發(fā)現(xiàn)占用內(nèi)存最大的對(duì)象是一個(gè)Byte數(shù)組,我們看看它到底被那個(gè)GC Root引用導(dǎo)致沒(méi)有被回收。按照上圖紅框操作指引,結(jié)果如下圖:

我們發(fā)現(xiàn)Byte數(shù)組是被線程對(duì)象引用的,圖中也標(biāo)明,Byte數(shù)組對(duì)象的GC Root是線程,所以它是不會(huì)被回收的,展開(kāi)詳細(xì)信息查看,我們發(fā)現(xiàn)最終的內(nèi)存占用對(duì)象是被ThreadLocal對(duì)象占據(jù)了。這也和MAT工具自動(dòng)幫我們分析的結(jié)果一致。
死鎖
死鎖會(huì)導(dǎo)致耗盡線程資源,占用內(nèi)存,表現(xiàn)就是內(nèi)存占用升高,CPU不一定會(huì)飆升(看場(chǎng)景決定),如果是直接new線程,會(huì)導(dǎo)致JVM內(nèi)存被耗盡,報(bào)無(wú)法創(chuàng)建線程的錯(cuò)誤,這也是體現(xiàn)了使用線程池的好處。
ExecutorService service = new ThreadPoolExecutor(4, 10,
? ? ? ? ? ? 0, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(1024),
? ? ? ? ? ? Executors.defaultThreadFactory(),
? ? ? ? ? ? new ThreadPoolExecutor.AbortPolicy());
? /**
? ? * 模擬死鎖
? ? */
? ? @GetMapping("/cpu/test")
? ? public String testCPU() throws InterruptedException {
? ? ? ? System.out.println("請(qǐng)求cpu");
? ? ? ? Object lock1 = new Object();
? ? ? ? Object lock2 = new Object();
? ? ? ? service.submit(new DeadLockThread(lock1, lock2), "deadLookThread-" + new Random().nextInt());
? ? ? ? service.submit(new DeadLockThread(lock2, lock1), "deadLookThread-" + new Random().nextInt());
? ? ? ? return "ok";
? ? }
public class DeadLockThread implements Runnable {
? ? private Object lock1;
? ? private Object lock2;
? ? public DeadLockThread(Object lock1, Object lock2) {
? ? ? ? this.lock1 = lock1;
? ? ? ? this.lock2 = lock2;
? ? }
? ? @Override
? ? public void run() {
? ? ? ? synchronized (lock2) {
? ? ? ? ? ? System.out.println(Thread.currentThread().getName()+"get lock2 and wait lock1");
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? TimeUnit.MILLISECONDS.sleep(2000);
? ? ? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? ? ? e.printStackTrace();
? ? ? ? ? ? }
? ? ? ? ? ? synchronized (lock1) {
? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName()+"get lock1 and lock2 ");
? ? ? ? ? ? }
? ? ? ? }
? ? }
}
復(fù)制代碼
我們循環(huán)請(qǐng)求接口2000次,發(fā)現(xiàn)不一會(huì)系統(tǒng)就出現(xiàn)了日志錯(cuò)誤,線程池和隊(duì)列都滿了,由于我選擇的當(dāng)隊(duì)列滿了就拒絕的策略,所以系統(tǒng)直接拋出異常。
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@2760298 rejected from java.util.concurrent.ThreadPoolExecutor@7ea7cd51[Running, pool size = 10, active threads = 10, queued tasks = 1024, completed tasks = 846]
復(fù)制代碼
通過(guò)ps -ef|grep java命令找出 Java 進(jìn)程 pid,執(zhí)行jstack pid 即可出現(xiàn)java線程堆棧信息,這里發(fā)現(xiàn)了5個(gè)死鎖,我們只列出其中一個(gè),很明顯線程pool-1-thread-2鎖住了0x00000000f8387d88等待0x00000000f8387d98鎖,線程pool-1-thread-1鎖住了0x00000000f8387d98等待鎖0x00000000f8387d88,這就產(chǎn)生了死鎖。
Java stack information for the threads listed above:
===================================================
"pool-1-thread-2":
? ? ? ? at top.luozhou.analysisdemo.controller.DeadLockThread2.run(DeadLockThread.java:30)
? ? ? ? - waiting to lock <0x00000000f8387d98> (a java.lang.Object)
? ? ? ? - locked <0x00000000f8387d88> (a java.lang.Object)
? ? ? ? at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
? ? ? ? at java.util.concurrent.FutureTask.run(FutureTask.java:266)
? ? ? ? at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
? ? ? ? at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
? ? ? ? at java.lang.Thread.run(Thread.java:748)
"pool-1-thread-1":
? ? ? ? at top.luozhou.analysisdemo.controller.DeadLockThread1.run(DeadLockThread.java:30)
? ? ? ? - waiting to lock <0x00000000f8387d88> (a java.lang.Object)
? ? ? ? - locked <0x00000000f8387d98> (a java.lang.Object)
? ? ? ? at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
? ? ? ? at java.util.concurrent.FutureTask.run(FutureTask.java:266)
? ? ? ? at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
? ? ? ? at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
? ? ? ? at java.lang.Thread.run(Thread.java:748)
Found 5 deadlocks.
復(fù)制代碼
線程頻繁切換
上下文切換會(huì)導(dǎo)致將大量CPU時(shí)間浪費(fèi)在寄存器、內(nèi)核棧以及虛擬內(nèi)存的保存和恢復(fù)上,導(dǎo)致系統(tǒng)整體性能下降。當(dāng)你發(fā)現(xiàn)系統(tǒng)的性能出現(xiàn)明顯的下降時(shí)候,需要考慮是否發(fā)生了大量的線程上下文切換。
@GetMapping(value = "/thread/swap")
? ? public String theadSwap(int num) {
? ? ? ? System.out.println("模擬線程切換");
? ? ? ? for (int i = 0; i < num; i++) {
? ? ? ? ? ? new Thread(new ThreadSwap1(new AtomicInteger(0)),"thread-swap"+i).start();
? ? ? ? }
? ? ? ? return "ok";
? ? }
public class ThreadSwap1 implements Runnable {
? ? private AtomicInteger integer;
? ? public ThreadSwap1(AtomicInteger integer) {
? ? ? ? this.integer = integer;
? ? }
? ? @Override
? ? public void run() {
? ? ? ? while (true) {
? ? ? ? ? ? integer.addAndGet(1);
? ? ? ? ? ? Thread.yield(); //讓出CPU資源
? ? ? ? }
? ? }
}
復(fù)制代碼
這里我創(chuàng)建多個(gè)線程去執(zhí)行基礎(chǔ)的原子+1操作,然后讓出 CPU 資源,理論上 CPU 就會(huì)去調(diào)度別的線程,我們請(qǐng)求接口創(chuàng)建100個(gè)線程看看效果如何,curl localhost:8080/thread/swap?num=100。接口請(qǐng)求成功后,我們執(zhí)行`vmstat 1 10,表示每1秒打印一次,打印10次,線程切換采集結(jié)果如下:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r? b? swpd? free? buff? cache? si? so? ? bi? ? bo? in? cs us sy id wa st
101? 0 128000 878384? ? 908 468684? ? 0? ? 0? ? 0? ? 0 4071 8110498 14 86? 0? 0? 0
100? 0 128000 878384? ? 908 468684? ? 0? ? 0? ? 0? ? 0 4065 8312463 15 85? 0? 0? 0
100? 0 128000 878384? ? 908 468684? ? 0? ? 0? ? 0? ? 0 4107 8207718 14 87? 0? 0? 0
100? 0 128000 878384? ? 908 468684? ? 0? ? 0? ? 0? ? 0 4083 8410174 14 86? 0? 0? 0
100? 0 128000 878384? ? 908 468684? ? 0? ? 0? ? 0? ? 0 4083 8264377 14 86? 0? 0? 0
100? 0 128000 878384? ? 908 468688? ? 0? ? 0? ? 0? 108 4182 8346826 14 86? 0? 0? 0
復(fù)制代碼
這里我們關(guān)注4個(gè)指標(biāo),r,cs,us,sy。
r=100,說(shuō)明等待的進(jìn)程數(shù)量是100,線程有阻塞。
cs=800多萬(wàn),說(shuō)明每秒上下文切換了800多萬(wàn)次,這個(gè)數(shù)字相當(dāng)大了。
us=14,說(shuō)明用戶態(tài)占用了14%的CPU時(shí)間片去處理邏輯。
sy=86,說(shuō)明內(nèi)核態(tài)占用了86%的CPU,這里明顯就是做上下文切換工作了。
我們通過(guò)top命令以及top -Hp pid查看進(jìn)程和線程CPU情況,發(fā)現(xiàn)Java線程CPU占滿了,但是線程CPU使用情況很平均,沒(méi)有某一個(gè)線程把CPU吃滿的情況。
PID USER? ? ? PR? NI? ? VIRT? ? RES? ? SHR S? %CPU %MEM? ? TIME+ COMMAND? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
87093 root? ? ? 20? 0 4194788 299056? 13252 S 399.7 16.1? 65:34.67 java
復(fù)制代碼
PID USER? ? ? PR? NI? ? VIRT? ? RES? ? SHR S %CPU %MEM? ? TIME+ COMMAND? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
87189 root? ? ? 20? 0 4194788 299056? 13252 R? 4.7 16.1? 0:41.11 java? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
87129 root? ? ? 20? 0 4194788 299056? 13252 R? 4.3 16.1? 0:41.14 java? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
87130 root? ? ? 20? 0 4194788 299056? 13252 R? 4.3 16.1? 0:40.51 java? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
87133 root? ? ? 20? 0 4194788 299056? 13252 R? 4.3 16.1? 0:40.59 java? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
87134 root? ? ? 20? 0 4194788 299056? 13252 R? 4.3 16.1? 0:40.95 java
復(fù)制代碼
結(jié)合上面用戶態(tài)CPU只使用了14%,內(nèi)核態(tài)CPU占用了86%,可以基本判斷是Java程序線程上下文切換導(dǎo)致性能問(wèn)題。
我們使用pidstat命令來(lái)看看Java進(jìn)程內(nèi)部的線程切換數(shù)據(jù),執(zhí)行pidstat -p 87093 -w 1 10,采集數(shù)據(jù)如下:
11:04:30 PM? UID? ? ? TGID? ? ? TID? cswch/s nvcswch/s? Command
11:04:30 PM? ? 0? ? ? ? -? ? 87128? ? ? 0.00? ? 16.07? |__java
11:04:30 PM? ? 0? ? ? ? -? ? 87129? ? ? 0.00? ? 15.60? |__java
11:04:30 PM? ? 0? ? ? ? -? ? 87130? ? ? 0.00? ? 15.54? |__java
11:04:30 PM? ? 0? ? ? ? -? ? 87131? ? ? 0.00? ? 15.60? |__java
11:04:30 PM? ? 0? ? ? ? -? ? 87132? ? ? 0.00? ? 15.43? |__java
11:04:30 PM? ? 0? ? ? ? -? ? 87133? ? ? 0.00? ? 16.02? |__java
11:04:30 PM? ? 0? ? ? ? -? ? 87134? ? ? 0.00? ? 15.66? |__java
11:04:30 PM? ? 0? ? ? ? -? ? 87135? ? ? 0.00? ? 15.23? |__java
11:04:30 PM? ? 0? ? ? ? -? ? 87136? ? ? 0.00? ? 15.33? |__java
11:04:30 PM? ? 0? ? ? ? -? ? 87137? ? ? 0.00? ? 16.04? |__java
復(fù)制代碼
根據(jù)上面采集的信息,我們知道Java的線程每秒切換15次左右,正常情況下,應(yīng)該是個(gè)位數(shù)或者小數(shù)。結(jié)合這些信息我們可以斷定Java線程開(kāi)啟過(guò)多,導(dǎo)致頻繁上下文切換,從而影響了整體性能。
為什么系統(tǒng)的上下文切換是每秒800多萬(wàn),而 Java 進(jìn)程中的某一個(gè)線程切換才15次左右?
系統(tǒng)上下文切換分為三種情況:
1、多任務(wù):在多任務(wù)環(huán)境中,一個(gè)進(jìn)程被切換出CPU,運(yùn)行另外一個(gè)進(jìn)程,這里會(huì)發(fā)生上下文切換。
2、中斷處理:發(fā)生中斷時(shí),硬件會(huì)切換上下文。在vmstat命令中是in
3、用戶和內(nèi)核模式切換:當(dāng)操作系統(tǒng)中需要在用戶模式和內(nèi)核模式之間進(jìn)行轉(zhuǎn)換時(shí),需要進(jìn)行上下文切換,比如進(jìn)行系統(tǒng)函數(shù)調(diào)用。
Linux 為每個(gè) CPU 維護(hù)了一個(gè)就緒隊(duì)列,將活躍進(jìn)程按照優(yōu)先級(jí)和等待 CPU 的時(shí)間排序,然后選擇最需要 CPU 的進(jìn)程,也就是優(yōu)先級(jí)最高和等待 CPU 時(shí)間最長(zhǎng)的進(jìn)程來(lái)運(yùn)行。也就是vmstat命令中的r。
那么,進(jìn)程在什么時(shí)候才會(huì)被調(diào)度到 CPU 上運(yùn)行呢?
進(jìn)程執(zhí)行完終止了,它之前使用的 CPU 會(huì)釋放出來(lái),這時(shí)再?gòu)木途w隊(duì)列中拿一個(gè)新的進(jìn)程來(lái)運(yùn)行
為了保證所有進(jìn)程可以得到公平調(diào)度,CPU 時(shí)間被劃分為一段段的時(shí)間片,這些時(shí)間片被輪流分配給各個(gè)進(jìn)程。當(dāng)某個(gè)進(jìn)程時(shí)間片耗盡了就會(huì)被系統(tǒng)掛起,切換到其它等待 CPU 的進(jìn)程運(yùn)行。
進(jìn)程在系統(tǒng)資源不足時(shí),要等待資源滿足后才可以運(yùn)行,這時(shí)進(jìn)程也會(huì)被掛起,并由系統(tǒng)調(diào)度其它進(jìn)程運(yùn)行。
當(dāng)進(jìn)程通過(guò)睡眠函數(shù) sleep 主動(dòng)掛起時(shí),也會(huì)重新調(diào)度。
當(dāng)有優(yōu)先級(jí)更高的進(jìn)程運(yùn)行時(shí),為了保證高優(yōu)先級(jí)進(jìn)程的運(yùn)行,當(dāng)前進(jìn)程會(huì)被掛起,由高優(yōu)先級(jí)進(jìn)程來(lái)運(yùn)行。
發(fā)生硬件中斷時(shí),CPU 上的進(jìn)程會(huì)被中斷掛起,轉(zhuǎn)而執(zhí)行內(nèi)核中的中斷服務(wù)程序。
結(jié)合我們之前的內(nèi)容分析,阻塞的就緒隊(duì)列是100左右,而我們的CPU只有4核,這部分原因造成的上下文切換就可能會(huì)相當(dāng)高,再加上中斷次數(shù)是4000左右和系統(tǒng)的函數(shù)調(diào)用等,整個(gè)系統(tǒng)的上下文切換到800萬(wàn)也不足為奇了。Java內(nèi)部的線程切換才15次,是因?yàn)榫€程使用Thread.yield()來(lái)讓出CPU資源,但是CPU有可能繼續(xù)調(diào)度該線程,這個(gè)時(shí)候線程之間并沒(méi)有切換,這也是為什么內(nèi)部的某個(gè)線程切換次數(shù)并不是非常大的原因。
總結(jié)
本文模擬了常見(jiàn)的性能問(wèn)題場(chǎng)景,分析了如何定位CPU100%、內(nèi)存泄漏、死鎖、線程頻繁切換問(wèn)題。分析問(wèn)題我們需要做好兩件事,第一,掌握基本的原理,第二,借助好工具。本文也列舉了分析問(wèn)題的常用工具和命令,希望對(duì)你解決問(wèn)題有所幫助。當(dāng)然真正的線上環(huán)境可能十分復(fù)雜,并沒(méi)有模擬的環(huán)境那么簡(jiǎn)單,但是原理是一樣的,問(wèn)題的表現(xiàn)也是類似的,我們重點(diǎn)抓住原理,活學(xué)活用,相信復(fù)雜的線上問(wèn)題也可以順利解決。
覺(jué)得文章寫的還不錯(cuò)的,麻煩老鐵點(diǎn)贊+關(guān)注支持一下,后期會(huì)不斷更新技術(shù)好文,不迷路