讓你月薪飆升的祕籍:Java效能調優的9個實用技巧

AskHarries發表於2018-11-06

現實裡可能沒有完美無缺的程式碼。如果有,那麼,過來,我寫一段程式碼給你看。

Java已經成為了程式語言的驕子。Java 技術具有卓越的通用性、高效性、平臺移植性和安全性,廣泛應用於PC、資料中心、遊戲控制檯、科學超級計算機、行動電話和網際網路,越來越多的企業在資料結構、演算法分析、軟體開發等研究設計時,都選擇以Java語言作為載體。這說明Java語言已經是人們構建軟體系統時主要使用的一種語言。如何讓Java程式執行是一回事,而如何讓它們跑的快又是另外一回事了……

讓你月薪飆升的祕籍:Java效能調優的9個實用技巧

下面我整理了一些Java效能調優的一些技巧,在此和大家淺淺的交流一下。

Java效能優化的重要性:

程式碼優化,一個很重要的課題。可能有些人覺得沒用,一些細小的地方有什麼好修改的,改與不改對於程式碼的執行效率有什麼影響呢?這個問題我是這麼考慮的,就像大海里面的鯨魚一樣,它吃一條小蝦米有用嗎?沒用,但是,吃的小蝦米一多之後,鯨魚就被餵飽了。

程式碼優化也是一樣,如果專案著眼於儘快無BUG上線,那麼此時可以抓大放小,程式碼的細節可以不精打細磨;但是如果有足夠的時間開發、維護程式碼,這時候就必須考慮每個可以優化的細節了,一個一個細小的優化點累積起來,對於程式碼的執行效率絕對是有提升的。

程式碼優化的目標是:

減小程式碼的體積

提高程式碼執行的效率

在我們分享基於Java的效能調優技巧之前,讓我們先討論一下這些通用的效能調優技巧。

通用效能調優的4個實用技巧

1. 在必要之前,先不要優化

這可能是最最重要的效能調優技巧之一。你應該遵循常見的最佳實踐,並嘗試有效地實現你的用例。但這並不意味著在證明它是必要之前,替換任何標準庫或構建複雜的優化。

在大多數情況下,過早的優化佔用了大量的時間,使得程式碼難以讀取和維護。更糟糕的是,這些優化通常不會帶來任何好處,因為你花費了大量時間來優化應用程式的非關鍵部分。

那麼,你如何證明你需要優化某些東西呢?

首先,你需要確定應用程式程式碼的速度,例如,為所有API呼叫指定一個最大響應時間,或者指定在特定時間範圍內匯入的記錄數量。完成之後,你可以度量應用程式的哪些部分太慢而需要改進。當這樣做之後,那麼請繼續看第二個調優技巧。

2. 使用分析器來找到真正的瓶頸

在你遵循第一條建議,並確定你的應用程式的某些部分的確需要改進之後,問自己從哪裡開始?

你可以用兩種方法來解決這個問題:

你可以看一下你的程式碼,從看起來可疑或者你覺得它可能會產生問題的部分開始。

或者使用分析器,獲取程式碼中每個部分的行為和效能的詳細資訊。

至於為什麼應該總是遵循第二種方法。

答案應該很明顯,基於分析器的方法能讓你更好地理解程式碼的效能含義,並允許你關注最關鍵的部分。如果你曾經使用過分析器,你將會驚訝於程式碼的哪些部分造成了效能問題。然而,很多時候,你的第一次猜想會把你引向錯誤的方向。

3. 為整個應用程式建立效能測試套件

這是另一個幫助你避免許多意想不到問題的一般技巧,這些問題通常發生在效能改進部署到生產環境之後。你應該經常定義測試整個應用程式的效能測試套件,並在你完成效能改進之前和之後執行它。

這些額外的測試執行將幫助你識別更改的功能和效能方面的影響,並確保你不會釋出一個弊大於利的更新。如果你的任務執行於應用程式的多個不同部分比如資料庫快取,這一點尤其重要。

4. 首先解決最大的瓶頸問題

在建立了測試套件並使用分析器對應用程式進行分析之後,你就有了一個需要提高效能的問題列表,這很好,但它仍然不能回答你應該從哪裡開始的問題。你可以從那些可以快速搞定的開始,亦或者從最重要的問題開始。

當然前者很誘人,因為這很快就能出結果。有時,可能需要說服其他團隊成員或你的管理層,效能分析是值得的。

但總的來說,我建議首先著手處理最重要的效能問題。這將為你提供最大的效能改進,而且你可能只需要修復這些問題中的幾個就可以解決你的效能需求

在瞭解通用效能調優技巧之後,讓我們再來仔細看看一些特定於Java的調優技巧。

Java效能調優的5個技巧

1. 使用 StringBuilder

幾乎所有Java程式碼中你都應該考慮這個問題。避免使用+號。你可能會認為 StringBuilder 只是個語法糖,比如:

String x = "a" + args.length + "b";

編譯

讓你月薪飆升的祕籍:Java效能調優的9個實用技巧

但是之後你需要根據條件來修改字串,會發生什麼事情呢?

讓你月薪飆升的祕籍:Java效能調優的9個實用技巧

