效能優化隨筆

朱清震發表於2018-02-08

效能優化遵循木桶原則,最短的一塊板決定了系統瓶頸,某一時刻只有一個瓶頸點,解決了這個瓶頸點,才能發現下一個瓶頸。

效能優化就是要在現有的資源裡(cpu、記憶體、硬碟io、網路io等),最大限度的把這些資源利用起來;

效能優化需要從3方面:

1. cpu 使用率:如果cpu使用率低,可以嘗試增加工作執行緒數,不能無限制增加,每個應用都有一個最優值,要看cpu密集型操作與io密集型操作佔用的時間比例,非cpu操作時間越多,那麼執行緒數需要的也就越多,極端情況下全部都是cpu密集型的操作,那麼理論上只需要建立與伺服器物理核數一樣的執行緒數即可(一般會建立cpu核數+1個執行緒,防止記憶體缺頁中斷之類的cpu等待),否則會導致頻繁的上下文切換,反而使系統效能下降(上下文切換見後面);極端情況下全部都是io密集型操作,那麼執行緒數越多越好,直到到達io裝置的瓶頸,即使這樣也無法充分將cpu利用起來;

隨著工作執行緒數的增加,cpu使用率應該越來越高,如果增加執行緒數,發現cpu使用率仍然不能提升,如果io沒有到達瓶頸,那麼接下來要排查程式裡面是否有加鎖操作,對某些資源的訪問需要進行同步,這些操作導致多執行緒只能序列執行,併發是不能突破序列的限制,這種瓶頸比較容易發現,通過列印java的執行緒棧,找到加鎖和等待鎖的位置,來判斷是否是因為這些鎖導致的問題;

 

2. 記憶體:jvm記憶體如果跟現有訪問量設定的大小不匹配,則會引發頻繁的gc操作,降低應用的吞吐率,嚴重的可以直接導致程式oom,另外gc操作是一項非常消耗cpu的操作,導致cpu接近100%的使用率;另外還要防止系統記憶體不夠用使用swap,swap會將部分記憶體中的資料儲存到磁碟上,一旦需要訪問這些資料,就會拖慢系統;

3.io:例如本地io例如讀取磁碟, 網路io例如查詢mysql,查詢redis,查詢es,查詢mq,呼叫遠端介面,這些都會降低cpu使用率,優化方式一般是使用快取,本地快取,分散式快取;

如果發現應用吞吐量上不去,並且cup利用率很低,嘗試增加執行緒數,如果增加執行緒數後,cpu利用率仍然上不去,那麼就要排查io問題或者是併發鎖問題,如果是io瓶頸,那麼就要看硬體問題,如果是鎖併發導致的,需要對鎖進行優化,或採用其它設計方案,消除鎖;

上下文切換

執行緒切換會導致上下文的切換,上下文切換需要儲存執行緒當前執行狀態,還會導致當前執行緒執行的cpu快取失效,這些都是額外的開銷,cpu密集型的應用如果切換頻繁,會導致吞吐率下降嚴重 ,更詳細內容見這篇文章:http://ifeve.com/java-context-switch/

cas鎖的使用場景

場景一:

1.業務程式碼耗時時間短,甚至比執行緒切換耗時還要少;

2.競爭情況不激烈;

場景二:

操作耗時長,競爭激烈,非公平模式;這種情況下可以先使用cas嘗試獲取鎖,如果獲取到了,就可以避免執行緒切換,避免cpu快取失效,從而提高吞吐量;缺點是會導致被阻塞的執行緒獲取鎖的概率下降,等待鎖時間變長;

在java中cas鎖會與volatile關鍵字一起使用,對於讀多,寫少的變數,可以使用volatile+cas控制。

無鎖程式設計

無鎖程式設計的優點是顯而易見的,它可以充分利用多核cpu的計算能力,避免執行緒切換、快取失效帶來的額外開銷,避免鎖帶來的死鎖問題;

很多人認為無鎖程式設計就是cas操作,這樣理解其實是不正確的,cas也是一種鎖,稱之為樂觀鎖,它可以避免執行緒切換、快取失效帶來的開銷,但是cas仍會使多核的並行執行變為序列執行,為了避免執行緒阻塞,會做一些無用功,下面的例子我們可以瞭解一下,除了cas,無鎖程式設計有哪些方式:

例如,計數器,不要設定一個全域性的計數器,這樣所有執行緒都競爭一個資源,可以給每個執行緒持有一個自己的計數器,這樣就沒有鎖競爭的問題了;

例如,客戶端load balance功能,有5個伺服器,客戶端輪詢的方式訪問這五個伺服器,實現負載均衡,一般做法是給每個伺服器編號,1、2、3、4、5,然後一個全域性變數記錄當前訪問的伺服器編號,每訪問一次編號+1,到5的時候就置為1;還有另外一種更好的解決方案,給每個執行緒一個編號,每個執行緒使用自己的編號去輪詢,實現負載均衡,這樣就消除了多執行緒對一個全域性變數的競爭,rocketmq就是這樣實現的;

