高性能Java代碼的最佳實踐
前言正則表達式
在這篇文章中,咱們將討論幾個有助於提高Java應用程序性能的方法。咱們首先將介紹如何定義可度量的性能指標,而後看看有哪些工具能夠用來度量和監控應用程序性能,以及肯定性能瓶頸。數據庫
咱們還將看到一些常見的Java代碼優化方法以及最佳編碼實踐。最後,咱們將看看用於提高Java應用程序性能的JVM調優技巧和架構調整。緩存
請注意,性能優化是一個很寬泛的話題,而本文只是對JVM探索的一個起點。性能優化
性能指標服務器
在開始優化應用程序的性能以前,咱們須要理解諸如可擴展性、性能、可用性等方面的非功能需求。網絡
如下是典型Web應用程序經常使用的一些性能指標:數據結構
1.應用程序平均響應時間架構
2.系統必須支持的平均併發用戶數併發
3.在負載高峯期間,預期的每秒請求數app
這些指標能夠經過使用多種監視工具監測到,它們對分析性能瓶頸和性能調優有着很是大的做用。
示例應用程序
咱們將使用一個簡單的Spring Boot Web應用程序做爲示例,在這篇文章中有相關的介紹。這個應用程序可用於管理員工列表,並對外公開了添加和檢索員工的REST API。
咱們將使用這個程序做爲參考來運行負載測試,並在接下來的章節中監控各類應用指標。
找出性能瓶頸
負載測試工具和應用程序性能管理(APM)解決方案經常使用於跟蹤和優化Java應用程序的性能。要找出性能瓶頸,主要就是對各類應用場景進行負載測試,並同時使用APM工具對CPU、IO、堆的使用狀況進行監控等等。
Gatling是進行負載測試最好的工具之一,它提供了對HTTP協議的支持,是HTTP服務器負載測試的絕佳選擇。
Stackify的Retrace是一個成熟的APM解決方案。它的功能很豐富,對肯定應用程序的性能基線頗有幫助。 Retrace的關鍵組件之一是它的代碼分析功能,它可以在不減慢應用程序的狀況下收集運行時信息。
Retrace還提供了監視基於JVM應用程序的內存、線程和類的小部件。除了應用程序自己的指標以外,它還支持監視託管應用程序的服務器的CPU和IO使用狀況。
所以,像Retrace這樣功能全面的監控工具是解鎖應用程序性能潛力的第一步。而第二步則是在你的系統上重現真實使用場景和負載。
提及來容易,作起來難,並且瞭解應用程序當前的性能也很是重要。這就是咱們接下來要關注的問題。
Gatling負載測試
Gatling的模擬測試腳本是用Scala編寫的,但該工具還附帶了一個很是有用的圖形界面,可用於記錄具體的場景,並生成Scala腳本。
在運行模擬腳本以後,Gatling會生成一份很是有用的、可用於分析的HTML報告。
定義場景
在啓動記錄器以前,咱們須要定義一個場景,表示用戶在瀏覽Web應用時發生的事情。
在咱們的這個例子中,具體的場景將是「啓動200個用戶,每一個用戶發出一萬個請求。」
配置記錄器
根據「Gatling的第一步」所述,用下面的代碼建立一個名爲EmployeeSimulation的scala文件:
class EmployeeSimulation extends Simulation { val scn = scenario("FetchEmployees").repeat(10000) { exec( http("GetEmployees-API") .get("http://localhost:8080/employees") .check(status.is(200)) ) } setUp(scn.users(200).ramp(100)) }
運行負載測試
要執行負載測試,請運行如下命令:
$GATLING_HOME/bin/gatling.sh-sbasic.EmployeeSimulation
對應用程序的API進行負載測試有助於發現及其細微的而且難以發現的錯誤,如數據庫鏈接耗盡、高負載狀況下的請求超時、由於內存泄漏而致使堆的高使用率等等。
監控應用程序
要使用Retrace進行Java應用程序的開發,首先須要在Stackify上申請免費試用帳號。而後,將咱們本身的Spring Boot應用程序配置爲Linux服務。咱們還須要在託管應用程序的服務器上安裝Retrace代理,按照這篇文章所述的操做便可。
Retrace代理和要監控的Java應用程序啓動後,咱們就能夠到Retrace儀表板上單擊AddApp按鈕添加應用了。添加應用完成以後,Retrace將開始監控應用程序了。
找到最慢的那個點
Retrace會自動監控應用程序,並跟蹤數十種常見框架及其依賴關係的使用狀況,包括SQL、MongoDB、Redis、Elasticsearch等等。Retrace能幫助咱們快速肯定應用程序爲何會出現以下性能問題:
某個SQL語句是否會拖慢系統的速度?
Redis忽然變慢了嗎?
特定的HTTP Web服務宕了,仍是變慢了?
例如,下面的圖形展現了在一段給定的時間內速度最慢的組件。
代碼級別的優化
負載測試和應用程序監控對於肯定應用程序的一些關鍵性能瓶頸很是有用。但同時,咱們須要遵循良好的編碼習慣,以免在對應用程序進行監控的時候出現過多的性能問題。
在下一章節中,咱們未來看一些最佳實踐。
使用StringBuilder來鏈接字符串
字符串鏈接是一個很是常見的操做,也是一個低效率的操做。簡單地說,使用+=來追加字符串的問題在於每次操做都會分配新的String。
下面這個例子是一個簡化了的但卻很典型的循環。前面使用了原始的鏈接方式,後面使用了構建器:
public String stringAppendLoop() { String s = ""; for (int i = 0; i < 10000; i++) { if (s.length() > 0) s += ", "; s += "bar"; } return s; } public String stringAppendBuilderLoop() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { if (sb.length() > 0) sb.append(", "); sb.append("bar"); } return sb.toString(); }
上面代碼中使用的StringBuilder對性能的提高很是有效。請注意,現代的JVM會在編譯或者運行時對字符串操做進行優化。
避免遞歸
致使出現StackOverFlowError錯誤的遞歸代碼邏輯是Java應用程序中另外一種常見的問題。若是沒法去掉遞歸邏輯,那麼尾遞歸做爲替代方案將會更好。
咱們來看一個頭遞歸的例子:
public int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); } }
如今咱們把它重寫爲尾遞歸:
private int factorial(int n, int accum) { if (n == 0) { return accum; } else { return factorial(n - 1, accum * n); } } public int factorial(int n) { return factorial(n, 1); }
其餘JVM語言(如Scala)已經在編譯器級支持尾遞歸代碼的優化,固然,對於這種優化目前也存在着一些爭議。
謹慎使用正則表達式
正則表達式在不少場景中都很是有用,但它們每每具備很是高的性能成本。瞭解各類使用正則表達式的JDK字符串方法很重要,例如String.replaceAll()、String.split()。
若是你不得不在計算密集的代碼段中使用正則表達式,那麼須要緩存Pattern的引用而避免重複編譯:
static final Pattern HEAVY_REGEX = Pattern.compile("(((X)Y)Z)*");
使用一些流行的庫,好比Apache Commons Lang也是一個很好的選擇,特別是在字符串的操做方面。
避免建立和銷燬過多的線程
線程的建立和處置是JVM出現性能問題的常見緣由,由於線程對象的建立和銷燬相對較重。
若是應用程序使用了大量的線程,那麼使用線程池會更加有用,由於線程池容許這些昂貴的對象被重用。
爲此,Java的ExecutorService是線程池的基礎,它提供了一個高級API來定義線程池的語義並與之進行交互。
Java 7中的Fork/Join框架也值得提一下,由於它提供了一些工具來嘗試使用全部可用的處理器核心以幫助加速並行處理。爲了提升並行執行效率,框架使用了一個名爲ForkJoinPool的線程池來管理工做線程。
JVM調優
堆大小的調優
爲生產系統肯定合適的JVM堆大小並非一件簡單的事情。要作的第一步是回答如下問題以預測內存需求:
計劃要把多少個不一樣的應用程序部署到單個JVM進程中,例如EAR文件、WAR文件、jar文件的數量是多少?
在運行時可能會加載多少個Java類,包括第三方API的類?
估計內存緩存所需的空間,例如,由應用程序(和第三方API)加載的內部緩存數據結構,好比從數據庫緩存的數據、從文件中讀取的數據等等。
估計應用程序將建立的線程數。
若是沒有通過真實場景的測試,這些數字很難估計。
要得到有關應用程序需求的最好最可靠的方法是對應用程序執行實際的負載測試,並在運行時跟蹤性能指標。咱們以前討論的基於Gatling的測試就是一個很好的方法。
選擇合適的垃圾收集器
Stop-the-world(STW)垃圾收集的週期是影響大多數面向客戶端應用程序響應和總體Java性能的大問題。可是,目前的垃圾收集器大多解決了這個問題,而且經過適當的優化和大小的調整,可以消除對收集週期的感知。
分析器、堆轉儲和詳細的GC日誌記錄工具對此有必定的幫助做用。再一次注意,這些都須要在真實場景的負載模式下進行監控。
有關不一樣垃圾收集器的更多信息,請查看這個指南。
JDBC性能
關係型數據庫是Java應用程序中另外一個常見的性能問題。爲了得到完整請求的響應時間,咱們很天然地必須查看應用程序的每一層,並思考如何讓代碼與底層SQL DB進行交互。
鏈接池
讓咱們從衆所周知的事實開始,即數據庫鏈接是昂貴的。 鏈接池機制是解決這個問題很是重要的第一步。
這裏建議使用HikariCP JDBC,這是一個很是輕量級(大約130Kb)而且速度極快的JDBC鏈接池框架。
JDBC批處理
持久化處理應儘量地執行批量操做。 JDBC批處理容許咱們在單次數據庫交互中發送多個SQL語句。
這樣,不管是在驅動端仍是在數據庫端,性能均可能獲得顯著地提高。 PreparedStatement是一個很是棒的的批處理命令,一些數據庫系統(例如Oracle)只支持預處理語句的批處理。
另外一方面,Hibernate則更加靈活,它容許咱們只需修改一個配置便可快速切換爲批處理操做。
語句緩存
語句緩存是另外一種提升持久層性能的方法,這是一種不爲人知但又容易掌握的性能優化方法。
只要底層的JDBC驅動程序支持,你就能夠在客戶端(驅動程序)或數據庫端(語法樹甚至執行計劃)中緩存PreparedStatement。
規模的縮放
數據庫複製和分片是提升吞吐量很是好的方法,咱們應該充分利用這些通過實踐檢驗的架構模式,以擴展企業應用的持久層。
架構改進
緩存
如今內存的價格很低,並且愈來愈低,從磁盤或經過網絡來檢索數據的性能代價仍然很高。緩存天然而然的變成了在應用程序性能方面不能忽視的關鍵。
固然,在應用的拓撲結構中引入一個獨立的緩存系統確實會增長架構的複雜度,因此,應當充分利用當前使用的庫和框架現有的緩存功能。
例如,大多數的持久化框架都支持緩存。 Spring MVC等Web框架還可使用Spring中內置的緩存支持,以及基於ETags的強大的HTTP級緩存。
橫向擴展
不管咱們在單個實例中準備了多少硬件,都會有不夠用的時候。簡而言之,擴展有着天生的侷限性,當系統遇到這些問題時,橫向擴展是處理更多負載的惟一途徑。這一步確定會至關的複雜,但倒是擴展應用的惟一辦法。
對大多數的現代框架和庫來講,這方面仍是支持得很好的,並且會變得愈來愈好。 Spring生態系統有一個完整的項目集,專門用於解決這個特定的應用程序架構領域,其餘大多數的框架也都有相似的支持。
除了可以提高Java的性能,經過集羣進行橫向擴展也有其餘的好處,添加新的節點能產生冗餘,並更好的處理故障,從而提升整個系統的可用性。
結論
在這篇文章中,咱們圍繞着提高Java應用的性能探討了許多概念。咱們首先介紹了負載測試、基於APM工具的應用程序和服務器監控,隨後介紹了編寫高性能Java代碼的一些最佳實踐。最後,咱們研究了JVM特定的調優技巧、數據庫端的優化和架構方面的調整。
感謝您耐心看完的文章
順便給你們推薦一個Java技術交流羣:710373545裏面會分享一些資深架構師錄製的視頻資料:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多!