讓你月薪飆升的祕籍:Java效能調優的9個實用技巧
現實裡可能沒有完美無缺的程式碼。如果有,那麼,過來,我寫一段程式碼給你看。
Java已經成為了程式語言的驕子。Java 技術具有卓越的通用性、高效性、平臺移植性和安全性,廣泛應用於PC、資料中心、遊戲控制檯、科學超級計算機、行動電話和網際網路,越來越多的企業在資料結構、演算法分析、軟體開發等研究設計時,都選擇以Java語言作為載體。這說明Java語言已經是人們構建軟體系統時主要使用的一種語言。如何讓Java程式執行是一回事,而如何讓它們跑的快又是另外一回事了......
下面我整理了一些Java效能調優的一些技巧,在此和大家淺淺的交流一下。
Java效能優化的重要性:
程式碼優化,一個很重要的課題。可能有些人覺得沒用,一些細小的地方有什麼好修改的,改與不改對於程式碼的執行效率有什麼影響呢?這個問題我是這麼考慮的,就像大海里面的鯨魚一樣,它吃一條小蝦米有用嗎?沒用,但是,吃的小蝦米一多之後,鯨魚就被餵飽了。
程式碼優化也是一樣,如果專案著眼於儘快無BUG上線,那麼此時可以抓大放小,程式碼的細節可以不精打細磨;但是如果有足夠的時間開發、維護程式碼,這時候就必須考慮每個可以優化的細節了,一個一個細小的優化點累積起來,對於程式碼的執行效率絕對是有提升的。
程式碼優化的目標是:
減小程式碼的體積
提高程式碼執行的效率
在我們分享基於Java的效能調優技巧之前,讓我們先討論一下這些通用的效能調優技巧。
通用效能調優的4個實用技巧
1. 在必要之前,先不要優化
這可能是最最重要的效能調優技巧之一。你應該遵循常見的最佳實踐,並嘗試有效地實現你的用例。但這並不意味著在證明它是必要之前,替換任何標準庫或構建複雜的優化。
在大多數情況下,過早的優化佔用了大量的時間,使得程式碼難以讀取和維護。更糟糕的是,這些優化通常不會帶來任何好處,因為你花費了大量時間來優化應用程式的非關鍵部分。
那麼,你如何證明你需要優化某些東西呢?
首先,你需要確定應用程式程式碼的速度,例如,為所有API呼叫指定一個最大響應時間,或者指定在特定時間範圍內匯入的記錄數量。完成之後,你可以度量應用程式的哪些部分太慢而需要改進。當這樣做之後,那麼請繼續看第二個調優技巧。
2. 使用分析器來找到真正的瓶頸
在你遵循第一條建議,並確定你的應用程式的某些部分的確需要改進之後,問自己從哪裡開始?
你可以用兩種方法來解決這個問題:
你可以看一下你的程式碼,從看起來可疑或者你覺得它可能會產生問題的部分開始。
或者使用分析器,獲取程式碼中每個部分的行為和效能的詳細資訊。
至於為什麼應該總是遵循第二種方法。
答案應該很明顯,基於分析器的方法能讓你更好地理解程式碼的效能含義,並允許你關注最關鍵的部分。如果你曾經使用過分析器,你將會驚訝於程式碼的哪些部分造成了效能問題。然而,很多時候,你的第一次猜想會把你引向錯誤的方向。
3. 為整個應用程式建立效能測試套件
這是另一個幫助你避免許多意想不到問題的一般技巧,這些問題通常發生在效能改進部署到生產環境之後。你應該經常定義測試整個應用程式的效能測試套件,並在你完成效能改進之前和之後執行它。
這些額外的測試執行將幫助你識別更改的功能和效能方面的影響,並確保你不會發佈一個弊大於利的更新。如果你的任務運行於應用程式的多個不同部分比如資料庫或快取,這一點尤其重要。
4. 首先解決最大的瓶頸問題
在建立了測試套件並使用分析器對應用程式進行分析之後,你就有了一個需要提高效能的問題列表,這很好,但它仍然不能回答你應該從哪裡開始的問題。你可以從那些可以快速搞定的開始,亦或者從最重要的問題開始。
當然前者很誘人,因為這很快就能出結果。有時,可能需要說服其他團隊成員或你的管理層,效能分析是值得的。
但總的來說,我建議首先著手處理最重要的效能問題。這將為你提供最大的效能改進,而且你可能只需要修復這些問題中的幾個就可以解決你的效能需求。
在瞭解通用效能調優技巧之後,讓我們再來仔細看看一些特定於Java的調優技巧。
Java效能調優的5個技巧
1. 使用 StringBuilder
幾乎所有Java程式碼中你都應該考慮這個問題。避免使用+號。你可能會認為 StringBuilder 只是個語法糖,比如:
String x = "a" + args.length + "b";
會編譯成
但是之後你需要根據條件來修改字串,會發生什麼事情呢?
你現在會有第二個 StringBuilder,這個 StringBuilder 本來沒有存在的必要,它會消耗堆記憶體,給 GC 增加負擔。你應該這樣寫:
2. 避免正則表示式
正則表示式相對便宜和方便。但是如果你在 N.O.P.E 分支 ,那很糟糕了。如果你必須在計算機密集的程式碼段中使用正則表示式,至少把 Pattern 的引用快取下來,避免每次都對其重新編譯:
static final Pattern HEAVY_REGEX =
Pattern.compile("(((X)*Y)*Z)*");
但是如果你的正則表示式真的很簡單,就像
String[] parts = ipAddress.split("\\.");
然後你真的最好訴諸普通的 char[] 或基於索引的操作。例如下面一段程式碼做了同樣的事情:
這也說明了為什麼你不應該過早進行優化。與 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[] 要好些。
總結:
正如你所看到的,提高應用程式的效能有時不需要做大量的工作。這篇文章中的大多數建議,其實只需要稍微的努力就可以將它們應用到程式碼中。
但通常最重要的建議是很程式語言無關的:
在你知道有必要之前,不要優化
使用分析器來找到真正的瓶頸
首先解決最大的瓶頸問題
歡迎加入 51軟體測試大家庭,在這裡你將獲得【最新行業資訊】,【免費測試工具安裝包】,【軟體測試技術乾貨】,【面試求職技巧】... 51與你共同學習,一起成長!期待你的加入: QQ 群: 755431660