Java 性能優化之最佳實踐

2021-02-23 21CTO

21CTO 導讀:讓 Java 應用程式以最佳性能運行需要開發者的一些努力。本文為確保 Java 以最佳性能運行的方法。

簡介

在本文中,我們將討論一些有助於提高 Java 應用程式性能的方法。

我們將從怎樣定義可量化的性能目標開始,然後我們再用不同的工具來衡量和監控應用程式的性能,以找出瓶頸出現的地方,以便能夠對症下藥。

另外,我們還會介紹一些常見的 Java 代碼級優化以及編程的最佳實踐。最後,我將介紹 JVM和體系結構的特定調優方法,以提高 Java 應用程式的性能。

當然,性能優化是一個很寬很廣的主題,這只是在 JVM 上探索一個起點。

優化目標

在開始提高應用程式性能之前,我們需要定義和理解關於非功能需求方法的關鍵點,這裡有可伸縮性,性能,可用性等。

我總結了典型 Web 應用程式的一些常用性能指標,如下:

1、應用平均響應時間

2、系統必須支持的平均並發用戶數

3、峰值最高負載期間,預期的每秒請求數(QPS)

通過使用不同的負載測試和應用監測工具(APM)解決方案通常用於跟蹤與優化 Java應用程式的性能。我們圍繞不同的應用場景運行負載測試,同時使用 APM 工具監控 CPU、IO、內存堆棧使用情況,此為識別瓶頸之關鍵。

Gatling (https://gatling.io/)

Gatling 就是一款負載測試的最佳工具之一,它對 HTTP 協議有著非常出色的支持,從而使之成為大多數 HTTP 伺服器做負載測試的良好選擇。

Retrace (https://stackify.com/retrace/)

Stackify 的Retrace 提供了一個成熟的 APM 解決方案,能夠幫助確定應用程式基線的好方法,也有很豐富的功能。其關鍵組件之一是代碼分析,可以在不降低應用程式性能的情況下收集運行時信息。

 Retrace 還有用於監視正在運行的應用程式,基於 JVM 的內存、線程和類的小組件。除了應用程式指標外,它還支持監控託管伺服器的 CPU 與 IO 使用的信息。其涵蓋了應用程式性能潛力,另外亦能看到真實情況的使用和負載率。

實際上比寫起文章來要複雜得多,要理解應用程式的當前性能配置也很關鍵。接下來我們就來關注以下內容。

Gatling 的負載試驗

Gatling 是仿真環境腳本是使用 Scala 編寫的,它還提供了一個 GUI 環境,可以記錄場景之數據,由 GUI 創建模擬的 Scala 腳本。運行完仿真模擬環境後,Gatling 會生成有用的,可供分析的 HTML 報告。

一、定義方案

在啟動之前,我們需要定義一個場景,即用戶在使用 Web 應用程式時會發生哪些動作。

在本例中,場景為:「啟動200個用戶,每個用戶生成10,000個請求」。

配置記錄器

我們使用 Gatling 官網上的手冊:Gatling first steps(https://github.com/excilys/gatling/wiki/First-Steps-with-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 -s basic.EmployeeSimulation

對應用程式的 API 進行負載測試有助開發者找到細微的、不容易發現的錯誤,例如資料庫連接池耗盡,在請求發生高負載的同時,因為代碼等問題導至內存洩漏等不必要的內存高使用率等問題。

監控應用程式

開始使用 Rectrace,第一步先在 Stackify 上註冊免費試用版。網址:https://stackify.com/retrace/

接下來,需要將 Spring Boot 應用程式配置為 Linux 服務,接下來再將 Rectrace Proxy 安裝到應用程式所在的伺服器上。詳細連結:https://support.stackify.com/hc/en-us/articles/205419575

啟動 Retrace 代理與Java 應用,接下在 Retrace 的網站儀錶盤中添加 AddApp 連結,填寫好信息好,Retrace 就會很好的監控我們的應用了。

找到應用中最慢的部分

Retrace 能夠自動檢測到應用程式中問題,包括數十種常見框架和依賴項的使用,包括 SQL、MongoDB、Redis、Elasticsearch 等,比如:

1)某個 SQL 語句拖慢了應用嗎?

2)Redis 掛掉了嗎?

3)某個 HTTP Web 伺服器的性能變慢了嗎?

