一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - Java教程 - 10種簡單的Java性能優化

10種簡單的Java性能優化

2021-02-24 14:30importnew Java教程

你是否正打算優化hashCode()方法?是否想要繞開正則表達式?Lukas Eder介紹了很多簡單方便的性能優化小貼士以及擴展程序性能的技巧

最近“全網域(Web Scale)”一詞被炒得火熱,人們也正在通過擴展他們的應用程序架構來使他們的系統變得更加“全網域”。但是究竟什么是全網域?或者說如何確保全網域?

擴展的不同方面

 

全網域被炒作的最多的是擴展負載(Scaling load),比如支持單個用戶訪問的系統也可以支持10 個、100個、甚至100萬個用戶訪問。在理想情況下,我們的系統應該保持盡可能的“無狀態化(stateless)”。即使必須存在狀態,也可以在網絡的不同處理終端上轉化并進行傳輸。當負載成為瓶頸時候,可能就不會出現延遲。所以對于單個請求來說,耗費50到100毫秒也是可以接受的。這就是所謂的橫向擴展(Scaling out)。

擴展在全網域優化中的表現則完全不同,比如確保成功處理一條數據的算法也可成功處理10條、100條甚至100萬條數據。無論這種度量類型是是否可行,事件復雜度(大O符號)是最佳描述。延遲是性能擴展殺手。你會想盡辦法將所有的運算處理在同一臺機器上進行。這就是所謂的縱向擴展(Scaling up)。

如果天上能掉餡餅的話(當然這是不可能的),我們或許能把橫向擴展和縱向擴展組合起來。但是,今天我們只打算介紹下面幾條提升效率的簡單方法。

大O符號

 

Java 7的 ForkJoinPool 和Java8 的并行數據流(parallel Stream) 都對并行處理有所幫助。當在多核處理器上部署Java程序時表現尤為明顯,因所有的處理器都可以訪問相同的內存。

所以,這種并行處理較之在跨網絡的不同機器上進行擴展,根本的好處是幾乎可以完全消除延遲。

但不要被并行處理的效果所迷惑!請謹記下面兩點:

  • 并行處理會吃光處理器資源。并行處理為批處理帶來了極大的好處,但同時也是非同步服務器(如HTTP)的噩夢。有很多原因可以解釋,為什么在過去的幾十年中我們一直在使用單線程的Servlet模型。并行處理僅在縱向擴展時才能帶來實際的好處。
  • 并行處理對算法復雜度沒有影響。如果你的算法的時間復雜度為 O(nlogn),讓算法在 c 個處理器上運行,事件復雜度仍然為 O(nlogn/c), 因為 c 只是算法中的一個無關緊要的常量。你節省的僅僅是時鐘時間(wall-clock time),實際的算法復雜度并沒有降低。

降低算法復雜度毫無疑問是改善性能最行之有效的辦法。比如對于一個 HashMap 實例的 lookup() 方法來說,事件復雜度 O(1) 或者空間復雜度 O(1) 是最快的。但這種情況往往是不可能的,更別提輕易地實現。

如果你不能降低算法的復雜度,也可以通過找到算法中的關鍵點并加以改善的方法,來起到改善性能的作用。假設我們有下面這樣的算法示意圖:

10種簡單的Java性能優化

該算法的整體時間復雜度為 O(N3),如果按照單獨訪問順序計算也可得出復雜度為 O(N x O x P)。但是不管怎樣,在我們分析這段代碼時會發現一些奇怪的場景:

  • 在開發環境中,通過測試數據可以看到:左分支(N->M->Heavy operation)的時間復雜度 M 的值要大于右邊的 O 和 P,所以在我們的分析器中僅僅看到了左分支。
  • 在生產環境中,你的維護團隊可能會通過 AppDynamicsDynaTrace 或其它小工具發現,真正導致問題的罪魁禍首是右分支(N -> O -> P -> Easy operation or also N.O.P.E.)。

在沒有生產數據參照的情況下,我們可能會輕易的得出要優化“高開銷操作”的結論。但我們做出的優化對交付的產品沒有起到任何效果。

優化的金科玉律不外乎以下內容:

  • 良好的設計將會使優化變得更加容易。
  • 過早的優化并不能解決多有的性能問題,但是不良的設計將會導致優化難度的增加。

理論就先談到這里。假設我們已經發現了問題出現在了右分支上,很有可能是因產品中的簡單處理因耗費了大量的時間而失去響應(假設N、O和 P 的值非常大), 請注意文章中提及的左分支的時間復雜度為 O(N3)。這里所做出的努力并不能擴展,但可以為用戶節省時間,將困難的性能改善推遲到后面再進行。

