Java程式設計師漲薪必備的效能調優知識點,收好了!

HeapDump效能社群發表於2021-10-28

Java 應用效能優化是一個老生常談的話題,典型的效能問題如頁面響應慢、介面超時,伺服器負載高、併發數低,資料庫頻繁死鎖等。尤其是在“糙快猛”的網際網路開發模式大行其道的今天,隨著系統訪問量的增加和程式碼的日漸臃腫,各種效能問題開始紛至沓來。Java 應用效能的瓶頸點非常多,比如磁碟、記憶體、網路 I/O 等系統因素,Java 應用程式碼,JVM GC,資料庫,快取等。將 Java 效能優化分為 4 個層級:應用層、資料庫層、框架層、JVM 層。

每層優化難度逐級增加,涉及的知識和解決的問題也會不同

  • 應用層需要理解程式碼邏輯,通過 Java 執行緒棧定位有問題程式碼行等;
  • 資料庫層面需要分析 SQL、定位死鎖等;
  • 框架層需要懂原始碼,理解框架機制;
  • JVM 層需要對 GC 的型別和工作機制有深入瞭解,對各種 JVM 引數作用瞭然於胸。

圍繞 Java 效能優化,有兩種最基本的分析方法:現場分析法和事後分析法。現場分析法通過保留現場,再採用診斷工具分析定位。現場分析對線上影響較大,部分場景(特別是涉及到使用者關鍵的線上業務時)不太合適。事後分析法需要儘可能多收集現場資料,然後立即恢復服務,同時針對收集的現場資料進行事後分析和復現。下面我們從效能診斷工具出發,分享回顧HeapDump效能社群中的一些經典案例與實踐。

效能診斷工具

效能診斷一種是針對已經確定有效能問題的系統和程式碼進行診斷,還有一種是對預上線系統提前效能測試,確定效能是否符合上線要求。本文主要針對前者,後者可以用各種效能壓測工具(例如 JMeter)進行測試,不在本文討論範圍內。針對 Java 應用,效能診斷工具主要分為兩層:OS 層面和 Java 應用層面(包括應用程式碼診斷和 GC 診斷)。

OS 診斷

OS 的診斷主要關注的是 CPU、Memory、I/O 三個方面。

CPU 診斷

對於 CPU 主要關注平均負載(Load Average),CPU 使用率,上下文切換次數(Context Switch)。

通過 top 命令可以檢視系統平均負載和 CPU 使用率。
PerfMa開源的XPocket外掛容器中整合了top_x,它是linux top的增強版, 可以顯示CPU佔用率/負載,CPU及記憶體程式使用的list。這個外掛對於繁雜的top命令輸出進行了功能的拆分和整理,更加清晰易用,支援管道化,尤其可以直接拿到top程式或執行緒tid、pid;。mem_s命令增加了按照程式swap大小佔用排序增強了原有top功能。

圖上顯示當前系統的 cpu被使用了51%多。在發現某些程式佔用cpu比較高時,可以使用top_x的 cpu_t命令,該命令會自動獲取當前cpu佔用最高程式的cpu情況,也可以通過-p引數指定程式pid,直接使用cpu_t可以看到:

通過 vmstat 命令可以檢視 CPU 的上下文切換次數,XPocket同樣整合了vmstat工具。

上下文切換次數發生的場景主要有如下幾種:

  • 時間片用完,CPU 正常排程下一個任務
  • 被其它優先順序更高的任務搶佔
  • 執行任務碰到 I/O 阻塞,掛起當前任務,切換到下一個任務
  • 使用者程式碼主動掛起當前任務讓出 CPU
  • 多工搶佔資源,由於沒有搶到被掛起
  • 硬體中斷。

Java 執行緒上下文切換主要來自共享資源的競爭。一般單個物件加鎖很少成為系統瓶頸,除非鎖粒度過大。但在一個訪問頻度高,對多個物件連續加鎖的程式碼塊中就可能出現大量上下文切換,成為系統瓶頸。作者朱紀兵的CPU上下文切換導致服務雪崩一文中就記錄了在log4j使用非同步AsyncLogger寫日誌導致的CPU頻繁上下文切換最終導致服務雪崩的案例。AsyncLogger使用了disruptor框架,而disruptor框架在核心資料結構RingBuffer上處理MultiProducer。在寫入日誌的時候需要Sequence,但是此時RingBuffer已經滿了,獲取不到Sequence,disruptor會呼叫Unsafe.park會將當前執行緒主動掛起。簡單來說就是消費速度跟不上生產速度的時候,生產執行緒做了無限重試,重試間隔為1 nano,導致cpu頻繁掛起喚醒,發生大量cpu切換,佔用cpu資源。把Distuptor版本和og4j2版本分別到3.3.6 和 2.7問題得以解決。