再例如,jvm中執行緒在年輕帶申請記憶體的時候,會給每個執行緒預先分配1m大小的TLAB,這個TLAB為執行緒私有的,執行緒申請記憶體空間時會在TLAB上申請,這樣就避免了執行緒在堆上申請記憶體空間時鎖住整個堆;

可以看出無鎖程式設計一種形式就是將資源拆分後分配給每個執行緒,這樣就可避免對資源的競爭,本質也是一種鎖的拆分,當拆到每個執行緒上以後,就不存在鎖了;

注意鎖的範圍

當然不是全部的場景都可以將資源拆分或是拆分給每個執行緒,如果一定要使用鎖,注意鎖的範圍,加鎖執行的每一個操作一定是需要互斥的操作,類鎖與物件鎖優先選擇物件鎖;

拆分鎖

避免資源集中,導致鎖的競爭提高,將一個需要競爭的資源拆分為多個,減小鎖的粒度;

例如,計數器,將一個計數器拆分為10個,這樣就鎖爭用衝突情況就變成了原來的1/10;

再例如,讀寫鎖,將讀與寫分離,而不是使用同一把鎖;

鎖的小結

通過上面的討論在多核伺服器上,我們一定可以得出以下結論:

無鎖效能>cas效能

無鎖效能>鎖效能

那麼下面這個結論是否成立呢?

cas效能>鎖操作

顯然是不一定成立的,如果你還不明白為什麼不一定成立,請回去在讀一遍cas鎖的使用場景。

自動拆箱裝箱導致的效能問題

Integer sum = 0;
 for(int i=0; i<10000; i++){
   sum+=i;
}

sum+=i 這個操作,首先會對sum執行拆箱操作,然後執行+i,最後將結果裝箱生成一個新的Integer物件,所以這個for迴圈會建立將近1w個Integer物件;

StringBuilder與+

現在的java中的字串連線符“+”在java中會被優化成隱式的StringBuilder操作;

但是下面的語句就存在效能問題

String[] str= new String[]{"aa","bb","cc", ........};

String newStr ="head ";

for(int i = 0 , j = str.length ; i < j ; i++){

    newStr += str[i] ;

}

newStr += str[i] 這個操作每次都會生成一個新的StringBuilder操作物件,所以這個迴圈執行多少次就會建立出多少個物件來;

另外StringBuilder物件內部實現就是生成一個char[]陣列,預設大小為16,如果超過長度,那麼就會生成一個新的陣列,使用Arrays.copyOf(char[] original, int newLength) 方法將舊的內容拷貝的新的陣列裡面去;會佔用額外的記憶體和產生額外的操作;所以初始化時儘量指定一個最大的大小;

ArrayList、HashMap等集合類的自動擴容問題

與上面提到的StringBuilder一樣,這兩個內部實現也是有一個陣列;陣列是不能擴容的,要想擴容就生成一個新的陣列,然後吧舊陣列的內容通過 Arrays.copyOf()方法拷貝過來,所以它們初始化時一定要指定一個合理的大小;

另外需要說明的是 Arrays.copyOf()方法是淺拷貝;

能用基本型別一定不要使用物件(除非維護、設計方面的需要)

另外物件型別都會有一個markword,需要分配更多的記憶體空間;

基本型別直接在棧上分配空間,會大大減輕你的gc壓力;

GC

導致頻繁ygc的原因:

短時間內生成大量物件;

年輕帶設定的過小,業務請求量大;

導致頻繁fgc的原因:

老年代設定過小,大量永久物件佔用老年代記憶體空間;

大量大物件直接晉升到老年代;

年輕帶設定不合理,導致大量臨時物件過早晉升到老年代;

gc停頓時間過長:

gc演算法中一般耗時就是標記、掃描、壓縮、複製這幾部分;

巨型連結串列資料結構會導致掃描時間過長;

CMS remark階段停頓時間過長,可能是年輕帶指向老年代物件過多導致,可以在remark操作之前執行一次ygc;

用到了swap 或者io繁忙被阻塞導致,jvm寫gc日誌阻塞,此時表現為[Times: user=0.20 sys=0.01, real=18.45 secs] ,user和sys之和大大小於real的情況  https://engineering.linkedin.com/blog/2016/02/eliminating-large-jvm-gc-pauses-caused-by-background-io-traffic 這篇文章詳細的說明這種情況,使用 sar -d -p 1命令可以監控io情況;

堆設定過大,垃圾回收器此時應該優先使用G1,在大堆的情況下其它垃圾回收器很難滿足低停頓的要求;

GC執行緒數少  [Times: user=25.56 sys=0.35, real=20.48 secs];

觸發FullGC

 