這里有10條改善Java性能的小建議:

1、使用StringBuilder

 

StingBuilder 應該是在我們的Java代碼中默認使用的,應該避免使用 + 操作符。或許你會對 StringBuilder 的語法糖(syntax sugar)持有不同意見,比如:

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

將會被編譯為:

?
1
2
3
4
5
6
7
8
9
10
11
0  new java.lang.StringBuilder [16]
 3  dup
 4  ldc <String "a"> [18]
 6  invokespecial java.lang.StringBuilder(java.lang.String) [20]
 9  aload_0 [args]
10  arraylength
11  invokevirtual java.lang.StringBuilder.append(int) : java.lang.StringBuilder [23]
14  ldc <String "b"> [27]
16  invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [29]
19  invokevirtual java.lang.StringBuilder.toString() : java.lang.String [32]
22  astore_1 [x]

但究竟發生了什么?接下來是否需要用下面的部分來對 String 進行改善呢?

?
1
2
3
4
String x = "a" + args.length + "b";
 
if (args.length == 1)
    x = x + args[0];

現在使用到了第二個 StringBuilder,這個 StringBuilder 不會消耗堆中額外的內存,但卻給 GC 帶來了壓力。

?
1
2
3
4
5
6
StringBuilder x = new StringBuilder("a");
x.append(args.length);
x.append("b");
 
if (args.length == 1);
    x.append(args[0]);

小結

在上面的樣例中,如果你是依靠Java編譯器來隱式生成實例的話,那么編譯的效果幾乎和是否使用了 StringBuilder 實例毫無關系。請記住:在  N.O.P.E 分支中,每次CPU的循環的時間到白白的耗費在GC或者為 StringBuilder 分配默認空間上了,我們是在浪費 N x O x P 時間。

一般來說,使用 StringBuilder 的效果要優于使用 + 操作符。如果可能的話請在需要跨多個方法傳遞引用的情況下選擇 StringBuilder,因為 String 要消耗額外的資源。JOOQ在生成復雜的SQL語句便使用了這樣的方式。在整個抽象語法樹AST Abstract Syntax Tree)SQL傳遞過程中僅使用了一個 StringBuilder 。

更加悲劇的是,如果你仍在使用 StringBuffer 的話,那么用 StringBuilder 代替 StringBuffer 吧,畢竟需要同步字符串的情況真的不多。

2、避免使用正則表達式

 

正則表達式給人的印象是快捷簡便。但是在 N.O.P.E 分支中使用正則表達式將是最糟糕的決定。如果萬不得已非要在計算密集型代碼中使用正則表達式的話,至少要將 Pattern 緩存下來,避免反復編譯Pattern。

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

如果僅使用到了如下這樣簡單的正則表達式的話:

?
1
String[] parts = ipAddress.split("\\.");

這是最好還是用普通的 char[] 數組或者是基于索引的操作。比如下面這段可讀性比較差的代碼其實起到了相同的作用。

?
1
2
3
4
5
6
7
8
9
10
11
12
int length = ipAddress.length();
int offset = 0;
int part = 0;
for (int i = 0; i < length; i++) {
    if (i == length - 1 ||
            ipAddress.charAt(i + 1) == '.') {
        parts[part] =
            ipAddress.substring(offset, i + 1);
        part++;
        offset = i + 2;
    }
}

上面的代碼同時表明了過早的優化是沒有意義的。雖然與 split() 方法相比較,這段代碼的可維護性比較差。

挑戰:聰明的小伙伴能想出更快的算法嗎?

小結

正則表達式是十分有用,但是在使用時也要付出代價。尤其是在 N.O.P.E 分支深處時,要不惜一切代碼避免使用正則表達式。還要小心各種使用到正則表達式的JDK字符串方法,比如 String.replaceAll() 或 String.split()。可以選擇用比較流行的開發庫,比如 Apache Commons Lang 來進行字符串操作。

3、不要使用iterator()方法

 

這條建議不適用于一般的場合,僅適用于在 N.O.P.E 分支深處的場景。盡管如此也應該有所了解。Java 5格式的循環寫法非常的方便,以至于我們可以忘記內部的循環方法,比如:

?
1
2
3
for (String value : strings) {
 // Do something useful here
}

當每次代碼運行到這個循環時,如果 strings 變量是一個 Iterable 的話,代碼將會自動創建一個Iterator 的實例。如果使用的是 ArrayList 的話,虛擬機會自動在堆上為對象分配3個整數類型大小的內存。

