Java性能調優還不會?這七個知識點帶你輕鬆掌握

前言

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤爲是在「糙快猛」的互聯網開發模式大行其道的今天,隨着系統訪問量的日益增長和代碼的臃腫,各類性能問題開始紛至沓來。小編也對應整理了一份JVM性能調優的思惟導圖,詳細的知識點能夠點開看看呢! Java 應用性能的瓶頸點很是多,好比磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據我的經驗,將 Java 性能優化分爲 4 個層級:應用層、數據庫層、框架層、JVM 層。前端

Java 性能優化分層模型

Java性能調優最強實踐 每層優化難度逐級增長,涉及的知識和解決的問題也會不一樣。好比應用層須要理解代碼邏輯,經過 Java 線程棧定位有問題代碼行等;數據庫層面須要分析 SQL、定位死鎖等;框架層須要懂源代碼,理解框架機制;JVM 層須要對 GC 的類型和工做機制有深刻了解,對各類 JVM 參數做用瞭然於胸。java

圍繞 Java 性能優化,有兩種最基本的分析方法:現場分析法和過後分析法。ios

現場分析法經過保留現場,再採用診斷工具分析定位。現場分析對線上影響較大,部分場景(特別是涉及到用戶關鍵的在線業務時)不太合適。程序員

過後分析法須要儘量多收集現場數據,而後當即恢復服務,同時針對收集的現場數據進行過後分析和復現。下面咱們從性能診斷工具出發,分享一些案例與實踐。面試

1、性能診斷工具

性能診斷一種是針對已經肯定有性能問題的系統和代碼進行診斷,還有一種是對預上線系統提早性能測試,肯定性能是否符合上線要求。算法

本文主要針對前者,後者能夠用各類性能壓測工具(例如 JMeter)進行測試,不在本文討論範圍內。sql

針對 Java 應用,性能診斷工具主要分爲兩層:OS 層面和 Java 應用層面(包括應用代碼診斷和 GC 診斷)。數據庫

OS 診斷編程

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

2、 CPU 診斷

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

經過 top 命令能夠查看系統平均負載和 CPU 使用率,圖 2 爲經過 top 命令查看某系統的狀態。

top 命令示例

平均負載有三個數字:63.66,58.39,57.18,分別表示過去 1 分鐘、5 分鐘、15 分鐘機器的負載。按照經驗,若數值小於 0.7*CPU 個數,則系統工做正常;若超過這個值,甚至達到 CPU 核數的四五倍,則系統的負載就明顯偏高。

top 命令示例中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統爲 16 核),說明系統出現負載問題,且存在進一步升高趨勢,須要定位具體緣由了。

經過 vmstat 命令能夠查看 CPU 的上下文切換次數,以下圖所示:

vmstat 命令示例

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

1)時間片用完,CPU 正常調度下一個任務;

2)被其它優先級更高的任務搶佔;

3)執行任務碰到 I/O 阻塞,掛起當前任務,切換到下一個任務;

4)用戶代碼主動掛起當前任務讓出 CPU;

5)多任務搶佔資源,因爲沒有搶到被掛起;

6)硬件中斷。

Java 線程上下文切換主要來自共享資源的競爭。通常單個對象加鎖不多成爲系統瓶頸,除非鎖粒度過大。但在一個訪問頻度高,對多個對象連續加鎖的代碼塊中就可能出現大量上下文切換,成爲系統瓶頸。

好比在咱們系統中就曾出現 log4j 1.x 在較大併發下大量打印日誌,出現頻繁上下文切換,大量線程阻塞,致使系統吞吐量大降的狀況,其相關代碼如清單 1 所示,升級到 log4j 2.x 才解決這個問題。

for(Category c = this; c != null; c=c.parent) {
 // Protected against simultaneous call to addAppender, removeAppender,…
 synchronized(c) {
 if (c.aai != null) {
 write += c.aai.appendLoopAppenders(event);
 }
 …
 }
}

3、 Memory

從操做系統角度,內存關注應用進程是否足夠,可使用 free –m 命令查看內存的使用狀況。

經過 top 命令能夠查看進程使用的虛擬內存 VIRT 和物理內存 RES,根據公式 VIRT = SWAP + RES 能夠推算出具體應用使用的交換分區(Swap)狀況,使用交換分區過大會影響 Java 應用性能,能夠將 swappiness 值調到儘量小。

由於對於 Java 應用來講,佔用太多交換分區可能會影響性能,畢竟磁盤性能比內存慢太多。

4、 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 等。Brendan 總結列出了 Linux 不一樣設備類型的性能診斷工具,以下圖所示,可供參考。

Linux 性能觀測工具

5、 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,能夠找到對應的問題線程棧。

