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 ,

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實戰:業務小姐姐查看圖片時一直在轉圈圈
一文中記錄了介面耗時長導致圖片無法訪問的情況,排除掉資料庫、同步日至阻塞問題、系統問題后,開始排查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)業務層面的優化
技術并不是提升系統性能的唯一手段,在很多出現性能問題的場景中,其實可以看到很大一部分都是因為特殊的業務場景引起的,如果能在業務上進行規避或者調整,其實往往是最有效的,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/333607.html
標籤:其他