?
1
2
3
4
5
private class Itr implements Iterator<E> {
 int cursor;
 int lastRet = -1;
 int expectedModCount = modCount;
 // ...

也可以用下面等價的循環方式來替代上面的 for 循環,僅僅是在棧上“浪費”了區區一個整形,相當劃算。

?
1
2
3
4
5
int size = strings.size();
for (int i = 0; i < size; i++) {
 String value : strings.get(i);
 // Do something useful here
}

如果循環中字符串的值是不怎么變化,也可用數組來實現循環。

?
1
2
3
for (String value : stringArray) {
 // Do something useful here
}

小結

無論是從易讀寫的角度來說,還是從API設計的角度來說迭代器、Iterable接口和 foreach 循環都是非常好用的。但代價是,使用它們時是會額外在堆上為每個循環子創建一個對象。如果循環要執行很多很多遍,請注意避免生成無意義的實例,最好用基本的指針循環方式來代替上述迭代器、Iterable接口和 foreach 循環。

討論

一些與上述內容持反對意見的看法(尤其是用指針操作替代迭代器)詳見Reddit上的討論

4、不要調用高開銷方法

 

有些方法的開銷很大。以 N.O.P.E 分支為例,我們沒有提到葉子的相關方法,不過這個可以有。假設我們的JDBC驅動需要排除萬難去計算 ResultSet.wasNull() 方法的返回值。我們自己實現的SQL框架可能像下面這樣:

?
1
2
3
4
5
6
7
8
9
10
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。

所以一個簡單有效的改善方法如下:

?
1
2
3
4
5
6
7
8
static final <T extends Number> T wasNull(
    ResultSet rs, T value
)
throws SQLException {
    return (value == null ||
           (value.intValue() == 0 && rs.wasNull()))
        ? null : value;
}

這是輕而易舉的事情。

小結

將方法調用緩存起來替代在葉子節點的高開銷方法,或者在方法約定允許的情況下避免調用高開銷方法。

5、使用原始類型和棧

 

上面介紹了來自 至少泛型在Java 10或者Valhalla項目中被專門化之前,不應該成為代碼的限制。因為可以通過下面的方法來進行替換:

?
1
2
//存儲在堆上
Integer i = 817598;

……如果這樣寫的話:

?
1
2
// 存儲在棧上
int i = 817598;

在使用數組時情況可能會變得更加糟糕:

?
1
2
//在堆上生成了三個對象
Integer[] i = { 1337, 424242 };

……如果這樣寫的話:

?
1
2
// 僅在堆上生成了一個對象
int[] i = { 1337, 424242 };

小結

當我們處于 N.O.P.E. 分支的深處時,應該極力避免使用包裝類。這樣做的壞處是給GC帶來了很大的壓力。GC將會為清除包裝類生成的對象而忙得不可開交。

所以一個有效的優化方法是使用基本數據類型、定長數組,并用一系列分割變量來標識對象在數組中所處的位置。

遵循LGPL協議的 trove4j 是一個Java集合類庫,它為我們提供了優于整形數組 int[] 更好的性能實現。

例外

下面的情況對這條規則例外:因為 boolean 和 byte 類型不足以讓JDK為其提供緩存方法。我們可以這樣寫:

?
1
2
3
4
5
Boolean a1 = true; // ... syntax sugar for:
Boolean a2 = Boolean.valueOf(true);
 
Byte b1 = (byte) 123; // ... syntax sugar for:
Byte b2 = Byte.valueOf((byte) 123);

其它整數基本類型也有類似情況,比如 char、short、int、long。

不要在調用構造方法時將這些整型基本類型自動裝箱或者調用 TheType.valueOf() 方法。

也不要在包裝類上調用構造方法,除非你想得到一個不在堆上創建的實例。這樣做的好處是為你為同事獻上一個巨坑的愚人節笑話

非堆存儲

當然了,如果你還想體驗下堆外函數庫的話,盡管這可能參雜著不少戰略決策,而并非最樂觀的本地方案。一篇由Peter Lawrey和 Ben Cotton撰寫的關于非堆存儲的很有意思文章請點擊: OpenJDK與HashMap——讓老手安全地掌握(非堆存儲!)新技巧

6、避免遞歸

 

現在,類似Scala這樣的函數式編程語言都鼓勵使用遞歸。因為遞歸通常意味著能分解到單獨個體優化的尾遞歸(tail-recursing)。如果你使用的編程語言能夠支持那是再好不過。不過即使如此,也要注意對算法的細微調整將會使尾遞歸變為普通遞歸。

希望編譯器能自動探測到這一點,否則本來我們將為只需使用幾個本地變量就能搞定的事情而白白浪費大量的堆棧框架(stack frames)。

小結

這節中沒什么好說的,除了在 N.O.P.E 分支盡量使用迭代來代替遞歸。

7、使用entrySet()

 

當我們想遍歷一個用鍵值對形式保存的 Map 時,必須要為下面的代碼找到一個很好的理由:

?
1
2
3
for (K key : map.keySet()) {
    V value : map.get(key);
}

更不用說下面的寫法:

?
1
2
3
4
for (Entry<K, V> entry : map.entrySet()) {
    K key = entry.getKey();
    V value = entry.getValue();
}

在我們使用 N.O.P.E. 分支應該慎用map。因為很多看似時間復雜度為 O(1) 的訪問操作其實是由一系列的操作組成的。而且訪問本身也不是免費的。至少,如果不得不使用map的話,那么要用 entrySet() 方法去迭代!這樣的話,我們要訪問的就僅僅是Map.Entry的實例。

小結

在需要迭代鍵值對形式的Map時一定要用 entrySet() 方法。

8、使用EnumSet或EnumMap

 

在某些情況下,比如在使用配置map時,我們可能會預先知道保存在map中鍵值。如果這個鍵值非常小,我們就應該考慮使用 EnumSet 或 EnumMap,而并非使用我們常用的 HashSet 或 HashMap。下面的代碼給出了很清楚的解釋:

?
1
2
3
4
5
6
7
8
private transient Object[] vals;
 
public V put(K key, V value) {
    // ...
    int index = key.ordinal();
    vals[index] = maskNull(value);
    // ...
}

上段代碼的關鍵實現在于,我們用數組代替了哈希表。尤其是向map中插入新值時,所要做的僅僅是獲得一個由編譯器為每個枚舉類型生成的常量序列號。如果有一個全局的map配置(例如只有一個實例),在增加訪問速度的壓力下,EnumMap 會獲得比 HashMap 更加杰出的表現。原因在于 EnumMap 使用的堆內存比 HashMap 要少 一位(bit),而且 HashMap 要在每個鍵值上都要調用 hashCode() 方法和 equals() 方法。

小結

Enum 和 EnumMap 是親密的小伙伴。在我們用到類似枚舉(enum-like)結構的鍵值時,就應該考慮將這些鍵值用聲明為枚舉類型,并將之作為 EnumMap 鍵。

9、優化自定義hasCode()方法和equals()方法

 

在不能使用EnumMap的情況下,至少也要優化 hashCode() 和 equals() 方法。一個好的 hashCode() 方法是很有必要的,因為它能防止對高開銷 equals() 方法多余的調用。

在每個類的繼承結構中,需要容易接受的簡單對象。讓我們看一下jOOQ的 org.jooq.Table 是如何實現的?

最簡單、快速的 hashCode() 實現方法如下:

?
1
2
3
4
5
6
7
8
// AbstractTable一個通用Table的基礎實現:
 
@Override
public int hashCode() {
 
    // [#1938] 與標準的QueryParts相比,這是一個更加高效的hashCode()實現
    return name.hashCode();
}

name即為表名。我們甚至不需要考慮schema或者其它表屬性,因為表名在數據庫中通常是唯一的。并且變量 name 是一個字符串,它本身早就已經緩存了一個 hashCode() 值。

這段代碼中注釋十分重要,因繼承自 AbstractQueryPart 的 AbstractTable 是任意抽象語法樹元素的基本實現。普通抽象語法樹元素并沒有任何屬性,所以不能對優化 hashCode() 方法實現抱有任何幻想。覆蓋后的 hashCode() 方法如下:

?
1
2
3
4
5
6
7
8
// AbstractQueryPart一個通用抽象語法樹基礎實現:
 
@Override
public int hashCode() {
    // 這是一個可工作的默認實現。
    // 具體實現的子類應當覆蓋此方法以提高性能。
    return create().renderInlined(this).hashCode();
}

換句話說,要觸發整個SQL渲染工作流程(rendering workflow)來計算一個普通抽象語法樹元素的hash代碼。

equals() 方法則更加有趣:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// AbstractTable通用表的基礎實現:
 
@Override
public boolean equals(Object that) {
    if (this == that) {
        return true;
    }
 
    // [#2144] 在調用高開銷的AbstractQueryPart.equals()方法前,
    // 可以及早知道對象是否不相等。
    if (that instanceof AbstractTable) {
        if (StringUtils.equals(name,
            (((AbstractTable<?>) that).name))) {
            return super.equals(that);
        }
 
        return false;
    }
 
    return false;
}

首先,不要過早使用 equals() 方法(不僅在N.O.P.E.中),如果:

