在經過工具獲得異常指標,初步定位瓶頸點後,若是進一步進行確認和調優?咱們在這裏提供一些可實踐、可借鑑、可參考的性能調優「套路」,即:如何在衆多異常性能指標中,找出最核心的那一個,進而定位性能瓶頸點,最後進行性能調優。如下會按照代碼、CPU、內存、網絡、磁盤等方向進行組織,針對對某一各優化點,會有系統的「套路」總結,便於思路的遷移實踐。java
1、應用代碼相關ios
遇到性能問題,首先應該作的是檢查否與業務代碼相關——不是經過閱讀代碼解決問題,而是經過日誌或代碼,排除掉一些與業務代碼相關的低級錯誤。性能優化的最佳位置,是應用內部。git
譬如,查看業務日誌,檢查日誌內容裏是否有大量的報錯產生,應用層、框架層的一些性能問題,大多數都能從日誌裏找到端倪(日誌級別設置不合理,致使線上瘋狂打日誌);再者,檢查代碼的主要邏輯,如 for 循環的不合理使用、NPE、正則表達式、數學計算等常見的一些問題,均可以經過簡單地修改代碼修復問題。github
別動輒就把性能優化和緩存、異步化、JVM 調優等名詞掛鉤,複雜問題可能會有簡單解,「二八原則」在性能優化的領域裏裏依然有效。固然了,瞭解一些基本的「代碼經常使用踩坑點」,能夠加速咱們問題分析思路的過程,從 CPU、內存、JVM 等分析到的一些瓶頸點優化思路,也有可能在代碼這裏體現出來。正則表達式
下面是一些高頻的,容易形成性能問題的編碼要點。數據庫
1)正則表達式很是消耗 CPU(如貪婪模式可能會引發回溯),慎用字符串的 split()、replaceAll() 等方法;正則表達式表達式必定預編譯。數組
2)String.intern() 在低版本(Java 1.6 以及以前)的 JDK 上使用,可能會形成方法區(永久代)內存溢出。在高版本 JDK 中,若是 string pool 設置過小而緩存的字符串過多,也會形成較大的性能開銷。緩存
3)輸出異常日誌的時候,若是堆棧信息是明確的,能夠取消輸出詳細堆棧,異常堆棧的構造是有成本的。注意:同一位置拋出大量重複的堆棧信息,JIT 會將其優化後成,直接拋出一個事先編譯好的、類型匹配的異常,異常堆棧信息就看不到了。安全
4)避免引用類型和基礎類型之間無謂的拆裝箱操做,請儘可能保持一致,自動裝箱發生太頻繁,會很是嚴重消耗性能。性能優化
5)Stream API 的選擇。複雜和並行操做,推薦使用 Stream API,能夠簡化代碼,同時發揮來發揮出 CPU 多核的優點,若是是簡單操做或者 CPU 是單核,推薦使用顯式迭代。
6)根據業務場景,經過 ThreadPoolExecutor 手動建立線程池,結合任務的不一樣,指定線程數量和隊列大小,規避資源耗盡的風險,統一命名後的線程也便於後續問題排查。
7)根據業務場景,合理選擇併發容器。如選擇 Map 類型的容器時,若是對數據要求有強一致性,可以使用 Hashtable 或者 「Map + 鎖」 ;讀遠大於寫,使用 CopyOnWriteArrayList;存取數據量小、對數據沒有強一致性的要求、變動不頻繁的,使用 ConcurrentHashMap;存取數據量大、讀寫頻繁、對數據沒有強一致性的要求,使用 ConcurrentSkipListMap。
8)鎖的優化思路有:減小鎖的粒度、循環中使用鎖粗化、減小鎖的持有時間(讀寫鎖的選擇)等。同時,也考慮使用一些 JDK 優化後的併發類,如對一致性要求不高的統計場景中,使用 LongAdder 替代 AtomicLong 進行計數,使用 ThreadLocalRandom 替代 Random 類等。
代碼層的優化除了上面這些,還有不少就不一一列出了。咱們能夠觀察到,在這些要點裏,有一些共性的優化思路,是能夠抽取出來的,譬如
a. 空間換時間:使用內存或者磁盤,換取更寶貴的CPU 或者網絡,如緩存的使用
b. 時間換空間:經過犧牲部分 CPU,節省內存或者網絡資源,如把一次大的網絡傳輸變成屢次;
c. 其餘諸如並行化、異步化、池化技術等。
2、CPU 相關
前面講到過,咱們更應該關注 CPU 負載,CPU 利用率高通常不是問題,CPU 負載 是判斷系統計算資源是否健康的關鍵依據。
3、CPU 利用率高&&平均負載高
這種狀況常見於 CPU 密集型的應用,大量的線程處於可運行狀態,I/O 不多,常見的大量消耗 CPU 資源的應用場景有:
a. 正則操做
b. 數學運算
c. 序列化/反序列化
d. 反射操做
e. 死循環或者不合理的大量循環
f. 基礎/第三方組件缺陷
排查高 CPU 佔用的通常思路:經過 jstack 屢次(> 5次)打印線程棧,通常能夠定位到消耗 CPU 較多的線程堆棧。或者經過 Profiling 的方式(基於事件採樣或者埋點),獲得應用在一段時間內的 on-CPU 火焰圖,也能較快定位問題。
還有一種可能的狀況,此時應用存在頻繁的 GC (包括 Young GC、Old GC、Full GC),這也會致使 CPU 利用率和負載都升高。排查思路:使用 jstat -gcutil 持續輸出當前應用的 GC 統計次數和時間。頻繁 GC 致使的負載升高,通常還伴隨着可用內存不足,可用 free 或者 top 等命令查看下當前機器的可用內存大小。
CPU 利用率太高,是否有多是 CPU 自己性能瓶頸致使的呢?也是有可能的。能夠進一步經過 vmstat 查看詳細的 CPU 利用率。用戶態 CPU 利用率(us)較高,說明用戶態進程佔用了較多的 CPU,若是這個值長期大於50%,應該着重排查應用自己的性能問題。內核態 CPU 利用率(sy)較高,說明內核態佔用了較多的 CPU,因此應該着重排查內核線程或者系統調用的性能問題。若是 us + sy 的值大於 80%,說明 CPU 可能不足
4、CPU 利用率低&&平均負載高
若是CPU利用率不高,說明咱們的應用並無忙於計算,而是在幹其餘的事。CPU 利用率低而平均負載高,常見於 I/O 密集型進程,這很容易理解,畢竟平均負載就是 R 狀態進程和 D 狀態進程的和,除掉了第一種,就只剩下 D 狀態進程了(產生 D 狀態的緣由通常是由於在等待 I/O,例如磁盤 I/O、網絡 I/O 等)。
排查&&驗證思路:使用 vmstat 1 定時輸出系統資源使用,觀察 %wa(iowait) 列的值,該列標識了磁盤 I/O 等待時間在 CPU 時間片中的百分比,若是這個值超過30%,說明磁盤 I/O 等待嚴重,這多是大量的磁盤隨機訪問或直接的磁盤訪問(沒有使用系統緩存)形成的,也可能磁盤自己存在瓶頸,能夠結合 iostat 或 dstat 的輸出加以驗證,如 %wa(iowait) 升高同時觀察到磁盤的讀請求很大,說明多是磁盤讀致使的問題。
此外,耗時較長的網絡請求(即網絡 I/O)也會致使 CPU 平均負載升高,如 MySQL 慢查詢、使用 RPC 接口獲取接口數據等。這種狀況的排查通常須要結合應用自己的上下游依賴關係以及中間件埋點的 trace 日誌,進行綜合分析。
5、CPU 上下文切換次數變高
先用 vmstat 查看系統的上下文切換次數,而後經過 pidstat 觀察進程的自願上下文切換(cswch)和非自願上下文切換(nvcswch)狀況。自願上下文切換,是由於應用內部線程狀態發生轉換所致,譬如調用 sleep()、join()、wait()等方法,或使用了 Lock 或 synchronized 鎖結構;非自願上下文切換,是由於線程因爲被分配的時間片用完或因爲執行優先級被調度器調度所致。
若是自願上下文切換次數較高,意味着 CPU 存在資源獲取等待,好比說,I/O、內存等系統資源不足等。若是是非自願上下文切換次數較高,可能的緣由是應用內線程數過多,致使 CPU 時間片競爭激烈,頻頻被系統強制調度,此時能夠結合 jstack 統計的線程數和線程狀態分佈加以佐證。
6、內存相關
前面提到,內存分爲系統內存和進程內存(含 Java 應用進程),通常咱們遇到的內存問題,絕大多數都會落在進程內存上,系統資源形成的瓶頸佔比較小。對於 Java 進程,它自帶的內存管理自動化地解決了兩個問題:如何給對象分配內存以及如何回收分配給對象的內存,其核心是垃圾回收機制。
垃圾回收雖然能夠有效地防止內存泄露、保證內存的有效使用,但也並非萬能的,不合理的參數配置和代碼邏輯,依然會帶來一系列的內存問題。此外,早期的垃圾回收器,在功能性和回收效率上也不是很好,過多的 GC 參數設置很是依賴開發人員的調優經驗。好比,對於最大堆內存的不恰當設置,可能會引起堆溢出或者堆震盪等一系列問題。
下面看看幾個常見的內存問題分析思路。
一、系統內存不足
Java 應用通常都有單機或者集羣的內存水位監控,若是單機的內存利用率大於 95%,或者集羣的內存利用率大於80%,就說明可能存在潛在的內存問題(注:這裏的內存水位是系統內存)。
除了一些較極端的狀況,通常系統內存不足,大機率是由 Java 應用引發的。使用 top 命令時,咱們能夠看到 Java 應用進程的實際內存佔用,其中 RES 表示進程的常駐內存使用,VIRT 表示進程的虛擬內存佔用,內存大小的關係爲:VIRT > RES > Java 應用實際使用的堆大小。除了堆內存,Java 進程總體的內存佔用,還有方法區/元空間、JIT 緩存等,主要組成以下:Java 應用內存佔用 = Heap(堆區)+ Code Cache(代碼緩存區) + Metaspace(元空間)+ Symbol tables(符號表)+ Thread stacks(線程棧區)+ Direct buffers(堆外內存)+ JVM structures(其餘的一些 JVM 自身佔用)+ Mapped files(內存映射文件)+ Native Libraries(本地庫)+ ...
Java 進程的內存佔用,可使用 jstat -gc 命令查看,輸出的指標中能夠獲得當前堆內存各分區、元空間的使用狀況。堆外內存的統計和使用狀況,能夠利用 NMT(Native Memory Tracking,HotSpot VM Java8 引入)獲取。線程棧使用的內存空間很容易被忽略,雖然線程棧內存採用的是懶加載的模式,不會直接使用 +Xss 的大小來分配內存,可是過多的線程也會致使沒必要要的內存佔用,可使用 jstackmem 這個腳本統計總體的線程佔用。
二、系統內存不足的排查思路:
a. 首先使用 free 查看當前內存的可用空間大小,而後使用 vmstat 查看具體的內存使用狀況及內存增加趨勢,這個階段通常能定位佔用內存最多的進程;
b. 分析緩存 / 緩衝區的內存使用。若是這個數值在一段時間變化不大,能夠忽略。若是觀察到緩存 / 緩衝區的大小在持續升高,則可使用 pcstat、cachetop、slabtop 等工具,分析緩存 / 緩衝區的具體佔用;
c. 排除掉緩存 / 緩衝區對系統內存的影響後,若是發現內存還在不斷增加,說明頗有可能存在內存泄漏。
7、Java 內存溢出
內存溢出是指應用新建一個對象實例時,所需的內存空間大於堆的可用空間。內存溢出的種類較多,通常會在報錯日誌裏看到 OutOfMemoryError 關鍵字。
常見內存溢出種類及分析思路以下:
1)java.lang.OutOfMemoryError: Java heap space。緣由:堆中(新生代和老年代)沒法繼續分配對象了、某些對象的引用長期被持有沒有被釋放,垃圾回收器沒法回收、使用了大量的 Finalizer 對象,這些對象並不在 GC 的回收週期內等。通常堆溢出都是因爲內存泄漏引發的,若是確認沒有內存泄漏,能夠適當經過增大堆內存。
2)java.lang.OutOfMemoryError:GC overhead limit exceeded。緣由:垃圾回收器超過98%的時間用來垃圾回收,但回收不到2%的堆內存,通常是由於存在內存泄漏或堆空間太小。
3)java.lang.OutOfMemoryError: Metaspace或java.lang.OutOfMemoryError: PermGen space。排查思路:檢查是否有動態的類加載但沒有及時卸載,是否有大量的字符串常量池化,永久代/元空間是否設置太小等。
4)java.lang.OutOfMemoryError : unable to create new native Thread。緣由:虛擬機在拓展棧空間時,沒法申請到足夠的內存空間。可適當下降每一個線程棧的大小以及應用總體的線程個數。此外,系統裏整體的進程/線程建立總數也受到系統空閒內存和操做系統的限制,請仔細檢查。注:這種棧溢出,和 StackOverflowError 不一樣,後者是因爲方法調用層次太深,分配的棧內存不夠新建棧幀致使。
此外,還有 Swap 分區溢出、本地方法棧溢出、數組分配溢出等 OutOfMemoryError 類型,因爲不是很常見,就不一一介紹了。
8、Java 內存泄漏
Java 內存泄漏能夠說是開發人員的噩夢,內存泄漏與內存溢出不一樣則,後者簡單粗暴,現場也比較好找。內存泄漏的表現是:應用運行一段時間後,內存利用率愈來愈高,響應愈來愈慢,直到最終出現進程「假死」。
Java 內存泄漏可能會形成系統可用內存不足、進程假死、OOM 等,排查思路卻不外乎下面兩種:
a. 經過 jmap 按期輸出堆內對象統計,定位數量和大小持續增加的對象;
b. 使用 Profiler 工具對應用進行 Profiling,尋找內存分配熱點。
此外,在堆內存持續增加時,建議 dump 一份堆內存的快照,後面能夠基於快照作一些分析。快照雖然是瞬時值,但也是有必定的意義的。
9、垃圾回收相關
GC(垃圾回收,下同)的各項指標,是衡量 Java 進程內存使用是否健康的重要標尺。垃圾回收最核心指標:GC Pause(包括 MinorGC 和 MajorGC) 的頻率和次數,以及每次回收的內存詳情,前者能夠經過 jstat 工具直接獲得,後者須要分析 GC 日誌。須要注意的是,jstat 輸出列中的 FGC/FGCT 表示的是一次老年代垃圾回收中,出現 GC Pause (即 Stop-the-World)的次數,譬如對於 CMS 垃圾回收器,每次老年代垃圾回收這個值會增長2(初始標記和從新標記着兩個 Stop-the-World 的階段,這個統計值會是 2。
何時須要進行 GC 調優?這取決於應用的具體狀況,譬如對響應時間的要求、對吞吐量的要求、系統資源限制等。一些經驗:GC 頻率和耗時大幅上升、GC Pause 平均耗時超過 500ms、Full GC 執行頻率小於1分鐘等,若是 GC 知足上述的一些特徵,說明須要進行 GC 調優了。
因爲垃圾回收器種類繁多,針對不一樣的應用,調優策略也有所區別,所以下面介紹幾種通用的的 GC 調優策略。
1)選擇合適的 GC 回收器。根據應用對延遲、吞吐的要求,結合各垃圾回收器的特色,合理選用。推薦使用 G1 替換 CMS 垃圾回收器,G1 的性能是在逐步優化的,在 8GB 內存及如下的機器上,其各方面的表現也在遇上甚至有超越之勢。G1 調參較方便,而 CMS 垃圾回收器參數太過複雜、容易形成空間碎片化、對 CPU 消耗較高等弊端,也使其目前處於廢棄狀態。Java 11 裏新引入的 ZGC 垃圾回收器,基本可用作到全階段併發標記和回收,值得期待。
2)合理的堆內存大小設置。堆大小不要設置過大,建議不要超過系統內存的 75%,避免出現系統內存耗盡。最大堆大小和初始化堆的大小保持一致,避免堆震盪。新生代的大小設置比較關鍵,咱們調整 GC 的頻率和耗時,不少時候就是在調整新生代的大小,包括新生代和老年代的佔比、新生代中 Eden 區和 Survivor 區的比例等,這些比例的設置還須要考慮各代中對象的晉升年齡,整個過程須要考慮的東西仍是比較多的。若是使用 G1 垃圾回收器,新生代大小這一塊須要考慮的東西就少不少了,自適應的策略會決定每一次的回收集合(CSet)。新生代的調整是 GC 調優的核心,很是依賴經驗,可是通常來講,Young GC 頻率高,意味着新生代過小(或 Eden 區和 Survivor 配置不合理),Young GC 時間長,意味着新生代過大,這兩個方向大致不差。
3)下降 Full GC 的頻率。若是出現了頻繁的 Full GC 或者 老年代 GC,頗有多是存在內存泄漏,致使對象被長期持有,經過 dump 內存快照進行分析,通常能較快地定位問題。除此以外,新生代和老年代的比例不合適,致使對象頻頻被直接分配到老年代,也有可能會形成 Full GC,這個時候須要結合業務代碼和內存快照綜合分析。
此外,經過配置 GC 參數,能夠幫助咱們獲取不少 GC 調優所需的關鍵信息,如配置-XX:+PrintGCApplicationStoppedTime-XX:+PrintSafepointStatistics-XX:+PrintTenuringDistribution,分別能夠獲取 GC Pause 分佈、安全點耗時統計、對象晉升年齡分佈的信息,加上 -XX:+PrintFlagsFinal 可讓咱們瞭解最終生效的 GC 參數等。
10、磁盤I/O和網絡I/O
磁盤 I/O 問題排查思路:
a. 使用工具輸出磁盤相關的輸出的指標,經常使用的有 %wa(iowait)、%util,根據輸判斷磁盤 I/O 是否存在異常,譬如 %util 這個指標較高,說明有較重的 I/O 行爲;
b. 使用 pidstat 定位到具體進程,關注下讀或寫的數據大小和速率;
c. 使用 lsof + 進程號,可查看該異常進程打開的文件列表(含目錄、塊設備、動態庫、網絡套接字等),結合業務代碼,通常可定位到 I/O 的來源,若是須要具體分析,還可使用 perf 等工具進行 trace 定位 I/O 源頭。
須要注意的是,%wa(iowait)的升高不表明必定意味着磁盤 I/O 存在瓶頸,這是數值表明 CPU 上 I/O 操做的時間佔用的百分比,若是應用進程的在這段時間內的主要活動就是 I/O,那麼也是正常的。
網絡 I/O 存在瓶頸,可能的緣由以下:
a. 一次傳輸的對象過大,可能會致使請求響應慢,同時 GC 頻繁;
b. 網絡 I/O 模型選擇不合理,致使應用總體 QPS 較低,響應時間長;
c. RPC 調用的線程池設置不合理。可以使用 jstack 統計線程數的分佈,若是處於 TIMED_WAITING 或 WAITING 狀態的線程較多,則須要重點關注。舉例:數據庫鏈接池不夠用,體如今線程棧上就是不少線程在競爭一把鏈接池的鎖;
d. RPC 調用超時時間設置不合理,形成請求失敗較多;
Java 應用的線程堆棧快照很是有用,除了上面提到的用於排查線程池配置不合理的問題,其餘的一些場景,如 CPU 飆高、應用響應較慢等,均可以先從線程堆棧入手。
11、有用的一行命令
給出若干在定位性能問題的命令,用於快速定位
1)查看系統當前網絡鏈接數
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
2)查看堆內對象的分佈 Top 50(定位內存泄漏)
jmap –histo:live $pid | sort-n -r -k2 | head-n 50
3)按照 CPU/內存的使用狀況列出前10 的進程
#內存 ps axo %mem,pid,euser,cmd | sort -nr | head -10 #CPU ps -aeo pcpu,user,pid,cmd | sort -nr | head -10
4)顯示系統總體的 CPU利用率和閒置率
grep "cpu " /proc/stat | awk -F ' ' '{total = $2 + $3 + $4 + $5} END {print "idle \t used\n" $5*100/total "% " $2*100/total "%"}'
5)按線程狀態統計線程數(增強版)
jstack $pid | grep java.lang.Thread.State:|sort|uniq -c | awk '{sum+=$1; split($0,a,":");gsub(/^[ \t]+|[ \t]+$/, "", a[2]);printf "%s: %s\n", a[2], $1}; END {printf "TOTAL: %s",sum}';
6)查看最消耗 CPU 的 Top10 線程機器堆棧信息
推薦使用 show-busy-java-threads 腳本,該腳本可用於快速排查 Java 的 CPU 性能問題(top us值太高),自動查出運行的 Java 進程中消耗 CPU 多的線程,並打印出其線程棧,從而肯定致使性能問題的方法調用,該腳本已經用於阿里線上運維環境。連接地址:https://github.com/oldratlee/useful-scripts/。
7)火焰圖生成(須要安裝 perf、perf-map-agent、FlameGraph 這三個項目):
# 1. 收集應用運行時的堆棧和符號表信息(採樣時間30秒,每秒99個事件); sudo perf record -F 99 -p $pid -g -- sleep 30; ./jmaps # 2. 使用 perf script 生成分析結果,生成的 flamegraph.svg 文件就是火焰圖。 sudo perf script | ./pkgsplit-perf.pl | grep java | ./flamegraph.pl > flamegraph.svg
8)按照 Swap 分區的使用狀況列出前 10 的進程
for file in /proc/*/status ; do awk '/VmSwap|Name|^Pid/{printf $2 " " $3}END{ print ""}' $file; done | sort -k 3 -n -r | head -10
9)JVM 內存使用及垃圾回收狀態統計
#顯示最後一次或當前正在發生的垃圾收集的誘發緣由 jstat -gccause $pid #顯示各個代的容量及使用狀況 jstat -gccapacity $pid #顯示新生代容量及使用狀況 jstat -gcnewcapacity $pid #顯示老年代容量 jstat -gcoldcapacity $pid #顯示垃圾收集信息(間隔1秒持續輸出) jstat -gcutil $pid 1000
10)其餘的一些平常命令
# 快速殺死全部的 java 進程 ps aux | grep java | awk '{ print $2 }' | xargs kill -9 # 查找/目錄下佔用磁盤空間最大的top10文件 find / -type f -print0 | xargs -0 du -h | sort -rh | head -n 10
參考資料:
[1]https://github.com/superhj1987/awesome-scripts?[2]https://github.com/jvm-profiling-tools/perf-map-agent?[3]https://github.com/brendangregg/FlameGraph?[4] https://github.com/apangin/jstackmem/blob/master/jstackmem.py