經過 top –H -p 查看運行時間較長 Java 線程

如圖 5 所示,其中的線程 24985 運行時間較長,可能存在問題,轉成 16 進制後,經過 Java 線程棧找到對應線程 0x6199 的棧以下,從而定位問題點,以下圖所示。

jstack 查看線程堆棧

Java性能調優最強實踐 JProfiler

JProfiler 可對 CPU、堆、內存進行分析,功能強大,以下圖所示。同時結合壓測工具,能夠對代碼耗時採樣統計。

經過 JProfiler 進行內存分析

6、 GC 診斷

Java GC 解決了程序員管理內存的風險,但 GC 引發的應用暫停成了另外一個須要解決的問題。JDK 提供了一系列工具來定位 GC 問題,比較經常使用的有 jstat、jmap,還有第三方工具 MAT 等。

jstat

jstat 命令可打印 GC 詳細信息,Young GC 和 Full GC 次數,堆信息等。其命令格式爲

jstat –gcxxx -t pid <interval> <count>,以下圖所示。

jstat 命令示例

jmap

jmap 打印 Java 進程堆信息 jmap –heap pid。經過 jmap –dump:file=xxx pid 可 dump 堆到文件,而後經過其它工具進一步分析其堆使用狀況

MAT

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

MAT 示例

Java性能調優最強實踐 圖 9 是 MAT 使用示例,MAT 有兩列顯示對象大小,分別是 Shallow size 和 Retained size,前者表示對象自己佔用內存的大小,不包含其引用的對象,後者是對象本身及其直接或間接引用的對象的 Shallow size 之和,即該對象被回收後 GC 釋放的內存大小,通常說來關注後者大小便可。

對於有些大堆 (幾十 G) 的 Java 應用,須要較大內存才能打開 MAT。

一般本地開發機內存太小,是沒法打開的,建議在線下服務器端安裝圖形環境和 MAT,遠程打開查看。或者執行 mat 命令生成堆索引,拷貝索引到本地,不過這種方式看到的堆信息有限。

爲了診斷 GC 問題,建議在 JVM 參數中加上-XX:+PrintGCDateStamps。經常使用的 GC 參數以下圖所示。

經常使用 GC 參數

對於 Java 應用,經過 top+jstack+jmap+MAT 能夠定位大多數應用和內存問題,可謂必備工具。有些時候,Java 應用診斷須要參考 OS 相關信息,可以使用一些更全面的診斷工具,好比 Zabbix(整合了 OS 和 JVM 監控)等。在分佈式環境中,分佈式跟蹤系統等基礎設施也對應用性能診斷提供了有力支持。

7、性能優化實踐

在介紹了一些經常使用的性能診斷工具後,下面將結合咱們在 Java 應用調優中的一些實踐,從 JVM 層、應用代碼層以及數據庫層進行案例分享。

JVM 調優:GC 之痛

XX商業平臺某系統重構時選擇 RMI 做爲內部遠程調用協議,系統上線後開始出現週期性的服務中止響應,暫停時間由數秒到數十秒不等。經過觀察 GC 日誌,發現服務自啓動後每小時會出現一次 Full GC。因爲系統堆設置較大,Full GC 一次暫停應用時間會較長,這對線上實時服務影響較大。

通過分析,在重構前系統沒有出現按期 Full GC 的狀況,所以懷疑是 RMI 框架層面的問題。經過公開資料,發現 RMI 的 GDC(Distributed Garbage Collection,分佈式垃圾收集)會啓動守護線程按期執行 Full GC 來回收遠程對象,清單 2 中展現了其守護線程代碼。

清單 2.DGC 守護線程源代碼

private static class Daemon extends Thread {
 public void run() {
 for (;;) { 
 //…
 long d = maxObjectInspectionAge();
 if (d >= l) {
 System.gc(); 
 d = 0;
 }
 //…
 }
 }
}

定位問題後解決起來就比較容易了。一種是經過增長-XX:+DisableExplicitGC 參數,直接禁用系統 GC 的顯示調用,但對使用 NIO 的系統,會有堆外內存溢出的風險。

另外一種方式是經過調大 -Dsun.rmi.dgc.server.gcInterval 和-Dsun.rmi.dgc.client.gcInterval 參數,增長 Full GC 間隔,同時增長參數-XX:+ExplicitGCInvokesConcurrent,將一次徹底 Stop-The-World 的 Full GC 調整爲一次併發 GC 週期,減小應用暫停時間,同時對 NIO 應用也不會形成影響。

從下圖可知,調整以後的 Full GC 次數 在 3 月以後明顯減小。

Full GC 監控統計