  • this == argument
  • this“不兼容:參數

CBOs (Cost-Based Optimisers) 。然后到了2010版,我們才終于將SQL的所有潛力全部挖掘出來。

但是我們還不需要用set方式來實現SQL。所有的語言和庫都支持Sets、collections、bags、lists。使用set的主要好處是能使我們的代碼變的簡潔明了。比如下面的寫法:

?
1
SomeSet INTERSECT SomeOtherSet

而不是

?
1
2
3
4
5
6
7
8
9
10
// Java 8以前的寫法
Set result = new HashSet();
for (Object candidate : someSet)
    if (someOtherSet.contains(candidate))
        result.add(candidate);
 
// 即使采用Java 8也沒有很大幫助
someSet.stream()
       .filter(someOtherSet::contains)
       .collect(Collectors.toSet());

有些人可能會對函數式編程和Java 8能幫助我們寫出更加簡單、簡潔的算法持有不同的意見。但這種看法不一定是對的。我們可以把命令式的Java 7循環轉換成Java 8的Stream collection,但是我們還是采用了相同的算法。但SQL風格的表達式則是不同的:

?
1
SomeSet INTERSECT SomeOtherSet
上面的代碼在不同的引擎上可以有1000種不同的實現。我們今天所研究的是,在調用 INTERSECT 操作之前,更加智能地將兩個set自動的轉化為 EnumSet 。甚至我們可以在不需要調用底層的 Stream.parallel() 方法的情況下進行并行 INTERSECT 操作。

總結

 

在這篇文章中,我們討論了關于N.O.P.E.分支的優化。比如深入高復雜性的算法。作為jOOQ的開發者,我們很樂于對SQL的生成進行優化。