你現在會有第二個 StringBuilder,這個 StringBuilder 本來沒有存在的必要,它會消耗堆記憶體,給 GC 增加負擔。你應該這樣寫:

讓你月薪飆升的祕籍:Java效能調優的9個實用技巧

2. 避免正規表示式

正規表示式相對便宜和方便。但是如果你在 N.O.P.E 分支 ,那很糟糕了。如果你必須在計算機密集的程式碼段中使用正規表示式,至少把 Pattern 的引用快取下來,避免每次都對其重新編譯:

static final Pattern HEAVY_REGEX =

Pattern.compile("(((X)*Y)*Z)*");

但是如果你的正規表示式真的很簡單,就像

String[] parts = ipAddress.split("//.");

然後你真的最好訴諸普通的 char[] 或基於索引的操作。例如下面一段程式碼做了同樣的事情:

讓你月薪飆升的祕籍:Java效能調優的9個實用技巧

這也說明了為什麼你不應該過早進行優化。與 split() 的版本相比,這簡直不可維護。

正規表示式很有用,但需要代價。如果你在 N.O.P.E 分支 ,就必須避免正規表示式的代價。

3. 不要使用 iterator()

這個建議不太適用於常規用例,只適用於 N.O.P.E. 分支,但你也可以用用看。編寫 Java-5 風格的 foreach 迴圈很方便。 你可以完全忽略迴圈內部變數,並編寫:

for (String value : strings) {

// Do something useful here}

然而,每當你執行到迴圈內部時,如果 string 是一個 Iterable,你就要建立一個新的 Iterator 例項。如果你正在使用 ArrayList,這將會在堆上分配一個含 3 個 int 的物件:

private class Itr implements Iterator<E> {

int cursor;

int lastRet = -1;

int expectedModCount = modCount;

// …

相反,你可以編寫以下程式碼——等價迴圈體,並且在棧上僅“浪費”一個 int 值,開銷低:

int size = strings.size();for (int i = 0; i < size; i++) {

String value : strings.get(i);

// Do something useful here}

… 或者,你可以選擇不改變連結串列,在陣列版本上使用同樣的操作:

for (String value : stringArray) {

// Do something useful here}

關鍵點

從可寫性和可讀性以及從 API 設計的角度來看,Iterators、Iterable 和 foreach 迴圈都是非常有用的。但它們在堆上為每次單獨的迭代建立一個小的新例項。 如果你執行這個迭代許多次,又想避免建立這個無用的例項,可以使用基於索引的迭代。

4. 不要呼叫這些方法

一些方法簡單但開銷不小。在N.O.P.E.分支示例中,我們沒有在葉節點上使用這樣的方法,但你可能使用到了。我們假設 JDBC 驅動程式需要耗費大量資源來計算 ResultSet.wasNull() 的值。你可能會用下列程式碼開發 SQL 框架:

if (type == Integer.class) {

result = (T) wasNull(rs,

Integer.valueOf(rs.getInt(index)));

}

// And then…static final <T> T wasNull(ResultSet rs, T value) throws SQLException {

return rs.wasNull() ? null : value;

}

此處邏輯每次都會在你從結果集中獲得一個 int 之後立即呼叫 ResultSet.wasNull()。但getInt() 的約定是:

返回: 列的數目;如果這個值是 SQL NULL,這個值將返回 0。

因此,對上述問題的簡單但可能有效的改進將是:

static final <T extends Number> T wasNull(

ResultSet rs, T value

) throws SQLException {

return (value == null ||

(value.intValue() == 0 && rs.wasNull()))

? null : value;

}

因此,這不需要過多考慮。

關鍵點

不要在演算法的“葉節點”中呼叫開銷昂貴的方法,而是快取該呼叫,或者如果方法規約允許則規避之。

5. 使用基本型別和棧

上面的例子大量使用了泛型。泛型會強制對 byte、short、int 和 long 這些型別進行裝箱 —— 至少在這之前:泛型會在 Java 10 和 Valhalla 專案中實現專業化。不過現在你的程式碼裡並沒實現這種約束,所以你得采取措施:

// Goes to the heapInteger i = 817598;

… 替換為下面這個:

// Stays on the stackint i = 817598;

如果你使用陣列的話,情況不太妙:

// Three heap objects!Integer[] i = { 1337, 424242 };

… 替換成這個:

// One heap http:// object.int[] i = { 1337, 424242 };

關鍵點

當你在深入 N.O.P.E. 分支時,要小心使用裝箱型別。你可能會給 GC 製造很大的壓力,因為它必須一直清理你的爛攤子。

有一個特別有效的辦法對此進行優化,即使用某些基本型別,併為它建立一個巨大的一維陣列,以及相應的定位變數來明確指出編碼後的物件放在陣列的哪個位置。

LGPL 授權的 trove4j 庫實現了基本資料型別的集合,它看起來比 int[] 要好些。

總結

正如你所看到的,提高應用程式的效能有時不需要做大量的工作。這篇文章中的大多數建議,其實只需要稍微的努力就可以將它們應用到程式碼中。

讓你月薪飆升的祕籍:Java效能調優的9個實用技巧


相關文章