System.gc()會導致FullGC,以下方式會執行System.gc():

1. 直接記憶體,如果直接記憶體滿了,會主動呼叫System.gc()去清理;

2. RMI,週期性的呼叫System.gc(),可以通過以下引數配置它的執行週期:

– Dsun.rmi.dgc.server.gcInterval=n

– Dsun.rmi.dgc.client.gcInterval=n

3. 程式中主動呼叫System.gc() 或Runtime.getRuntime().gc();

4. jmx中能夠呼叫

注意:不建議使用-XX:+DisableExplicitGC引數禁止System.gc()的呼叫,原因見第一條;

jmap -histo:live 命令觸發FullGC

 

1. 儘量不要生成大量的臨時大物件,大物件直接晉升到老年代,大量的臨時大物件會導致頻繁的fullgc,大物件大小設定引數為-XX:PretenureSizeThreshold=1000000;

 

2. 如何判斷eden設定是否合理

    使用jstat -gccause pid 1s 觀察eden回收頻率是否過於頻繁,如果過於頻繁有兩方面原因,一方面可能是伺服器訪問量高,瞬間的高併發產生了很多臨時物件,這種情況是正常的,可以通過擴容年輕帶的方式來降低年輕帶的回收頻率 ,雖然增加年輕帶大小會增加單次ygc時間,但是這個關係並不是線性的;另一方面是應用建立了大量的臨時物件,這時要根據應用情況判斷是否可避免大量臨時物件的建立;

3. 如何判斷Survivor設定是否合理

     ygc後eden區倖存下來的物件就會進入到Survivor中,如果物件生命週期比較短,經歷幾次ygc後,在Survivor區就被回收掉了,Survivor是防止臨時物件從年輕帶晉升到老年代的一個緩衝區;

    對於那些存活時間比較長的物件,不可能讓它一直呆在Survivor中來回複製,jvm通過物件的age來控制物件的晉升,Survivor中的物件都有一個age,每經歷一次ygc,如果Survivor中的物件沒有被回收掉,age就會加1,達到一定的age的物件就晉升到老年代了;

     Survivor可以通過-XX:MaxTenuringThreshold=16 來配置物件最大晉升age,注意是最大晉升age,而不是晉升age;那麼如何確定晉升age呢?

     這就涉及到另外一個引數了-XX:TargetSurvivorRatio=50(預設值為50),代表Survivor區域使用量超過50%的物件會全部晉升到老年代,超過50%比例的物件的最小age就是晉升age,但是這個晉升age還不是最終的晉升age,它會與-XX:MaxTenuringThreshold設定的最大值進行比較,選擇其中最小的值作為晉升age,大於等於這個age的物件都會晉升到老年代;

    一般Survivor區域不夠用(即發生溢位,Survivor to區域大小無法容納所有eden和Survivor from中的存活物件,溢位的物件直接進入老年代)或者Survivor區域使用量超過50%,物件就會晉升到老年代,我們可以通過增加Survivor區域大小(例如:-XX:SurvivorRatio=6 代表 <2個Surivivor:Eden=2:6>)和調高Survivor區域的使用量晉升百分比(如-XX:TargetSurvivorRatio=70代表將from區利用率到70%才晉升,),將大部分臨時物件留在年輕帶,需要注意的是要防止Survivor區域溢位,一旦溢位,Survivor to容納不了的物件直接晉升到老年代,如果這些物件都是臨時物件,會加重fullgc

 

這裡有一個引數-XX:+PrintTenuringDistribution可以列印每次ygc後Survivor區域的物件年齡分佈情況以及晉升age;

 

優化手段

1.  處理批量資料(集合類),將單執行緒變為多執行緒,可以自己寫,也可以使用fock/join框架 、stream;

2.  對於處理耗時、更新不頻繁、讀取頻繁的資料,採用快取+定時任務非同步更新的方式,確定一個可以接受的更新頻率;

3.  對於不需要馬上返回處理結果,且處理耗時的資料,可以採用mq快取,後臺任務批量處理;

問題排查

jstack排查應用執行慢的問題,兩種情況可以使用jstack排查

1. 某段程式碼執行時間過長;

2. 鎖導致並行變序列;

如果是因為執行緒數太少導致的,通過jstack是發現不了的;

jvm記憶體溢位問題

1.如果對業務程式碼熟練,使用jmap histo 匯出類的直方圖,找到大物件,根據大物件找到業務程式碼;

2.如果大物件不是業務類,或者對程式碼不熟悉,則dump堆後,看引用關係,找到引用的程式碼位置;

 

 

推薦工具

 

http://gceasy.io/

gclog分析工具,狠給力;

 

http://xxfox.perfma.com/

你假笨的perfma公司出品,狠給力,至少不會讓你的引數設定的離譜;

 

http://www.perfma.com/

笨神的創業產品

 

 

 

 

 

 

相關文章