下圖完整的顯示了在指定連續時間中技術棧最慢部分和周邊的數據。

代碼級優化

雖然 APM 提供了負載和應用程式測試能夠幫助我們識別應用程式中的瓶頸。但是最重要還是開發者的技術能力,遵循良好的編碼實踐,才能在進行應用程式監控之前避免很多性能問題。

使用 StringBuilder 進行字符串連接

字符串連接是一種常見的操作,但也是一種低效的操作。簡單的講,在 Java 中使用+=的問題,它會為每個新操作分配一個新的 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的效率要高得多,特別是要考慮到這些基於String的操作頻率。

在我們繼續之前,請注意當前一代JVM確實對字符串操作執行編譯和/或運行時優化(參考:https://docs.oracle.com/javase/specs/jls/se9/html/jls-15.html#jls-15.18.1)。

避免使用遞歸

使用遞歸操作是導至 StackOverFlow Error 的常見現象。如果你非用遞歸不可,那麼使用尾遞歸作為替代方案可能會更好。

先來看一個頭部遞歸(Head-recursive)的例子:

public int factorial(int n) {

    if (n == 0) {

        return 1;

    } else {

        return n * factorial(n - 1);

    }

}

下面重寫為 尾部遞歸 (tail recursive)的例子:

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 已經有編譯器級別的支持來特別優化尾遞歸代碼,已經有議程討論將此類優化加入 Java 的問題。

謹慎使用正則表達式

正則在編程很多場景都很有用,但是用它們太多會付出比較高的性能成本。

因此,開發者要了解各種 JDK 字符串處理方法也很重要,這些方法比使用正則表達式效率更高更快,如 String.replaceAll()或 String.split()。

如果在計算密集型的代碼中使用正則表達式,則可以使用緩存 Pattern 引用,而不用重複編譯。如下代碼:

static final Pattern HEAVY_REGEX = Pattern.compile("(((X)*Y)*Z)*");

另外,使用像Apache Commons Lang(https://commons.apache.org/proper/commons-lang/)這樣的庫也是很好的選擇,特別是對字符串的操作。

避免創建和銷毀太多線程

創建和處理線程是 JVM 出現性能問題的常見原因,因為線程對象的創建和銷毀開銷相對較重。如果應用程式中使用大量線程,那麼可以使用線程池就非常有意義,它允許我們重用這些昂貴的對象。

因此,Java Executor Service 是基於此點的基礎,它提供了一個高級的 API 來定義線程池的語義並與其交互。

Java 7的Fork/Join框架中提供了一些工具,可以嘗試使用所有可用處理器內核來加速並行處理。 為提供有效的並行執行,框架用一個名為ForkJoinPool的線程池來管理工作線程。

JVM 調優

堆大小調整

確定生產系統的 JVM 的正確大小並非一件簡單的工作。我們通過以下問題確定可預測的內存需求:

1、我們計劃將多少個不同的應用程式部署到單個JVM進程,例如,EAR文件,WAR文件,jar文件等的數量?

2、在運行時可能加載多少個Java類,包含多少個第三方API?

3、估算內存緩存所需的佔用空間,例如,由我們的應用程式(加第三方API)加載的內部緩存數據結構,例如來自資料庫的緩存數據,從文件讀取的數據等。

4、估計應用程式要創建的線程數。

如果沒有一些真實的測試,這些數字是很難估算出來的。

了解應用需求的最可靠方法是對應用程式進行實際負載測試並在運行時跟蹤度量。 前面討論的基於Gatling的測試是一個很好的方法。

選擇垃圾收集器

停止全局的垃圾收集周期。過去此種原因是大多數面向用戶的應用程式的響應能力和整體Java性能的巨大問題所在。

現在的 Java 垃圾收集器已經解決了這個問題,並且通過適當的調整和調整,沒有明顯的收集周期。 話雖如此,也需要深入了解整個JVM上的GC,以及應用程式的特定配置文件,能夠更好地實現目標,比如分析器,堆轉儲和詳細的GC日誌等會幫到我們。這些例外都需要在實際負載中捕獲,也就是前面Gatling性能測試的結果。

有關不同垃圾收集器的更多主題,可以查看指南:https://stackify.com/what-is-java-garbage-collection/。

JDBC性能

關係資料庫是Java應用程式中的另一個典型且常見的性能問題。為了獲取完整請求的響應時間,我們需要查看應用程式的每一層,看代碼如何與底層資料庫進行SQL交互。

連接池

從眾所周知的事實開始,Java 中的資料庫連接很昂貴。 連接池機制是解決這個問題的第一步。

向大家推薦是HikariCP JDBC - https://vladmihalcea.com/2014/04/17/the-anatomy-of-connection-pooling/一個非常輕量級(大約130Kb)和快速JDBC連接池框架。

JDBC批處理

我們處理持久性的方式的另一個方面是嘗試儘可能批量操作。批處理讓我們在單個資料庫通訊中發送多個SQL語句。

在驅動程序和資料庫端中性能都很重要。PreparedStatement是批處理的理想選擇,一些資料庫系統(如Oracle)僅支持對預準備語句進行批處理。

Hibernate更靈活,允許我們使用單一配置切換到JDBC批處理。

聲明緩存

接下來,SQL語句緩存是另一種可能提高持久層性能的方法:這是一種鮮為人知的性能優化。

根據底層JDBC驅動程序,你可以在客戶端(驅動程序)或資料庫端(語法樹甚至執行計劃)緩存PreparedStatement 。

橫向擴展和擴容

資料庫複製和分片也是提高吞吐量的最佳實踐,我們應利用這些經過實戰考驗的體系架構來擴展應用程式的持久層。

架構改進

高速緩存

內存的價格會越來越低,但從磁碟或通過網絡檢索數據仍然很昂貴。 緩存是我們不應忽視的應用程式性能的一個重要方面。

在應用程式中引入獨立的緩存系統會增加架構的一部分複雜性,開始時就要充分利用好已有的庫和框架中的緩存功能。

大多數持久性框架都有很好的緩存支持。諸如Spring MVC之類的Web框架,可以利用Spring中的內置緩存支持,以及基於ETag的HTTP級緩存。

於是,選擇完所有懸而未決的成果之後,架設一個獨立的緩存伺服器(如Redis,Ehcache或Memcache),緩存經常訪問應用的內容是開發者的下一步。

為減少資料庫負載,為提升應用程式性能提供重要的支撐。

擴容

無論我們在單個實例上投入多少硬體,在某些時候這都是不夠的。單機擴展具有很大的局限性 ,當系統遇到瓶頸時,不要被成本束縛手腳,該擴容擴容,它有時是處理更大負載的唯一方法。

也許,這一步具有很大的複雜性,它是在某一點達到瓶頸後,擴展應用程式的唯一方法。

在大多數現代框架和庫中,大多數支持是好的並且會越來越好。Spring生態系統有一整套專門用於解決這一特定應用程式架構的,其它大多數框架也都有類似支持。

除了純Java性能之外,在集群或雲架構下進行擴展的另一個優勢是,添加新節點還可以實現冗餘和更好的處理單點故障,從而實現系統整體的更高可用性。

小結

在本文裡,我們一起討論了很多有關提高Java應用性能的各種概念。 我們從負載測試談起,討論基於APM工具的應用程式和伺服器監控,接下來圍繞編寫高性能Java代碼的最佳實踐。

最後,我們還討論了JVM特定的調優技巧,資料庫端性能增強和架構的升級優化,通過這些,有效擴展Java應用程式,讓你不再為 Java 性能而擔心發愁。

作者:喬喬

來源:21CTO社區

相關焦點

  • java必讀書籍_最佳5本Java性能調優書籍–精選,必讀
    鑑於Java具有使用JIT(及時編譯器)本地編譯熱代碼的能力,它幾乎可以與用C和C ++編寫的本地應用程式相提並論,但是可以通過遵循最佳實踐,避免常見的性能陷阱並使用最新工具來完成很多工作和技術。 在本文中,我將介紹有關Java性能的不錯的書,它們不僅會教您測量什麼,如何測量,而且還將解釋這些問題背後的基礎知識和概念。
  • 10種AWS成本優化最佳實踐
    這三個「解決方案」可能是大多數AWS用戶熟悉的AWS成本優化最佳實踐,但不一定是「最佳」實踐。有時,他們無法節省聲稱的成本的一小部分,而許多其他通常被忽視的AWS成本優化最佳實踐可以節省更多。正如我們已經提到的調整大小,調度和保留實例一樣,讓我們從這三個AWS成本優化最佳實踐開始。調整大小的目的是使實例大小與其工作負載相匹配。
  • 程序丨Unity3D性能優化最佳實踐(三):協程
    作者:Ian翻譯:Kelvin Lo / 海龜系列回顧:Unity3D性能優化最佳實踐(一):
  • 搜尋引擎優化最佳實踐
    搜尋引擎優化最佳實踐什麼是 SEO 最佳實踐?
  • Java 處理 Exception 的 9 個最佳實踐!
    本文給出幾個被很多團隊使用的異常處理最佳實踐。1. 在Finally塊中清理資源或者使用try-with-resource語句當使用類似InputStream這種需要使用後關閉的資源時,一個常見的錯誤就是在try塊的最後關閉資源。
  • Java 中處理 Exception 的最佳實踐
    本文給出幾個被很多團隊使用的異常處理最佳實踐。 1. 在Finally塊中清理資源或者使用try-with-resource語句。當使用類似InputStream這種需要使用後關閉的資源時,一個常見的錯誤就是在try塊的最後關閉資源。
  • Java性能優化全攻略
    Refcard的目的是,幫助開發者通過專注於JVM內部,性能調整原則和最佳實踐,以及利用現有監測和故障診斷工具,來提升應用程式在商業環境中的性能。它能以不同的方式定義「optimal performance(最佳性能)」,但基本要素是:Java程序在業務響應時間要求內執行計算任務的能力,程序在高容量下執行業務功能的能力,並具有可靠性高和延遲低的特點。
  • Java中異常處理的9個最佳實踐
    儘管如此,前輩們依然總結了幾個最佳實踐可以遵循,這些實踐被絕大多數的團隊所採用,本文將為你列出9個最常用且最重要的實踐來幫助你提升異常處理的技能。在做任何事的行動之前,知道為什麼做?做了能解決什麼問題?然後才去思考怎麼做!
  • Java處理Exception的9個最佳實踐
    本文給出幾個被很多團隊使用的異常處理最佳實踐。1. 在Finally塊中清理資源或者使用try-with-resource語句當使用類似InputStream這種需要使用後關閉的資源時,一個常見的錯誤就是在try塊的最後關閉資源。
  • Java性能優化的50個細節(珍藏版)
    在JAVA核心API中,有許多應用final的例子,例如java、lang、String,為String類指定final防止了使用者覆蓋length()方法。另外,如果一個類是final的,則該類所有方法都是final的。java編譯器會尋找機會內聯(inline)所有的final方法(這和具體的編譯器實現有關),此舉能夠使性能平均提高50%。
  • Java性能優化的50個細節
    養成良好的編碼習慣非常重要,能夠顯著地提升程序性能。使用單例可以減輕加載的負擔,縮短加載的時間,提高加載的效率,但並不是所有地方都適用於單例,簡單來說,單例主要適用於以下三個方面:第一,控制資源的使用,通過線程同步來控制資源的並發訪問;第三,控制數據共享,在不建立直接關聯的條件下,讓多個不相關的進程或線程之間實現通信。
  • Java 處理 Exception 的 9 個最佳實踐
    譯文:http://www.rowkey.me/blog/2017/09/17/java-exception/在Java中處理異常並不是一個簡單的事情。首先捕獲最具體的異常現在很多IDE都能智能提示這個最佳實踐,當你試圖首先捕獲最籠統的異常時,會提示不能達到的代碼。當有多個catch塊中,按照捕獲順序只有第一個匹配到的catch塊才能執行。因此,如果先捕獲IllegalArgumentException,那麼則無法運行到對NumberFormatException的捕獲。
  • Java 應用性能調優最強實踐指南!
    圍繞 Java 性能優化,有兩種最基本的分析方法:現場分析法和事後分析法。現場分析法通過保留現場,再採用診斷工具分析定位。現場分析對線上影響較大,部分場景(特別是涉及到用戶關鍵的在線業務時)不太合適。事後分析法需要儘可能多收集現場數據,然後立即恢復服務,同時針對收集的現場數據進行事後分析和復現。下面我們從性能診斷工具出發,分享一些案例與實踐。
  • Java 中 9 個處理 Exception 的最佳實踐
    本文給出幾個被很多團隊使用的異常處理最佳實踐。1. 在Finally塊中清理資源或者使用try-with-resource語句當使用類似InputStream這種需要使用後關閉的資源時,一個常見的錯誤就是在try塊的最後關閉資源。
  • Java的最佳實踐
    而 Java 事實上還算是一門不錯的語言,隨著 Java 8 最近的問世,我決定編制一個庫,實踐和工具的清單,匯集 Java 的一些最佳實踐。本文被放到了 Github 上。你可以隨意地提交貢獻,並加入自己的有關 Java 方面的建議和最佳實踐。
  • FinOps:雲成本優化的最佳實踐
    但現實是,許多人都在匆忙中制定了自己的FinOps實踐。「這是一個開放的新概念,我們正在開發最佳實踐,」Landis說。「當你為一個新功能編寫代碼時,現在的服務選擇是如此之快、如此之小,以至於你可以為很多人花很少的錢。但是當你開始在300毫秒內運行數十億個東西時,它很快就會積少成多。」Fuller說。「我認為這將是一場鬥爭。」   Cloudability的聯合創始人J.R.Storment表示同意。該公司生產的FinOps軟體可以為消費雲服務的企業創建價目表。
  • Java 異常處理的 9 個最佳實踐
    不過,有很多最佳實踐的規則,被大部分團隊接受。這裡有 9 大重要的約定,幫助你學習或者改進異常處理。1、在 Finally 清理資源或者使用 Try-With-Resource 特性大部分情況下,在 try 代碼塊中使用資源後需要關閉資源,例如 InputStream 。在這些情況下,一種常見的失誤就是在 try 代碼塊的最後關閉資源。
  • 十個 JDBC 的最佳實踐
    如有好文章投稿,請點擊 → 這裡了解詳情JDBC是Java為多種關係型資料庫提供的統一的訪問接口,以下是我長期使用JDBC總結的十個最佳實踐。使用ConnectionPool(連接池)使用連接池作為最佳實踐幾乎都成了公認的標準。一些框架已經提供了內建的連接池支持, 例如Spring中的Database Connection Pool,如果你的應用部署在JavaEE的應用伺服器中, 例如JBoss,WAS,這些伺服器也會有內建的連接池支持,例如DBCP。
  • 《深入理解Java虛擬機:JVM高級特性與最佳實踐》讀書筆記
    ,實現了「一次編寫,到處運行」的理想2、java技術體系結構  按照功能來劃分包括以下幾個組成部分:Java程序設計語言,各種硬體平臺的java虛擬機,Java API類庫,來自商業機構和開源社區的第三方Java類庫,Class文件格式Java程序設計語言,java虛擬機,Java API類庫統稱為JDK,是用於支持java程序開發的最小環境
  • 9 個 Java 處理 Exception 的最佳實踐!好用~
    本文給出幾個被很多團隊使用的異常處理最佳實踐。1. 在Finally塊中清理資源或者使用try-with-resource語句當使用類似InputStream這種需要使用後關閉的資源時,一個常見的錯誤就是在try塊的最後關閉資源。