GC 調優對高併發大數據量交互的應用仍是頗有必要的,尤爲是默認 JVM 參數一般不知足業務需求,須要進行專門調優。GC 日誌的解讀有不少公開的資料,本文再也不贅述。

GC 調優目標基本有三個思路:下降 GC 頻率,能夠經過增大堆空間,減小沒必要要對象生成;下降 GC 暫停時間,能夠經過減小堆空間,使用 CMS GC 算法實現;避免 Full GC,調整 CMS 觸發比例,避免 Promotion Failure 和 Concurrent mode failure(老年代分配更多空間,增長 GC 線程數加快回收速度),減小大對象生成等。

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

從應用層代碼調優入手,剖析代碼效率降低的根源,無疑是提升 Java 應用性能的很好的手段之一。

某商業廣告系統(採用 Nginx 進行負載均衡)某第二天常上線後,其中有幾臺機器負載急劇升高,CPU 使用率迅速打滿。咱們對線上進行了緊急回滾,並經過 jmap 和 jstack 對其中某臺服務器的現場進行保存。

經過 MAT 分析堆棧現場

Java性能調優最強實踐 堆棧現場如上圖所示,根據 MAT 對 dump 數據的分析,發現最多的內存對象爲 byte[] 和 java.util.HashMap $Entry,且 java.util.HashMap $Entry 對象存在循環引用。初步定位在該 HashMap 的 put 過程當中有可能出現了死循環問題(圖中 java.util.HashMap $Entry 0x2add6d992cb8 和 0x2add6d992ce8 的 next 引用造成循環)。

查閱相關文檔定位這屬於典型的併發使用的場景錯誤

簡要的說就是 HashMap 自己並不具有多線程併發的特性,在多個線程同時 put 操做的狀況下,內部數組進行擴容時會致使 HashMap 的內部鏈表造成環形結構,從而出現死循環。

針對這次上線,最大的改動在於經過內存緩存網站數據來提高系統性能,同時使用了懶加載機制,如清單 3 所示。

清單 3. 網站數據懶加載代碼

private static Map<Long, UnionDomain> domainMap = new HashMap<Long, UnionDomain>();
 private boolean isResetDomains() {
 if (CollectionUtils.isEmpty(domainMap)) {
 // 從遠端 http 接口獲取網站詳情
 List<UnionDomain> newDomains = unionDomainHttpClient
 .queryAllUnionDomain();
 if (CollectionUtils.isEmpty(domainMap)) {
 domainMap = new HashMap<Long, UnionDomain>();
 for (UnionDomain domain : newDomains) {
 if (domain != null) {
 domainMap.put(domain.getSubdomainId(), domain);
 }
 }
 }
 return true;
 }
 return false;
 }

能夠看到此處的 domainMap 爲靜態共享資源,它是 HashMap 類型,在多線程狀況下會致使其內部鏈表造成環形結構,出現死循環。

經過對前端 Nginx 的鏈接和訪問日誌能夠看到,因爲在系統重啓後 Nginx 積攢了大量的用戶請求,在 Resin 容器啓動,大量用戶請求涌入應用系統,多個用戶同時進行網站數據的請求和初始化工做,致使 HashMap 出現併發問題。在定位故障緣由後解決方法則比較簡單,主要的解決方法有:

(1)採用 ConcurrentHashMap 或者同步塊的方式解決上述併發問題; (2)在系統啓動前完成網站緩存加載,去除懶加載等; (3)採用分佈式緩存替換本地緩存等。

對於壞代碼的定位,除了常規意義上的代碼審查外,藉助諸如 MAT 之類的工具也能夠在必定程度對系統性能瓶頸點進行快速定位。可是一些與特定場景綁定或者業務數據綁定的狀況,卻須要輔助代碼走查、性能檢測工具、數據模擬甚至線上引流等方式才能最終確認性能問題的出處。如下是咱們總結的一些壞代碼可能的一些特徵,供你們參考:

(1)代碼可讀性差,無基本編程規範; (2)對象生成過多或生成大對象,內存泄露等; (3)IO 流操做過多,或者忘記關閉; (4)數據庫操做過多,事務過長; (5)同步使用的場景錯誤; (6)循環迭代耗時操做等。

數據庫層調優:死鎖噩夢

對於大部分 Java 應用來講,與數據庫進行交互的場景很是廣泛,尤爲是 OLTP 這種對於數據一致性要求較高的應用,數據庫的性能會直接影響到整個應用的性能。搜狗商業平臺系統做爲廣告主的廣告發布和投放平臺,對其物料的實時性和一致性都有極高的要求,咱們在關係型數據庫優化方面也積累了必定的經驗。