Memory

從作業系統角度,記憶體關注應用程式是否足夠,可以使用 free –m 命令檢視記憶體的使用情況。通過 top 命令可以檢視程式使用的虛擬記憶體 VIRT 和實體記憶體 RES,根據公式 VIRT = SWAP + RES 可以推算出具體應用使用的交換分割槽(Swap)情況,使用交換分割槽過大會影響 Java 應用效能,可以將 swappiness 值調到儘可能小。因為對於 Java 應用來說,佔用太多交換分割槽可能會影響效能,畢竟磁碟效能比記憶體慢太多。

I/O

I/O 包括磁碟 I/O 和網路 I/O,一般情況下磁碟更容易出現 I/O 瓶頸。通過 iostat 可以檢視磁碟的讀寫情況,通過 CPU 的 I/O wait 可以看出磁碟 I/O 是否正常。如果磁碟 I/O 一直處於很高的狀態,說明磁碟太慢或故障,成為了效能瓶頸,需要進行應用優化或者磁碟更換。

除了常用的 top、 ps、vmstat、iostat 等命令,還有其他 Linux 工具可以診斷系統問題,如 mpstat、tcpdump、netstat、pidstat、sar 等。此處總結列出了 Linux 不同裝置型別的效能診斷工具,如下圖所示,可供參考。

Java 應用診斷工具

應用程式碼診斷

應用程式碼效能問題是相對好解決的一類效能問題。通過一些應用層面監控報警,如果確定有問題的功能和程式碼,直接通過程式碼就可以定位;或者通過 top+jstack,找出有問題的執行緒棧,定位到問題執行緒的程式碼上,也可以發現問題。對於更復雜,邏輯更多的程式碼段,通過 Stopwatch 列印效能日誌往往也可以定位大多數應用程式碼效能問題。

常用的 Java 應用診斷包括執行緒、堆疊、GC 等方面的診斷。

jstack

jstack 命令通常配合 top 使用,通過 top -H -p pid 定位 Java 程式和執行緒,再利用 jstack -l pid 匯出執行緒棧。由於執行緒棧是瞬態的,因此需要多次 dump,一般 3 次 dump,一般每次隔 5s 就行。將 top 定位的 Java 執行緒 pid 轉成 16 進位制,得到 Java 執行緒棧中的 nid,可以找到對應的問題執行緒棧。

XPocket中整合了jstack_x工具,可以使用stack -t nid命令檢視某個等待鎖執行緒的呼叫棧,通過呼叫棧來定位業務程式碼。

XElephant、XSheepdog

XElephant是HeapDmp效能社群免費提供的一款線上分析Java記憶體Dump檔案的產品。可以讓記憶體裡物件之間的各種依賴關係更加清晰明瞭,無需安裝軟體,提供上傳方式,不受本地機器記憶體限制,支援超大Dump檔案分析。

XSheepdog是HeapDmp效能社群免費提供的一款線上分析執行緒Dump檔案的產品,將執行緒、執行緒池、棧、方法及鎖的關係梳理清楚,通過多種視角呈獻給使用者,讓執行緒問題一目瞭然。

GC 診斷

Java GC 解決了程式設計師管理記憶體的風險,但 GC 引起的應用暫停成了另一個需要解決的問題。JDK 提供了一系列工具來定位 GC 問題,比較常用的有 jstat、jmap,還有第三方工具 MAT 等。

jstat

jstat 命令可列印 GC 詳細資訊,Young GC 和 Full GC 次數,堆資訊等。其命令格式為
jstat –gcxxx -t pid <interval> <count>。

MAT

MAT 是 Java 堆的分析利器,提供了直觀的診斷報告,內建的 OQL 允許對堆進行類 SQL 查詢,功能強大,outgoing reference 和 incoming reference 可以對物件引用追根溯源。

