'Java應用性能調優最強實踐指南(基礎到精通必看'

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 2 中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統為 16 核),說明系統出現負載問題,且存在進一步升高趨勢,需要定位具體原因了。

通過 vmstat 命令可以查看 CPU 的上下文切換次數,如圖 3 所示:

圖 3.vmstat 命令示例

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 2 中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統為 16 核),說明系統出現負載問題,且存在進一步升高趨勢,需要定位具體原因了。

通過 vmstat 命令可以查看 CPU 的上下文切換次數,如圖 3 所示:

圖 3.vmstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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);

}

}

}

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

圖 4.Linux 性能觀測工具

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 2 中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統為 16 核),說明系統出現負載問題,且存在進一步升高趨勢,需要定位具體原因了。

通過 vmstat 命令可以查看 CPU 的上下文切換次數,如圖 3 所示:

圖 3.vmstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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);

}

}

}

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

圖 4.Linux 性能觀測工具

Java應用性能調優最強實踐指南(基礎到精通必看

五、 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,可以找到對應的問題線程棧。

圖 5. 通過 top –H -p 查看運行時間較長 Java 線程

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 2 中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統為 16 核),說明系統出現負載問題,且存在進一步升高趨勢,需要定位具體原因了。

通過 vmstat 命令可以查看 CPU 的上下文切換次數,如圖 3 所示:

圖 3.vmstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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);

}

}

}

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

圖 4.Linux 性能觀測工具

Java應用性能調優最強實踐指南(基礎到精通必看

五、 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,可以找到對應的問題線程棧。

圖 5. 通過 top –H -p 查看運行時間較長 Java 線程

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 6.jstack 查看線程堆棧

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 2 中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統為 16 核),說明系統出現負載問題,且存在進一步升高趨勢,需要定位具體原因了。

通過 vmstat 命令可以查看 CPU 的上下文切換次數,如圖 3 所示:

圖 3.vmstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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);

}

}

}

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

圖 4.Linux 性能觀測工具

Java應用性能調優最強實踐指南(基礎到精通必看

五、 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,可以找到對應的問題線程棧。

圖 5. 通過 top –H -p 查看運行時間較長 Java 線程

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 6.jstack 查看線程堆棧

Java應用性能調優最強實踐指南(基礎到精通必看

JProfiler

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

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 2 中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統為 16 核),說明系統出現負載問題,且存在進一步升高趨勢,需要定位具體原因了。

通過 vmstat 命令可以查看 CPU 的上下文切換次數,如圖 3 所示:

圖 3.vmstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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);

}

}

}

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

圖 4.Linux 性能觀測工具

Java應用性能調優最強實踐指南(基礎到精通必看

五、 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,可以找到對應的問題線程棧。

圖 5. 通過 top –H -p 查看運行時間較長 Java 線程

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 6.jstack 查看線程堆棧

Java應用性能調優最強實踐指南(基礎到精通必看

JProfiler

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

Java應用性能調優最強實踐指南(基礎到精通必看

六、 GC 診斷

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

jstat

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

jstat –gcxxx -t pid <interval> <count>,如圖 8 所示。

圖 8.jstat 命令示例

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 2 中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統為 16 核),說明系統出現負載問題,且存在進一步升高趨勢,需要定位具體原因了。

通過 vmstat 命令可以查看 CPU 的上下文切換次數,如圖 3 所示:

圖 3.vmstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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);

}

}

}

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

圖 4.Linux 性能觀測工具

Java應用性能調優最強實踐指南(基礎到精通必看

五、 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,可以找到對應的問題線程棧。

圖 5. 通過 top –H -p 查看運行時間較長 Java 線程

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 6.jstack 查看線程堆棧

Java應用性能調優最強實踐指南(基礎到精通必看

JProfiler

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

Java應用性能調優最強實踐指南(基礎到精通必看

六、 GC 診斷

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

jstat

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

jstat –gcxxx -t pid <interval> <count>,如圖 8 所示。

圖 8.jstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

jmap

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

MAT

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

圖 9.MAT 示例

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 2 中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統為 16 核),說明系統出現負載問題,且存在進一步升高趨勢,需要定位具體原因了。

通過 vmstat 命令可以查看 CPU 的上下文切換次數,如圖 3 所示:

圖 3.vmstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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);

}

}

}

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