對於廣告物料庫來講,較高的操做頻繁度(特別是經過批量物料工具操做)很極易形成數據庫的死鎖狀況發生,其中一個比較典型的場景是廣告物料調價。客戶每每會頻繁的對物料的出價進行調整,從而間接給數據庫系統形成較大的負載壓力,也加重了死鎖發生的可能性。下面以搜狗商業平臺某廣告系統廣告物料調價的案例進行說明。

某商業廣告系統某天訪問量突增,形成系統負載升高以及數據庫頻繁死鎖,死鎖語句以下圖所示。

死鎖語句

其中,groupdomain 表上索引爲 idx_groupdomain_accountid (accountid),idx_groupdomain_groupid(groupid),primary(groupdomainid) 三個單索引結構,採用 Mysql innodb 引擎。

此場景發生在更新組出價時,場景中存在着組、組行業(groupindus 表)和組網站(groupdomain 表)。

當更新組出價時,若組行業出價使用組出價(經過 isusegroupprice 標示,若爲 1 則使用組出價)。同時若組網站出價使用組行業出價(經過 isuseindusprice 標示,若爲 1 則使用組行業出價)時,也須要同時更新其組網站出價。因爲每一個組下面最大能夠有 3000 個網站,所以在更新組出價時會長時間的對相關記錄進行鎖定。

從上面發生死鎖的問題能夠看到,事務 1 和事務 2 均選擇了 idx_groupdomain_accountid 的單列索引。根據 Mysql innodb 引擎加鎖的特色,在一次事務中只會選擇一個索引使用,並且若是一旦使用二級索引進行加鎖後,會嘗試將主鍵索引進行加鎖。進一步分析可知事務 1 在請求事務 2 持有的idx_groupdomain_accountid二級索引加鎖(加鎖範圍「space id 5726 page no 8658 n bits 824 index」),可是事務 2 已得到該二級索引 (「space id 5726 page no 8658 n bits 824 index」) 上所加的鎖,在等待請求鎖定主鍵索引 PRIMARY 索引上的鎖。因爲事務 2 等待執行時間過長或長時間不釋放鎖,致使事務 1 最終發生回滾。

經過對當天訪問日誌跟蹤能夠看到,當天有客戶經過腳本方式發起大量的修改推廣組出價的操做,致使有大量事務在循環等待前一個事務釋放鎖定的主鍵 PRIMARY 索引。該問題的根源實際上在於 Mysql innodb 引擎對於索引利用有限,在 Oracle 數據庫中此問題並不突出。

解決的方式天然是但願單個事務鎖定的記錄數越少越好,這樣產生死鎖的機率也會大大下降。最終使用了(accountid, groupid)的複合索引,縮小了單個事務鎖定的記錄條數,也實現了不一樣計劃下的推廣組數據記錄的隔離,從而減小該類死鎖的發生概率。

一般來講,對於數據庫層的調優咱們基本上會從如下幾個方面出發:

(1)在 SQL 語句層面進行優化:慢 SQL 分析、索引分析和調優、事務拆分等;

(2)在數據庫配置層面進行優化:好比字段設計、調整緩存大小、磁盤 I/O 等數據庫參數優化、數據碎片整理等;

(3)從數據庫結構層面進行優化:考慮數據庫的垂直拆分和水平拆分等;

(4)選擇合適的數據庫引擎或者類型適應不一樣場景,好比考慮引入 NoSQL 等。

8、總結與建議

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

1)基礎性能的調優

這裏的基礎性能指的是硬件層級或者操做系統層級的升級優化,好比網絡調優,操做系統版本升級,硬件設備優化等。好比 F5 的使用和 SDD 硬盤的引入,包括新版本 Linux 在 NIO 方面的升級,均可以極大的促進應用的性能提高;

2)數據庫性能優化

包括常見的事務拆分,索引調優,SQL 優化,NoSQL 引入等,好比在事務拆分時引入異步化處理,最終達到一致性等作法的引入,包括在針對具體場景引入的各種 NoSQL 數據庫,均可以大大緩解傳統數據庫在高併發下的不足;

3)應用架構優化

引入一些新的計算或者存儲框架,利用新特性解決原有集羣計算性能瓶頸等;或者引入分佈式策略,在計算和存儲進行水平化,包括提早計算預處理等,利用典型的空間換時間的作法等;均可以在必定程度上下降系統負載;

4)業務層面的優化

技術並非提高系統性能的惟一手段,在不少出現性能問題的場景中,其實能夠看到很大一部分都是由於特殊的業務場景引發的,若是能在業務上進行規避或者調整,其實每每是最有效的。

小編分享的內容到這裏就結束了!

小編這邊整理了一些JVM性能調優400多頁的資料文檔 以及 2020最新的Java核心面試資料集錦200多頁,關注公衆號:麒麟改bug。

相關文章
相關標籤/搜索