  • 每條查詢都用唯一的StringBuilder來生成。
  • 模板引擎實際上處理的是字符而并非正則表達式。
  • 選擇盡可能的使用數組,尤其是在對監聽器進行迭代時。
  • 對JDBC的方法敬而遠之。
  • 等等。

jOOQ處在“食物鏈的底端”,因為它是在離開JVM進入到DBMS時,被我們電腦程序所調用的最后一個API。位于食物鏈的底端意味著任何一條線路在jOOQ中被執行時都需要 N x O x P 的時間,所以我要盡早進行優化。

我們的業務邏輯可能沒有N.O.P.E.分支那么復雜。但是基礎框架有可能十分復雜(本地SQL框架、本地庫等)。所以需要按照我們今天提到的原則,用Java Mission Control 或其它工具進行復查,確認是否有需要優化的地方。

原文鏈接: jaxenter 翻譯: ImportNew.com - 一直在路上

原文鏈接:http://www.importnew.com/16181.html

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 国产男女乱淫真视频全程播放 | 青青成人 | 欧美精品一区二区三区免费播放 | 久久综合视频网站 | 胸大的姑娘中文字幕视频 | 爽好紧别夹宝贝叫大声点护士 | 99热影院| 欧美3d怪物交videos网站 | 性直播免费| 美女福利视频一区二区 | 亚洲AV中文字幕无码久久 | 男生和老师一起差差差 | 国产精品久久国产三级国电话系列 | 88av免费观看 | 成人网中文字幕色 | 莫莉瑞典1977k | 日韩在线视频在线 | 性夜影院爽黄A爽免费动漫 性色欲情网站IWWW九文堂 | 欧美一区二区三区精品 | 国产亚洲精品自在线亚洲情侣 | 日本ww视频 | 91视频无限看 | 国产精品永久免费自在线观看 | 日本妇人成熟免费观看18 | 久久亚洲免费视频 | 男女真实无遮挡xx00动态图软件 | 日本免费观看的视频在线 | 欧美区日韩区 | 欧美贵妇vs高跟办公室 | 久久性综合亚洲精品电影网 | 99热在线只有精品 | 被老头肉至怀孕小说 | 国产伦精品一区二区三区免费迷 | a男人天堂 | 好姑娘在线视频观看免费 | 久久re6热在线视频 久久AV喷吹AV高潮欧美 | 女人扒开下面让男人桶爽视频 | 久久久久免费视频 | 无套内谢大学生A片 | 天天操天天舔 | 精品一产品大全 |