圖 4.Linux 性能觀測工具

Java應用性能調優最強實踐指南(基礎到精通必看

五、 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,可以找到對應的問題線程棧。

圖 5. 通過 top –H -p 查看運行時間較長 Java 線程

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 6.jstack 查看線程堆棧

Java應用性能調優最強實踐指南(基礎到精通必看

JProfiler

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

Java應用性能調優最強實踐指南(基礎到精通必看

六、 GC 診斷

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

jstat

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

jstat –gcxxx -t pid <interval> <count>,如圖 8 所示。

圖 8.jstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

jmap

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

MAT

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

圖 9.MAT 示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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

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

為了診斷 GC 問題,建議在 JVM 參數中加上-XX:+PrintGCDateStamps。常用的 GC 參數如圖 10 所示。

圖 10. 常用 GC 參數

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 2 中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統為 16 核),說明系統出現負載問題,且存在進一步升高趨勢,需要定位具體原因了。

通過 vmstat 命令可以查看 CPU 的上下文切換次數,如圖 3 所示:

圖 3.vmstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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);

}

}

}

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

圖 4.Linux 性能觀測工具

Java應用性能調優最強實踐指南(基礎到精通必看

五、 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,可以找到對應的問題線程棧。

圖 5. 通過 top –H -p 查看運行時間較長 Java 線程

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 6.jstack 查看線程堆棧

Java應用性能調優最強實踐指南(基礎到精通必看

JProfiler

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

Java應用性能調優最強實踐指南(基礎到精通必看

六、 GC 診斷

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

jstat

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

jstat –gcxxx -t pid <interval> <count>,如圖 8 所示。

圖 8.jstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

jmap

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

MAT

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

圖 9.MAT 示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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

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

為了診斷 GC 問題,建議在 JVM 參數中加上-XX:+PrintGCDateStamps。常用的 GC 參數如圖 10 所示。

圖 10. 常用 GC 參數

Java應用性能調優最強實踐指南(基礎到精通必看

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

七、性能優化實踐

在介紹了一些常用的性能診斷工具後,下面將結合我們在 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 應用也不會造成影響。

從圖 11 可知,調整之後的 Full GC 次數 在 3 月之後明顯減少。

圖 11.Full GC 監控統計

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 2 中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統為 16 核),說明系統出現負載問題,且存在進一步升高趨勢,需要定位具體原因了。

通過 vmstat 命令可以查看 CPU 的上下文切換次數,如圖 3 所示:

圖 3.vmstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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);

}

}

}

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

圖 4.Linux 性能觀測工具

Java應用性能調優最強實踐指南(基礎到精通必看

五、 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,可以找到對應的問題線程棧。

圖 5. 通過 top –H -p 查看運行時間較長 Java 線程

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 6.jstack 查看線程堆棧

Java應用性能調優最強實踐指南(基礎到精通必看

JProfiler

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

Java應用性能調優最強實踐指南(基礎到精通必看

六、 GC 診斷

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

jstat

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

jstat –gcxxx -t pid <interval> <count>,如圖 8 所示。

圖 8.jstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

jmap

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

MAT

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

圖 9.MAT 示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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

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

為了診斷 GC 問題,建議在 JVM 參數中加上-XX:+PrintGCDateStamps。常用的 GC 參數如圖 10 所示。

圖 10. 常用 GC 參數

Java應用性能調優最強實踐指南(基礎到精通必看

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

七、性能優化實踐

在介紹了一些常用的性能診斷工具後,下面將結合我們在 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 應用也不會造成影響。

從圖 11 可知,調整之後的 Full GC 次數 在 3 月之後明顯減少。

圖 11.Full GC 監控統計

Java應用性能調優最強實踐指南(基礎到精通必看

GC 調優對高併發大數據量交互的應用還是很有必要的,尤其是默認 JVM 參數通常不滿足業務需求,需要進行專門調優。GC 日誌的解讀有很多公開的資料,本文不再贅述。

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

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

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

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

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 2 中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統為 16 核),說明系統出現負載問題,且存在進一步升高趨勢,需要定位具體原因了。

通過 vmstat 命令可以查看 CPU 的上下文切換次數,如圖 3 所示:

圖 3.vmstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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);

}

}

}

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