MAT 有兩列顯示物件大小,分別是 Shallow size 和 Retained size,前者表示物件本身佔用記憶體的大小,不包含其引用的物件,後者是物件自己及其直接或間接引用的物件的 Shallow size 之和,即該物件被回收後 GC 釋放的記憶體大小,一般說來關注後者大小即可。對於有些大堆 (幾十 G) 的 Java 應用,需要較大記憶體才能開啟 MAT。通常本地開發機記憶體過小,是無法開啟的,建議線上下伺服器端安裝圖形環境和 MAT,遠端開啟檢視。或者執行 mat 命令生成堆索引,拷貝索引到本地,不過這種方式看到的堆資訊有限。

為了診斷 GC 問題,建議在 JVM 引數中加上-XX:+PrintGCDateStamps。

對於 Java 應用,通過 top+jstack+jmap+MAT 可以定位大多數應用和記憶體問題,可謂必備工具。有些時候,Java 應用診斷需要參考 OS 相關資訊,可使用一些更全面的診斷工具,比如 Zabbix(整合了 OS 和 JVM 監控)等。在分散式環境中,分散式跟蹤系統等基礎設施也對應用效能診斷提供了有力支援。

效能優化實踐

在介紹了一些常用的效能診斷工具後,下面將結合我們在 Java 應用調優中的一些實踐,從 JVM 層、應用程式碼層以及資料庫層進行案例分享。

JVM 調優:GC 之痛