圖 4.Linux 性能觀測工具

Java應用性能調優最強實踐指南(基礎到精通必看

五、 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,可以找到對應的問題線程棧。

圖 5. 通過 top –H -p 查看運行時間較長 Java 線程

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 6.jstack 查看線程堆棧

Java應用性能調優最強實踐指南(基礎到精通必看

JProfiler

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

Java應用性能調優最強實踐指南(基礎到精通必看

六、 GC 診斷

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

jstat

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

jstat –gcxxx -t pid <interval> <count>,如圖 8 所示。

圖 8.jstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

jmap

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

MAT

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

圖 9.MAT 示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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

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

為了診斷 GC 問題,建議在 JVM 參數中加上-XX:+PrintGCDateStamps。常用的 GC 參數如圖 10 所示。

圖 10. 常用 GC 參數

Java應用性能調優最強實踐指南(基礎到精通必看

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

七、性能優化實踐

在介紹了一些常用的性能診斷工具後,下面將結合我們在 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 應用也不會造成影響。

從圖 11 可知,調整之後的 Full GC 次數 在 3 月之後明顯減少。

圖 11.Full GC 監控統計

Java應用性能調優最強實踐指南(基礎到精通必看

GC 調優對高併發大數據量交互的應用還是很有必要的,尤其是默認 JVM 參數通常不滿足業務需求,需要進行專門調優。GC 日誌的解讀有很多公開的資料,本文不再贅述。

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

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

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

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

Java應用性能調優最強實踐指南(基礎到精通必看

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

查閱相關文檔定位這屬於典型的併發使用的場景錯誤 (http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6423457) ,簡要的說就是 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 這種對於數據一致性要求較高的應用,數據庫的性能會直接影響到整個應用的性能。搜狗商業平臺系統作為廣告主的廣告發布和投放平臺,對其物料的實時性和一致性都有極高的要求,我們在關係型數據庫優化方面也積累了一定的經驗。

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

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

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 2 中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統為 16 核),說明系統出現負載問題,且存在進一步升高趨勢,需要定位具體原因了。

通過 vmstat 命令可以查看 CPU 的上下文切換次數,如圖 3 所示:

圖 3.vmstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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);

}

}

}

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

圖 4.Linux 性能觀測工具

Java應用性能調優最強實踐指南(基礎到精通必看

五、 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,可以找到對應的問題線程棧。

圖 5. 通過 top –H -p 查看運行時間較長 Java 線程

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 6.jstack 查看線程堆棧

Java應用性能調優最強實踐指南(基礎到精通必看

JProfiler

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

Java應用性能調優最強實踐指南(基礎到精通必看

六、 GC 診斷

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

jstat

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

jstat –gcxxx -t pid <interval> <count>,如圖 8 所示。

圖 8.jstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

jmap

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

MAT

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

圖 9.MAT 示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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

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

為了診斷 GC 問題,建議在 JVM 參數中加上-XX:+PrintGCDateStamps。常用的 GC 參數如圖 10 所示。

圖 10. 常用 GC 參數

Java應用性能調優最強實踐指南(基礎到精通必看

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

七、性能優化實踐

在介紹了一些常用的性能診斷工具後,下面將結合我們在 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 應用也不會造成影響。

從圖 11 可知,調整之後的 Full GC 次數 在 3 月之後明顯減少。

圖 11.Full GC 監控統計

Java應用性能調優最強實踐指南(基礎到精通必看

GC 調優對高併發大數據量交互的應用還是很有必要的,尤其是默認 JVM 參數通常不滿足業務需求,需要進行專門調優。GC 日誌的解讀有很多公開的資料,本文不再贅述。

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

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

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

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

Java應用性能調優最強實踐指南(基礎到精通必看

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

查閱相關文檔定位這屬於典型的併發使用的場景錯誤 (http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6423457) ,簡要的說就是 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 這種對於數據一致性要求較高的應用,數據庫的性能會直接影響到整個應用的性能。搜狗商業平臺系統作為廣告主的廣告發布和投放平臺,對其物料的實時性和一致性都有極高的要求,我們在關係型數據庫優化方面也積累了一定的經驗。

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

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

Java應用性能調優最強實踐指南(基礎到精通必看

其中,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 等。

八、總結與建議

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

1)基礎性能的調優

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

2)數據庫性能優化

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

3)應用架構優化

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

4)業務層面的優化

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

Java操作系統中央處理器Java虛擬機數據庫跳槽那些事兒SQL硬件

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 2 中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統為 16 核),說明系統出現負載問題,且存在進一步升高趨勢,需要定位具體原因了。

通過 vmstat 命令可以查看 CPU 的上下文切換次數,如圖 3 所示:

圖 3.vmstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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);

}

}

}

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

圖 4.Linux 性能觀測工具

Java應用性能調優最強實踐指南(基礎到精通必看

五、 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,可以找到對應的問題線程棧。

圖 5. 通過 top –H -p 查看運行時間較長 Java 線程

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 6.jstack 查看線程堆棧

Java應用性能調優最強實踐指南(基礎到精通必看

JProfiler

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

Java應用性能調優最強實踐指南(基礎到精通必看

六、 GC 診斷

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

jstat

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

jstat –gcxxx -t pid <interval> <count>,如圖 8 所示。

圖 8.jstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

jmap

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

MAT

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

圖 9.MAT 示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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

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

為了診斷 GC 問題,建議在 JVM 參數中加上-XX:+PrintGCDateStamps。常用的 GC 參數如圖 10 所示。

圖 10. 常用 GC 參數

Java應用性能調優最強實踐指南(基礎到精通必看

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

七、性能優化實踐

在介紹了一些常用的性能診斷工具後,下面將結合我們在 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 應用也不會造成影響。

從圖 11 可知,調整之後的 Full GC 次數 在 3 月之後明顯減少。

圖 11.Full GC 監控統計

Java應用性能調優最強實踐指南(基礎到精通必看

GC 調優對高併發大數據量交互的應用還是很有必要的,尤其是默認 JVM 參數通常不滿足業務需求,需要進行專門調優。GC 日誌的解讀有很多公開的資料,本文不再贅述。

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

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

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

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

Java應用性能調優最強實踐指南(基礎到精通必看

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

查閱相關文檔定位這屬於典型的併發使用的場景錯誤 (http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6423457) ,簡要的說就是 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 這種對於數據一致性要求較高的應用,數據庫的性能會直接影響到整個應用的性能。搜狗商業平臺系統作為廣告主的廣告發布和投放平臺,對其物料的實時性和一致性都有極高的要求,我們在關係型數據庫優化方面也積累了一定的經驗。

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

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

Java應用性能調優最強實踐指南(基礎到精通必看

其中,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 等。

八、總結與建議

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

1)基礎性能的調優

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

2)數據庫性能優化

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

3)應用架構優化

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

4)業務層面的優化

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

Java操作系統中央處理器Java虛擬機數據庫跳槽那些事兒SQL硬件

Java應用性能調優最強實踐指南(基礎到精通必看

"

Java應用性能調優最強實踐指南沒有之一

Java 應用性能調優最強實踐指南

Java應用性能調優最強實踐指南(基礎到精通必看

Java 應用性能調優最強實踐指南

Java 應用性能優化是一個老生常談的話題,典型的性能問題如頁面響應慢、接口超時,服務器負載高、併發數低,數據庫頻繁死鎖等。尤其是在“糙快猛”的互聯網開發模式大行其道的今天,隨著系統訪問量的日益增加和代碼的臃腫,各種性能問題開始紛至沓來。

Java 應用性能的瓶頸點非常多,比如磁盤、內存、網絡 I/O 等系統因素,Java 應用代碼,JVM GC,數據庫,緩存等。筆者根據個人經驗,將 Java 性能優化分為 4 個層級:應用層、數據庫層、框架層、JVM 層,如圖 1 所示。

圖 1.Java 性能優化分層模型

Java應用性能調優最強實踐指南(基礎到精通必看

每層優化難度逐級增加,涉及的知識和解決的問題也會不同。比如應用層需要理解代碼邏輯,通過 Java 線程棧定位有問題代碼行等;數據庫層面需要分析 SQL、定位死鎖等;框架層需要懂源代碼,理解框架機制;JVM 層需要對 GC 的類型和工作機制有深入瞭解,對各種 JVM 參數作用瞭然於胸。

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

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

事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。

一、性能診斷工具

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

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

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

OS 診斷

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

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

通過 top 命令可以查看系統平均負載和 CPU 使用率,圖 2 為通過 top 命令查看某系統的狀態。

圖 2.top 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 2 中 15 分鐘負載已經高達 57.18,1 分鐘負載是 63.66(系統為 16 核),說明系統出現負載問題,且存在進一步升高趨勢,需要定位具體原因了。

通過 vmstat 命令可以查看 CPU 的上下文切換次數,如圖 3 所示:

圖 3.vmstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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);

}

}

}

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

圖 4.Linux 性能觀測工具

Java應用性能調優最強實踐指南(基礎到精通必看

五、 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,可以找到對應的問題線程棧。

圖 5. 通過 top –H -p 查看運行時間較長 Java 線程

Java應用性能調優最強實踐指南(基礎到精通必看

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

圖 6.jstack 查看線程堆棧

Java應用性能調優最強實踐指南(基礎到精通必看

JProfiler

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

Java應用性能調優最強實踐指南(基礎到精通必看

六、 GC 診斷

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

jstat

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

jstat –gcxxx -t pid <interval> <count>,如圖 8 所示。

圖 8.jstat 命令示例

Java應用性能調優最強實踐指南(基礎到精通必看

jmap

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

MAT

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

圖 9.MAT 示例

Java應用性能調優最強實踐指南(基礎到精通必看

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

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

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

為了診斷 GC 問題,建議在 JVM 參數中加上-XX:+PrintGCDateStamps。常用的 GC 參數如圖 10 所示。

圖 10. 常用 GC 參數

Java應用性能調優最強實踐指南(基礎到精通必看

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

七、性能優化實踐

在介紹了一些常用的性能診斷工具後,下面將結合我們在 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 應用也不會造成影響。

從圖 11 可知,調整之後的 Full GC 次數 在 3 月之後明顯減少。

圖 11.Full GC 監控統計

Java應用性能調優最強實踐指南(基礎到精通必看

GC 調優對高併發大數據量交互的應用還是很有必要的,尤其是默認 JVM 參數通常不滿足業務需求,需要進行專門調優。GC 日誌的解讀有很多公開的資料,本文不再贅述。

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

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

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

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

Java應用性能調優最強實踐指南(基礎到精通必看

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

查閱相關文檔定位這屬於典型的併發使用的場景錯誤 (http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6423457) ,簡要的說就是 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 這種對於數據一致性要求較高的應用,數據庫的性能會直接影響到整個應用的性能。搜狗商業平臺系統作為廣告主的廣告發布和投放平臺,對其物料的實時性和一致性都有極高的要求,我們在關係型數據庫優化方面也積累了一定的經驗。

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

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

Java應用性能調優最強實踐指南(基礎到精通必看

其中,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 等。

八、總結與建議

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

1)基礎性能的調優

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

2)數據庫性能優化

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

3)應用架構優化

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

4)業務層面的優化

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

Java操作系統中央處理器Java虛擬機數據庫跳槽那些事兒SQL硬件

Java應用性能調優最強實踐指南(基礎到精通必看

Java應用性能調優最強實踐指南(基礎到精通必看

後臺 回覆 01 麵肥拿走,,,記得 轉發或評論 一下 就好,謝謝支持95後碼農

"

相關推薦

推薦中...