作者阿飛Javaer的[FullGC實戰:業務小姐姐檢視圖片時一直在轉圈圈
](https://heapdump.cn/article/2...)一文中記錄了介面耗時長導致圖片無法訪問的情況,排除掉資料庫、同步日至阻塞問題、系統問題後,開始排查GC問題。使用jstat命令後輸出結果如下所示

bash-4.4$ /app/jdk1.8.0_192/bin/jstat -gc 1 2s
 S0C     S1C       S0U    S1U   EC       EU       OC          OU        MC      MU      CCSC   CCSU      YGC   YGCT   FGC    FGCT     GCT 
170496.0 170496.0  0.0    0.0   171008.0 130368.9 1024000.0   590052.8  70016.0 68510.8 8064.0 7669.0    983   13.961 1400   275.606  289.567
170496.0 170496.0  0.0    0.0   171008.0 41717.2  1024000.0   758914.9  70016.0 68510.8 8064.0 7669.0    987   14.011 1401   275.722  289.733
170496.0 170496.0  0.0    0.0   171008.0 126547.2 1024000.0   770587.2  70016.0 68510.8 8064.0 7669.0    990   14.091 1403   275.986  290.077
170496.0 170496.0  0.0    0.0   171008.0 45488.7  1024000.0   650767.0  70016.0 68531.9 8064.0 7669.0    994   14.148 1405   276.222  290.371
170496.0 170496.0  0.0    0.0   171008.0 146029.1 1024000.0   714857.2  70016.0 68531.9 8064.0 7669.0    995   14.166 1406   276.366  290.531
170496.0 170496.0  0.0    0.0   171008.0 118073.5 1024000.0   669163.2  70016.0 68531.9 8064.0 7669.0    998   14.226 1408   276.736  290.962
170496.0 170496.0  0.0    0.0   171008.0  3636.1  1024000.0   687630.0  70016.0 68535.6 8064.0 7669.6   1001   14.342 1409   276.871  291.213
170496.0 170496.0  0.0    0.0   171008.0 87247.2  1024000.0   704977.5  70016.0 68535.6 8064.0 7669.6   1005   14.463 1411   277.099  291.562

幾乎每1秒都有一次FGC,且停頓時間相當長。最後關閉了引數 -XX:-UseAdaptiveSizePolicy,優化後重啟服務,訪問速度又快起來了。

GC 調優對高併發大資料量互動的應用還是很有必要的,尤其是預設 JVM 引數通常不滿足業務需求,需要進行專門調優。GC 日誌的解讀有很多公開的資料,本文不再贅述。GC 調優目標基本有三個思路:降低 GC 頻率,可以通過增大堆空間,減少不必要物件生成;降低 GC 暫停時間,可以通過減少堆空間,使用 CMS GC 演算法實現;避免 Full GC,調整 CMS 觸發比例,避免 Promotion Failure 和 Concurrent mode failure(老年代分配更多空間,增加 GC 執行緒數加快回收速度),減少大物件生成等。

應用層調優:嗅到壞程式碼的味道

從應用層程式碼調優入手,剖析程式碼效率下降的根源,無疑是提高 Java 應用效能的很好的手段之一。
FGC實戰:壞程式碼導致服務頻繁FGC無響應問題分析一文就記錄了壞程式碼導致記憶體洩漏CPU佔用過高大量介面超時的案例。

使用MAT工具分析jvm Heap,從上面的餅圖中可以看出,絕大多數堆記憶體都被同一個記憶體佔用了,再檢視堆記憶體詳情,向上層追溯,很快就發現了罪魁禍首。

找到記憶體洩漏的物件了,在專案裡全域性搜尋物件名,它是一個 Bean 物件,然後定位到它的一個型別為 Map 的屬性。這個 Map 根據型別用 ArrayList 儲存了每次探測介面響應的結果,每次探測完都塞到 ArrayList 裡去分析,由於 Bean 物件不會被回收,這個屬性又沒有清除邏輯,所以在服務十來天沒有上線重啟的情況下,這個 Map 越來越大,直至將記憶體佔滿。記憶體滿了之後,無法再給 HTTP 響應結果分配記憶體了,所以一直卡在 readLine 那。而我們那個大量 I/O 的介面報警次數特別多,估計跟響應太大需要更多記憶體有關。

對於壞程式碼的定位,除了常規意義上的程式碼審查外,藉助 MAT 等工具也可以在一定程度對系統效能瓶頸點進行快速定位。但是一些與特定場景繫結或者業務資料繫結的情況,卻需要輔助程式碼走查、效能檢測工具、資料模擬甚至線上引流等方式才能最終確認效能問題的出處。以下是我們總結的一些壞程式碼可能的一些特徵,供大家參考:
(1)程式碼可讀性差,無基本程式設計規範;
(2)物件生成過多或生成大物件,記憶體洩露等;
(3)IO 流操作過多,或者忘記關閉;
(4)資料庫操作過多,事務過長;
(5)同步使用的場景錯誤;
(6)迴圈迭代耗時操作等。

資料庫層調優:死鎖噩夢

對於大部分 Java 應用來說,與資料庫進行互動的場景非常普遍,尤其是 OLTP 這種對於資料一致性要求較高的應用,資料庫的效能會直接影響到整個應用的效能。

通常來說,對於資料庫層的調優我們基本上會從以下幾個方面出發:
(1)在 SQL 語句層面進行優化:慢 SQL 分析、索引分析和調優、事務拆分等;
(2)在資料庫配置層面進行優化:比如欄位設計、調整快取大小、磁碟 I/O 等資料庫引數優化、資料碎片整理等;
(3)從資料庫結構層面進行優化:考慮資料庫的垂直拆分和水平拆分等;
(4)選擇合適的資料庫引擎或者型別適應不同場景,比如考慮引入 NoSQL 等。

總結與建議

效能調優同樣遵循 2-8 原則,80%的效能問題是由 20%的程式碼產生的,因此優化關鍵程式碼事半功倍。同時,對效能的優化要做到按需優化,過度優化可能引入更多問題。對於 Java 效能優化,不僅要理解系統架構、應用程式碼,同樣需要關注 JVM 層甚至作業系統底層。總結起來主要可以從以下幾點進行考慮:

1)基礎效能的調優
這裡的基礎效能指的是硬體層級或者作業系統層級的升級優化,比如網路調優,作業系統版本升級,硬體裝置優化等。比如 F5 的使用和 SDD 硬碟的引入,包括新版本 Linux 在 NIO 方面的升級,都可以極大的促進應用的效能提升;

2)資料庫效能優化
包括常見的事務拆分,索引調優,SQL 優化,NoSQL 引入等,比如在事務拆分時引入非同步化處理,最終達到一致性等做法的引入,包括在針對具體場景引入的各類 NoSQL 資料庫,都可以大大緩解傳統資料庫在高併發下的不足;

3)應用架構優化
引入一些新的計算或者儲存框架,利用新特性解決原有叢集計算效能瓶頸等;或者引入分散式策略,在計算和儲存進行水平化,包括提前計算預處理等,利用典型的空間換時間的做法等;都可以在一定程度上降低系統負載;

4)業務層面的優化
技術並不是提升系統效能的唯一手段,在很多出現效能問題的場景中,其實可以看到很大一部分都是因為特殊的業務場景引起的,如果能在業務上進行規避或者調整,其實往往是最有效的。

相關文章