編碼性能規範

Table of Contents html

1    前言    7 java

2    編碼性能規範    7 正則表達式

2.1    線程同步規則    7 算法

2.1.1    規則描述    7 編程

2.1.2    案例研究    8 windows

2.2    字符串使用規則    10 api

2.2.1    規則描述    10 數組

2.2.2    案例研究    10 緩存

2.3    臨時對象建立規則    17 性能優化

2.3.1    規則描述    17

2.3.2    案例研究    17

2.4    集合類使用規則    22

2.4.1    規則描述    23

2.4.2    案例研究    23

2.5    IO讀寫規則    26

2.5.1    規則描述    26

2.5.2    案例研究    26

2.6    數組、集合操做規則    28

2.6.1    規則描述    29

2.6.2    案例研究    29

2.7    內存泄漏防範規則    30

2.7.1    規則描述    30

2.7.2    案例研究    31

3    設計性能規範    32

3.1    事件派發線程使用規則    32

3.1.1    規則描述    32

3.1.2    案例研究    32

3.2    界面組件設計規則    35

3.2.1    規則描述    38

3.2.2    案例研究    38

3.3    業務流程設計規則    41

3.3.1    規則描述    42

3.3.2    案例研究    42

3.4    界面響應設計規則    45

3.4.1    規則描述    45

3.4.2    案例研究    46

3.5    系統抗負載能力設計規則    46

3.5.1    規則描述    47

3.5.2    案例研究    47

3.6    多線程設計規則    48

3.6.1    規則描述    49

3.6.2    案例研究    49

4    附錄A:安裝盤壓縮    52

4.1    背景介紹    52

4.2    Pack200壓縮格式介紹    52

4.3    7z壓縮格式介紹    52

5    附錄B:性能測試專題    53

5.1    背景介紹    53

5.2    經常使用的java性能測試工具    54

5.2.1    Profiler工具介紹    54

5.2.2    Visualgc工具介紹    55

5.2.3    GC日誌分析工具介紹    55

5.2.4    Windows性能檢視器介紹    56

5.3    如何分析性能測試數據    57

5.3.1    檢查測試數據的真實性    57

5.3.2    經過Excel對測試數據進行處理    58

5.3.3    分析測試數據的關注點    58

5.3.4    對測試項進行理論評估和公式推導    58

6    參考文獻    59

表目錄 List of Tables

1 三種字符串分割算法的性能對比表    14

2 JTextArea組件和UltralEdit程序打開文件時的內存增量狀況    39

3 本身開發的文本組件和JTextArea組件的性能對比數據    40

4 經常使用性能計數器列表    56

圖目錄 List of Figures

1 JDK中使用線程局部變量的示例    8

2 JDK中同步代碼塊的示例    9

3 字符串常量測試代碼    10

4 經過"+"號實現字符串常量拼接的測試代碼    11

5 錯誤的字符串拼接方式    11

6 不一樣查找匹配方式實現的測試代碼    13

7 字符串分割的三種算法實現    14

8 字符串處理算法優化實例    16

9 優化前的跟蹤消息體保存處理流程    17

10 優化後跟蹤消息體保存處理流程    18

11 優化前的報文隱藏密碼方法的代碼    19

12 優化後的報文隱藏密碼方法的代碼    20

13 優化前的跟蹤過濾代碼片段    20

14 優化後的跟蹤過濾代碼片段    21

15 IntArrayList類的參考實現    23

16 Trove集合類和java集合類的性能對比    25

17 六種讀寫文件實現方式的測試代碼    27

18 數組拷貝的代碼示例    28

19 JDK的排序算法實現    29

20 JDKClassLoader代碼片段    31

21 跟蹤消息上報表格定時刷新處理流程    32

22 優化前的上報消息處理流程    33

23 優化後的上報消息處理流程    34

24 Swing輕型組件的繪製流程    37

25 優化表格的界面繪製流程    38

26 優化前的跟蹤消息碼流解析處理流程    42

27 優化後的跟蹤消息碼流解析處理流程    42

28 告警瀏覽上報消息的解析流程    43

29 優化後的消息詳細解釋窗口顯示流程    45

30 系統負載與響應時間關係圖    46

31 跟蹤流控機制的流程圖    47

32 優化前的跟蹤過濾代碼處理邏輯    49

33 優化後的跟蹤過濾代碼處理邏輯    50

34 Profiler工具的運行界面    54

35 Visualgc工具的運行界面    55

36 性能檢視器配置界面    57

 

 

Java編程性能規範

Scope:

本規範規定了在基於程序性能考慮的狀況下,java語言的系統編碼和系統設計規則。

本規範適用於使用Java語言編程的部門和產品。

Brief introduction

本規範從如何提升java系統性能的角度,給出了系統設計和編碼時的重要關注點。在本規範中,針對每個關注點,都從概述、規則描述和案例研究三個方面來展開描述。本規範中的規則描述都是從實際的優化案例和技術研究中提煉出來的,能夠供java編程人員在系統設計和編碼時做爲意見參考。值得一提的是,本文中列舉了大量的實際優化案例(基於JDK1.4環境),這些案例對於java系統的性能優化有着比較高的借鑑價值,能夠供開發人員在性能優化時參考。

關鍵詞Key wordsjava,性能優化

 

引用文件:

下列文件中的條款經過本規範的引用而成爲本規範的條款。凡是注日期的引用文件,其隨後全部的修改單(不包括勘誤的內容)或修訂版均不適用於本規範,然而,鼓勵根據本規範達成協議的各方研究是否可以使用這些文件的最新版本。凡是不注日期的引用文件,其最新版本適用於本規範。

序號No.

文件編號Doc No.

文件名稱 Doc Title

1

   

2

   
     
     

 

術語和定義Term&Definition<對本文所用術語進行說明,要求提供每一個術語的英文全名和中文解釋。List all Terms in this document, full spelling of the abbreviation and Chinese explanation should be provided.>

縮略語Abbreviations

英文全名 Full spelling

中文解釋 Chinese explanation

     
     
     
     

 

  1. 前言

性能是軟件產品開發中須要關注的一個重要質量屬性。在咱們產品實現的各個環節,均可能引入不一樣程度的性能問題,包括從最初的軟件構架選擇、到軟件的詳細設計,再到軟件的編碼實現等等。但在本規範中,主要內容是針對java系統,給出一些在軟件詳細設計和編碼實現過程當中須要注意的規則和建議。

即便咱們在產品開發的各個環節都考慮了性能質量屬性,可能仍是不夠的。隨着系統業務功能的不斷增多,用戶要求的不斷提升,或者爲了提升產品的可用性和競爭力,咱們不少狀況下仍是須要對已開發的產品進行一次集中式的專門的性能優化工做。本規範中所提供的大量實際優化案例,可供這種專門的性能優化工做進行參考。

可是在性能優化工做中,咱們也需警戒"過早優化"的問題。咱們的基本指導策略仍是首先讓系統運行起來,再考慮怎麼讓它變得更快。通常只有在咱們證明某部分代碼的確存在一個性能瓶頸的時候,才應進行優化。除非用專門的工具分析瓶頸,不然頗有多是在浪費本身的時間。另外,性能優化的隱含代價會使咱們的代碼變得難於理解和維護,這一點也是須要權衡和關注的。

  1. 編碼性能規範

    1. 線程同步規則

關於線程同步的處理,通常在編碼過程當中存在如下幾個問題:

  1. 在須要進行多線程同步處理的地方,沒有進行同步處理;
  2. 本是在單線程(如派發線程)中運行的代碼,加了沒必要要的同步;
  3. 原本已是在同步方法裏運行的代碼,又加了沒必要要的二次同步;
  4. 在能夠用線程局部變量來規避同步問題的地方,直接使用了synchronized來進行同步;
  5. 原本只須要對方法裏一小段代碼進行同步的地方,直接使用synchronized對整個方法進行了同步;

從性能角度來說,並非全部的同步方法都影響性能。若是同步方法調用的頻率足夠少,則加同步和不加同步,對性能影響不是很大,甚至能夠忽略。咱們在編寫代碼時,須要對一些重點的地方關注同步問題。

  1. 規則描述

建議1.1.1.1:對於可能被其它代碼頻繁調用的方法,須要關注同步問題

建議1.1.1.2:對於經常使用工具類的方法,須要關注同步問題

建議1.1.1.3:對於不能確認被其它代碼如何調用的方法,須要關注同步問題

 

在代碼編寫過程當中,很容易犯同步問題的錯誤,不恰當的使用同步機制。咱們在編寫代碼(重點是:可能被頻繁調用的方法、做爲經常使用工具類的方法、不能確認被其它代碼如何調用的方法)時,須要重點關注如下同步問題:

  1. 避免沒必要要的同步。如明確只是在事件派發線程中調用的方法,就沒必要要加同步;在單線程環境下,就儘可能不要用HashTable、Vector等類,而採用HashMap、ArrayList類;在多線程環境下,若是隻是局部代碼須要涉及同步問題,能夠經過Collections.synchronizedCollection(…)來對ArrayList等集合類實現同步。
  2. 避免進行二次同步。仔細檢查被頻繁調用的熱點代碼,對於那些已是在同步方法裏運行的代碼,就儘可能沒必要要再進行二次同步。測試證實,屢次同步比一次同步更影響性能。
  3. 在有些地方,能夠用線程局部變量來規避同步。如對於那些做爲臨時緩衝區的成員變量,在多線程環境下就能夠採用線程局部變量來實現。
  4. 儘可能縮小同步的代碼範圍。建議儘可能採用同步代碼塊的方式來進行同步,而不要直接對整個方法加synchronized進行同步。

總的來講,咱們不必對全部代碼都關注同步問題,在處理同步問題的性能優化時還須要保證代碼的可讀性和可維護性。因此對於進入維護階段的代碼,咱們不要盲目的進行同步問題的優化,以避免引入一些新的問題。

  1. 案例研究

    1. JDK中使用線程局部變量ThreadLocal來規避同步

在JDK的Integer類的public static String toString(int i)方法中,使用了線程局部變量perThreadBuffer來保存char數組,供構建String對象時使用。其代碼以下:

......

// Per-thread buffer for string/stringbuffer conversion

private static ThreadLocal perThreadBuffer = new ThreadLocal() {

protected synchronized Object initialValue() {

return new char[12];

}

};

public static String toString(int i) {

switch(i) {

case Integer.MIN_VALUE: return "-2147483648";

case -3: return "-3";

case -2: return "-2";

case -1: return "-1";

case 0: return "0";

case 1: return "1";

case 2: return "2";

case 3: return "3";

case 4: return "4";

case 5: return "5";

case 6: return "6";

case 7: return "7";

case 8: return "8";

case 9: return "9";

case 10: return "10";

}

char[] buf = (char[])(perThreadBuffer.get());

int charPos = getChars(i, buf);

return new String(buf, charPos, 12 - charPos);

}

......

 

  1. JDK中使用線程局部變量的示例

     

    關於以上代碼,若是不使用線程局部變量的話,通常作法會是首先在Integer類中定義一個char數組的靜態成員變量,而後直接將toString(…)方法加上synchronized關鍵字來進行同步處理。但這種作法咱們能夠想象,當Integer的toString(…)方法被頻繁調用的話,對性能影響是很是大的。

    改用線程局部變量後,能夠爲每一個調用的線程單獨維護一個char數組,而且保證每一個線程只使用本身的char數組變量,從而也就不存在須要同步的問題了,所以性能就比直接使用synchronized來進行同步要好不少。其實在JDK的源代碼中,Integer.java、Long.java、Charset.java、StringCoding.java等類中都用到了線程局部變量。

     

    1. JDK中使用代碼塊同步來代替直接對方法進行同步

    在JDK的源代碼中,不少地方都是使用代碼塊同步,而不是直接對整個方法進行同步的。直接對方法進行同步的一個主要問題在於:當一個線程對對象的一個同步方法進行調用時,會阻止其它線程對對象其它同步方法的調用,而無論對象的這些同步方法之間是否具備相關性。如下是一個java.util.Timer.java中同步代碼塊的例子:

    ……

    private TaskQueue queue = new TaskQueue();

    ……

    public void cancel() {

    synchronized(queue) {

    thread.newTasksMayBeScheduled = false;

    queue.clear();

    queue.notify(); // In case queue was already empty.

    }

    }

    ……

  2. JDK中同步代碼塊的示例

     

    1. 字符串使用規則

    在咱們實際開發的代碼中,有很大一部分代碼都是在作各類字符串操做。常常用到的字符串操做包括:字符串鏈接、字符串比較、字符串大小寫轉換、字符串切分、字符串查找匹配等。咱們在編碼過程當中存在的問題是:每每對於一種字符串操做,有多種處理方式可供選擇,而對於程序中的那些熱點方法,若是選擇了不恰當的字符串處理方式,將會對程序性能產生較大的影響。

    1. 規則描述

    規則1.2.1.1:對於常量字符串,不要經過new方式來建立

    規則1.2.1.2:對於常量字符串之間的拼接,請使用"+";對於字符串變量(不能在編譯期間肯定其具體值的字符串對象)之間的拼接,請使用StringBuffer;在JDK1.5或更新的版本中,若字符串拼接發生在單線程環境,可使用StringBuilder

    建議1.2.1.3:在使用StringBuffer進行字符串操做時,請儘可能設定初始容量大小;也儘可能避免經過String/CharSequence對象來構建StringBuffer對象

    規則1.2.1.4:當查找字符串時,若是不須要支持正則表達式請使用indexOf(…)實現查找;當須要支持正則表達式時,若是須要頻繁的進行查找匹配,請直接使用正則表達式工具類實現查找

    建議1.2.1.5:對於簡單的字符串分割,請儘可能使用本身定義的公用方法或StringTokenizer

    建議1.2.1.6:當須要對報文等文本字符串進行分析處理時,請增強檢視,注意算法實現的優化

    1. 案例研究

      1. 不恰當的字符串建立方式

    在java語言中,對於字符串常量,虛擬機會經過常量池機制確保其只有一個實例。常量池中既包括了字符串常量,也包括關於類、方法、接口等中的常量。當應用程序要建立一個字符串常量的實例時,虛擬機首先會在常量池中查找,看是否該字符串實例已經存在,若是存在則直接返回該字符串實例,不然新建一個實例返回。咱們說常量是能夠在編譯期就能被肯定的,因此經過new方法建立的字符串不屬於常量。關於字符串常量的特性,能夠經過如下代碼作一個測試:

    String s0="abcd";

    String s1="abcd";

    String s2=new String("abcd");

    String s3=new String("abcd");

    System.out.println( s0==s1 ); //true

    System.out.println( s2==s3 ); //false

    System.out.println( s0==s2 ); //false

    System.out.println( "============================" );

     

    s2=s2.intern(); //把常量池中"abcd"的引用賦給s2

    System.out.println( s0==s2 ); //true

     

     

    輸出結果爲:

    true

    false

    false

    ============================

    true

  3. 字符串常量測試代碼

    經過上面測試代碼的輸出結果能夠了解兩點:

    1. 對於字符串常量,不要經過new方法來進行建立,由於這樣可能會致使建立多個沒必要要的實例。
    2. 字符串對象有一個intern()方法,調用該方法後,若是常量池中沒有該字符串常量,則會促使虛擬機建立一個新字符串對象實例,並保存到常量池中;若是常量池中含有該字符串常量,則直接從常量池中返回該字符串常量實例。

     

    1. 不恰當的字符串拼接方式

    在java中,字符串拼接方式經常使用的有兩種,一是經過"+"號進行拼接,另一種是經過StringBuffer進行拼接。這兩種拼接方式都有本身特定的適用場合。通常規則是對於字符串常量之間的拼接,請使用"+";對於字符串變量(不能在編譯期間肯定其具體值的字符串對象),請使用StringBuffer。另外,當使用StringBuffer進行字符串拼接時,請儘可能指定合適的初始容量大小。如下代碼是對字符串常量拼接的一個測試代碼,經過"+"號來實現字符串常量拼接,能夠達到共享實例的目的:

    ……

    private static String constStr1="abcde";

    private static String constStr2="fghi";

     

    private final static String constStr3="abcde";

    private final static String constStr4="fghi";

     

    public static void main(String[] args) {

          

    String str0="abcdefghi";

    String str1="abcde";

    String str2="fghi";

    final String str_1="abcde";

    final String str_2="fghi";

     

    String str3="abcde"+"fghi";

    String str4=str1+str2;

    String str_4=str_1+str_2;

    String str5=constStr1+constStr2;

    String str6=constStr3+constStr4;

     

    System.out.println(str0==str3); //true,直接經過常量相加,能夠共享實例

    System.out.println(str0==str4); //false,經過引用對常量進行相加,將會獲得一個新的字符串變量

    System.out.println(str0==str_4); //true,經過final引用對常量相加,能夠共享實例

    System.out.println(str0==str5); //false,沒有加final,仍是會獲得一個新的字符串變量

    System.out.println(str0==str6); //true,成員變量加了final後,才能夠看成常量來使用

    }

    ……

     

    以上代碼輸出結果爲:

    true

    false

    true

    false

    true

  4. 經過"+"號實現字符串常量拼接的測試代碼

     

    最可怕的不恰當的字符串拼接方式,是在for循環中使用"+"來進行字符串對象拼接,相似以下代碼:

     

    for(int i = 0 ; i < 1024*1024; i++ )

    {

          str += "XXX" ;

    }

  5. 錯誤的字符串拼接方式

     

    運行上面代碼的結果是:將致使整個操做系統CPU佔有率長時間達到100%,操做系統長時間(應該在5分鐘以上)幾乎處於不響應狀態。具體緣由很簡單,每次"+"號操做後,都會生成一個新的臨時字符串對象,隨着循環的深刻,建立的臨時字符串對象愈來愈大,執行起來就會愈來愈困難。若是將上述代碼改成StringBuffer來實現拼接,能夠看到程序可以正常運行。

     

    1. 不恰當的字符串查找匹配

    在java中,進行字符串查找匹配時通常有三種實現方式:第一種是調用String對象的indexOf(String str)方法;第二種是調用String對象的matches(String regex)方法;第三種是直接使用正則表達式工具類(包括Pattern類、Matcher類)來實現匹配。這三種實現方式的各自特色以下:

    1. indexOf(String str)方法運行速度最快,效率最高,但不支持正則表達式。
    2. matches(String regex)方法性能最差,但支持正則表達式,使用起來簡單(該方法性能差的緣由是每調用一次時,就從新對正則表達式編譯了一次,新建了一個Pattern對象出來,而不是重複利用同一個Pattern對象)。
    3. 直接使用正則表達式工具類來實現匹配,能夠支持正則表達式,在頻繁操做下性能比matches(String regex)方法要好不少。

    如下是對三種查找匹配實現方式的性能測試代碼:

    String s0="abcdefghjkilmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    String s1="890ABCDE";

    Pattern p = Pattern.compile(s1);

    Matcher m = p.matcher(s0);

    int loop=1000000;

    long start=System.currentTimeMillis();

    for(int i=0;i<loop;i++)

    {

        s0.indexOf(s1); //經過String對象的indexOf(String str)方法實現查找匹配

    }

    long end=System.currentTimeMillis();

    long time1=end-start;

     

    start=System.currentTimeMillis();

    for(int i=0;i<loop;i++)

    {

        s0.matches(s1); //經過String對象的matches(String str)方法實現查找匹配

    }

    end=System.currentTimeMillis();

    long time2=end-start;

     

    start=System.currentTimeMillis();

    for(int i=0;i<loop;i++)

    {

        m.matches(); //經過正則表達式工具類實現查找匹配

    }

    end=System.currentTimeMillis();

    long time3=end-start;

     

    System.out.println("time1:"+time1);

    System.out.println("time2:"+time2);

    System.out.println("time3:"+time3);

     

     

    上述代碼某次執行時的輸出結果:

    time1:187

    time2:1844

    time3:219

     

  6. 不一樣查找匹配方式實現的測試代碼

     

    1. 不恰當的字符串分割

    在咱們實際開發中,經常須要對字符串進行分割,如將"abc def ghi"按空格分割成三個字符串而後放到一個String數組中。在java中,通常有三種方法能夠實現字符串的分割:

    1. 利用String對象提供的split(String regex)方法實現分割;
    2. 利用StringTokenizer對象實現字符串分割;
    3. 本身開發代碼實現字符串分割;

    對於以上三種分割方法,其運行效率差異是很大的。採用split(String regex)方法性能最差,但因爲使用了正則表達式,功能性最強;採用StringTokenizer對象分割字符串,性能能夠接受,功能也較強;採用本身開發代碼實現字符串分割,速度最快,但不具有通用性。下面是對三種方法的一個測試代碼:

    ……

    //經過String對象的split(String regex)方法實現字符串分割

    public static String[] testSplit1(String str)

    {

        return str.split(",");

    }

     

    //經過StringTokenizer對象實現字符串分割

    public static String[] testSplit2(String str)

    {

    ArrayList list=new ArrayList();

    StringTokenizer st = new StringTokenizer(str,",");

    while(st.hasMoreTokens())

         {

            list.add(st.nextToken());

         }

        String[] obj=(String[])list.toArray(new String[0]);

        return obj;

    }

     

    //經過本身開發代碼實現字符串分割

    public static String[] testSplit3(String str)

    {

        int fromIndex=0;

        int index0=0;

        int signLen=",".length();

        int strLen=str.length();

          

        index0=str.indexOf(",",fromIndex);

        if(index0==-1)

        {

            return new String[]{str};

        }

    ArrayList list=new ArrayList();

        String subStr=str.substring(fromIndex,index0);

        if(!subStr.equals(""))

        {

            list.add(subStr);

        }

          

        fromIndex=index0+1;

          

        while(fromIndex<strLen-1)

        {

            index0=str.indexOf(",",fromIndex);

            if(index0==-1)

            {

                list.add(str.substring(fromIndex));

                break;

            }

              

            String subStr1=str.substring(fromIndex,index0);

        if(!subStr1.equals(""))

        {

            list.add(subStr1);

        }

    fromIndex=index0+signLen;

        }

        return (String[])list.toArray(new String[0]);

    }

    ……

  7. 字符串分割的三種算法實現

    通過測試,假設要分割的字符串是"aaaa,bbbb,cccc,dddd,eeee,ffff",對上述三種算法各調用1000次後,所花的時間大概以下表:

    實現算法

    所花時間(單位:毫秒)

    採用split(String regex)方法進行分割

    141 

    採用StringTokenizer進行分割

    46 

    採用自定義方法進行分割

    16 

  8. 三種字符串分割算法的性能對比表

    根據上表的性能測試結果可知,採用split(String regex)方法進行分割字符串性能是很低的。因此在實際代碼開發中,若是須要頻繁對字符串進行分割的話,最好不要採用String對象的split(…)方法進行字符串分割,一個比較好的選擇是本身寫一個公用的字符串分割方法。

     

    1. 不高效的字符串處理算法

    在實際開發中,會遇到須要對報文等文本字符串進行分析處理的問題,對於不一樣的開發人員,實現文本字符串分析處理的算法會存在較大差別,有的實現會比較高效,有的實現看起來比較簡單但性能卻不好。如下是一個字符串處理算法優化案例:

    //優化前的方法實現

    private String extractPingReportBody(String strPingReport)

    {

    String intialMessage = strPingReport;

    int index = intialMessage.toUpperCase().indexOf(PING_MESSAGE_FORMAT);

    if(0 <= index)

    {

    intialMessage = intialMessage.substring(index);//建立了一個新的子串

    index = intialMessage.indexOf(DOUBLE_LINE_SEPARATOR);

    if(0 <= index)

    {

    //又建立了一個新的子串

    intialMessage = intialMessage.substring(index + DOUBLE_LINE_SEPARATOR.length());

    index = intialMessage.indexOf(DOUBLE_LINE_SEPARATOR);

    if(0 <= index)

    {

    intialMessage = intialMessage.substring(0, index); //又建立了一個新的子串

    return intialMessage;

    }

    else

    {

    ......

    }

    }

    else

    {

    ......

    }

    }

    else

    {

    ......

    }

    return strPingReport;

    }

     

    //優化後的方法實現

    private String extractPingReportBody(String strPingReport)

    {

        int index0 = strPingReport.toUpperCase().indexOf(PING_MESSAGE_FORMAT);

        if(index0<0)

        {

    ......

        }

        int index1= strPingReport.indexOf(DOUBLE_LINE_SEPARATOR,index0);//再也不建立子串

        if(index1<0)

        {

    ......

        }

        else

        {

         //再也不建立子串

    int dex2=strPingReport.indexOf(DOUBLE_LINE_SEPARATOR,index1

    + DOUBLE_LINE_SEPARATOR.length());

         if(index2>=0)

         {

            return strPingReport.substring(index1,index2);

         }

         else

         {

            ......

         }

        }

        return strPingReport;

    }

  9. 字符串處理算法優化實例

    總結字符串處理的優化案例,通常不高效的字符串處理算法表現爲:

    1. 使用StringBuffer時,沒有給其合適的初始容量大小;
    2. 在多分支處理流程中,沒有按需建立對象;
    3. 字符串方法使用不正確。如如下代碼片段,就是因爲indexOf方法使用不正確致使了錯誤的作法:

    錯誤的作法:

    intialMessage = intialMessage.substring(index);

    index = intialMessage.indexOf(DOUBLE_LINE_SEPARATOR);

     

     

    好的作法:

    index= intialMessage.indexOf(DOUBLE_LINE_SEPARATOR,index);

     

    1. 臨時對象建立規則

    在java語言中,或者說在面嚮對象語言中,對象的建立是既耗時間,又佔用內存。若是在系統運行過程當中建立了大量沒必要要的臨時對象,對性能的影響是比較大的。所以如何避免建立沒必要要的臨時對象,對系統性能的提高有着重要做用。在實際代碼開發中,建立了沒必要要的臨時對象的緣由通常可分爲如下幾種:

    1. 業務處理流程不夠精簡,增長了沒必要要的中間環節;
    2. 提早建立對象,而不是按需建立對象;
    3. 在for、while等循環裏面建立對象,而不是在外面建立對象,使對象沒有重用;
    4. 對於高頻度使用的對象,沒有進行優化處理給以重用;

     

    1. 規則描述

    建議1.3.1.1:在實現業務處理流程的過程當中,須要考慮臨時對象引發的性能問題,精簡業務處理流程,減小沒必要要的中間環節

    建議1.3.1.2:對象的建立應儘可能按需建立,而不是提早建立

    建議1.3.1.3:對象的建立應儘可能在for、while等循環外面建立,在循環裏面進行重用

    建議1.3.1.4:對於高頻度使用的對象,須要進行單獨優化處理給以重用

    1. 案例研究

      1. 跟蹤消息體碼流保存的處理流程不夠精簡

    在某OM系統中,有一個對跟蹤消息體碼流進行保存的處理流程。在該處理流程中,主機會頻繁的向OM系統上報跟蹤消息,OM系統須要對全部這些上報消息實時進行保存,以便用戶後續根據消息分析和定位問題。在優化前,跟蹤消息體的保存處理流程以下圖所示:

  10. 優化前的跟蹤消息體保存處理流程

    從上面的處理流程能夠看出,每保存一條消息體碼流,就新建了一個ByteBuffer對象和一個byte數組對象,這是沒必要要的浪費,當跟蹤消息上報速度很大時,這種ByteBuffer和byte數組臨時對象的建立對性能影響是很大的。通過對該處理流程改進優化後,新的處理流程以下圖:

  11. 優化後跟蹤消息體保存處理流程

    優化前和優化後的處理流程的差異在於,優化前每保存一條消息碼流,都要建立一個ByteBuffer對象,而優化後把這一過程給省略了,直接經過IO寫入數據,減小了中間碼流的轉換過程。

    1. 報文隱藏密碼方法的性能優化

    在某OM系統的MML報文隱藏密碼方法中,優化前存在三個問題:第一個問題是StringBuffer沒有按需建立;第二個問題是存在多餘處理邏輯;第三個問題是StringBuffer沒有指定初始容量大小。如下是隱藏密碼方法優化前的代碼:

    public static String hidePassword(final String msg)

    {

    if (null == msg || "".equals(msg))

    {

    return msg;

    }

    Matcher matcher = getPattern().matcher(msg);

    //StringBuffer沒有按需建立,由於若是一開始就沒有發現匹配的話,StringBuffer對象就是多餘的;

    //StringBuffer沒有指定初始大小,實際上StringBuffer的容量能夠用msg.length()來指定;

    StringBuffer sbf = new StringBuffer();

    int iLastEnd = 0;

    int iEnd = 0;

    int iValueIdx = 0;

    while (matcher.find())

    {

    iEnd = matcher.end();

    sbf.append(msg.substring(iLastEnd, iEnd));

    iValueIdx = msg.indexOf('"', iEnd);

    if (0 <= iValueIdx)

    {

    sbf.append("*****");

    iLastEnd = iValueIdx;

    }

    else

    {

    iLastEnd = iEnd;

    }

    }

    if (iLastEnd < msg.length())

    {

    //若是一開始就沒有發現匹配的話,即iLastEnd==0,這時只須要直接返回msg就好了,

    //因此在這種狀況下,下面的語句就是多餘的。

    sbf.append(msg.substring(iLastEnd));

    }

    return sbf.toString();

    }

  12. 優化前的報文隱藏密碼方法的代碼

    通過優化後, 報文隱藏密碼方法的代碼以下:

    public static String hidePassword(final String msg)

    {

    if (null == msg || msg.length()==0)

    {

    return msg;

    }

    Matcher matcher = getPattern().matcher(msg);

        StringBuffer sbf = null;

    int iLastEnd = 0;

    int iEnd = 0;

    int iValueIdx = 0;

    while (matcher.find())

    {

         //將sbf的new操做放到下面來進行。

         if(null == sbf)

         {

            //最後StringBuffer的長度確定與msg的長度差很少,因此能夠給其指定初始大小

    sbf = new StringBuffer(msg.length());

         }

    iEnd = matcher.end();

    sbf.append(msg.substring(iLastEnd, iEnd));

    iValueIdx = msg.indexOf('"', iEnd);

    if (0 <= iValueIdx)

    {

    sbf.append("*****");

    iLastEnd = iValueIdx;

    }

    else

    {

    iLastEnd = iEnd;

    }

    }

         //若是一開始就沒有發現匹配的話,則直接返回msg就好了。

         if(0 == iLastEnd)

         {

            return msg;

    }

    else if (iLastEnd < msg.length())

    {

    sbf.append(msg.substring(iLastEnd));

    }

    return sbf.toString();

    }

  13. 優化後的報文隱藏密碼方法的代碼

     

    在實際代碼開發中,沒有按需建立對象是一個常常容易犯的錯誤。不少開發人員每每圖一時編碼方便或時間緊張來不及細想,將一些對象提早建立出來,而在後面某些條件分支中又徹底用不到這些對象。

     

    1. 跟蹤過濾在for循環裏面建立Map對象,而不是重用Map對象

    在某OM系統的跟蹤過濾功能中,有一段熱點代碼以下圖:

    ……

    public void executeColumnFilter(String[] selectedValues)

    {

    ……

    for (int tableIndex = 0; tableIndex < size; tableIndex++)

    {

        Vector rowData = (Vector) tableDataV.get(tableIndex);

        String detailMsgStr = getDetailMsgStr(tableIndex);

    ……

    }    

    ……

    }

    ……

     

    private String getDetailMsgStr(int tableIndex)

    {

        String detailMsgStr = "";//消息詳細解釋碼流字符串        

        HashMap detailParameter = new HashMap(); //該對象沒有重用

    ……

    }

    ……

  14. 優化前的跟蹤過濾代碼片段

    在上面顯示的代碼中,for循環裏面每次都調用了getDetailMsgStr()方法,而該方法每次都會建立一個臨時HashMap對象,方法調用結束後,HashMap就再也不使用了。通過測試(512內存、CPU2.4G),在for循環裏面建立50000個HashMap對象(不指定初始容量大小)的開銷是:須要時間約16毫秒、須要內存約320K。因此,在性能比較緊張的狀況下,如何重用該Map對象仍是有意義的。

    通過優化後,代碼結構以下圖:

    ……

    public void executeColumnFilter(String[] selectedValues)

    {

    ……

    Map map=new HashMap(3); //在for循環外面建立對象,在使用時進行重用。

    for (int tableIndex = 0; tableIndex < size; tableIndex++)

    {

    Vector rowData = (Vector) tableDataV.get(tableIndex);

    map.clear();

    String detailMsgStr = getDetailMsgStr(tableIndex,map);

    ……

    }

    ……

    }

    ……

     

    private String getDetailMsgStr(int tableIndex,Map map)

    {

    String detailMsgStr = "";//消息詳細解釋碼流字符串

    ……

    }

    ……

  15. 優化後的跟蹤過濾代碼片段

     

    雖然優化後的代碼比優化前更加高效,但可讀性要比優化前差些,須要多加一些註釋進行說明。從這個優化方案,能夠總結出如下結論:

    1. 在不少狀況下,因爲方法的調用層次太多,咱們一般是無心識的或沒有察覺到在for、while等循環裏面建立了大量臨時對象。
    2. 在設計類的時候,若是方法的粒度太細,則會因爲方法之間無法共用局部變量,一般會致使建立的臨時對象會比粗粒度的方法要多。
    3. 若是經過優化來減小局部變量,通常代碼的可讀性會變差。因此咱們一般只須要對熱點方法進行優化便可,對不頻繁調用的方法進行優化,每每是得不償失的。熱點方法能夠經過業務分析、性能測試等手段來找出。

     

    1. 集合類使用規則

    在java API的集合框架(Java Collections Framework)中,提供了豐富的集合處理類,包括無序集合(Set集合)、有序集合(List集合)和映射關係集合(Map集合)。在一般狀況下,集合框架提供了足夠的功能供咱們使用,咱們關注的重點是如何選擇這些集合類。但須要指出的是,java API的集合框架也並無想象的那麼完善,它不可能解決全部應用場景下的問題。咱們在使用集合類進行編程時,經常面臨如下問題:

    1. 集合類的選擇問題。實現同一個業務處理,每每能夠有多個集合類供選擇,可是在特定的應用場景下,出於性能的考慮只有一種集合類是最佳的選擇。例如在單線程環境下,使用ArrayList會比Vector更加高效;在須要隨機提取集合中的數據時,ArrayList會比LinkedList更加高效。
    2. 採用特定的集合類。Java API的集合框架是基於對象模型來設計的,其操做的數據必須是對象,不能是基本類型。這種約束在某些應用場景下,將會產生性能問題。例如本只是對純數字進行處理的業務,採用集合框架後,全部數據就都須要用Integer等對象來表示,而不能用基本類型。對於這種狀況,咱們能夠根據業務須要採用特定的集合類(這種特定集合類能夠是本身開發,也能夠採用Trove這樣的第三方開源類庫),以優化系統性能。
    3. 結構體和集合類的選擇問題。在實際開發中,當咱們須要在方法之間傳遞多個數據時,既能夠經過一個結構體對象(java bean)來傳遞多個屬性值,也能夠用一個ArrayList或Vector來傳遞多個屬性值。顯然,用結構體來傳遞屬性值會更加高效,而用集合類來傳遞屬性值通用性會更強一些。

     

    1. 規則描述

    建議1.4.1.1:在代碼開發中,須要根據應用場景合理選擇集合框架中的集合類,應用場景可按單線程和多線程來劃分,也可按頻繁插入、隨機提取等具體操做場景來劃分

    建議1.4.1.2:對於熱點代碼,能夠採用特定的集合類來提供系統性能,特定集合類能夠是本身開發,也能夠直接採用Trove這樣的第三方開源類庫

    建議1.4.1.3:當須要在方法之間傳遞多個屬性值時,從性能角度考慮,應優先採用結構體,而非ArrayList或Vector等集合類

    1. 案例研究

      1. 根據業務處理特色,開發特定的集合類

    在某網管系統中,故障查詢數據中的流水號佔用了大量內存。當數據量達到必定數值(通常達到300萬)時,容易形成前臺內存溢出。其緣由主要是流水號的存取是採用集合框架的ArrayList來存取的。因爲ArrayList必須以Object的方式保存內容,所以在保存流水號的時候必須用Integer對象,而不是基本整數類型。在java中一個Integer對象佔用的內存大約爲32個字節,而int類型只佔用4個字節,因此在大數據量狀況下,採用Integer對象比int類型耗費的內存要多得多。針對上面的問題,優化思路是開發一個特定的集合類IntArrayList,該類有兩個特色:

    1. 保持與原接口兼容。它實現List接口,那麼對外的接口幾乎和原來ArrayList類一致,這樣對現有系統的改動是很是小的,只須要用新寫的類替換掉程序中原有的類。
    2. 用基本數據類型替換對象。在IntArrayList類裏面用一個int型數組來保存數據而不是用Object對象數組,從而達到減小內存佔用的目的。

    IntArrayList類的參考實現以下:

    public class IntArrayList implements List

    {

    int size = 0;

    //保存Integer數值的數祖

    int[] elementData = null;

    public IntArrayList(int initialCapacity)

    {

    super();    

    if (initialCapacity < 0){

    throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);

    }

    this.elementData = new int[initialCapacity];

    }

    /**

    * 增長數據

    */

    public boolean add(Object o)

    {

    try

    {

    ensureCapacity(size + 1); // Increments modCount!!

    elementData[size++] = ( (Integer)o).intValue();

    return true;

    }

    catch (Exception ex){}

    return false;

    }

    /**

    * 獲取數據

    */

    public Object get(int index)

    {

    RangeCheck(index);

    return new Integer(elementData[index]);

    }

    /**

    * 刪除指定位置的數據

    */

    public Object remove(int index)

    {

    Object oldValue = null;

    try

    {

    RangeCheck(index);

    oldValue = new Integer(elementData[index]);

    int numMoved = size - index - 1;

    if (numMoved > 0){

    System.arraycopy(elementData, index + 1, elementData, index,numMoved);

    }

    sizee--;

    }

    catch (Exception ex){

    return oldValue;

    }

    }

    }

  16. IntArrayList類的參考實現

     

    1. Trove集合類和java集合類的性能對比

    Trove集合類是一種開放源代碼的java集合包,聽從LGPL協議(能夠商用),提供了java集合類的高效替代品。Trove能夠從http://trove4j.sourceforge.net/處獲取。相對於java集合框架,Trove具備如下特色:

    1. 提供了基於基本數據類型(byte short char int long float double boolean )的Map集合類。好比HashMap,根據鍵值對的組合關係共有81種Map,如TIntIntHashMap、TIntObjectHashMap。
    2. 提供了能夠存儲基本數據類型的List和Set類。如TIntHashSet、TFloatArrayList等。
    3. 採用開放選址方式而非連接方式來實現映射。在java集合框架中,多數映射都是使用連接實現,就是說若是多個鍵映射到表中的同一索引位置,則索引位置保存一個鏈表,其中存放映射到該位置的全部元素。開放選址映射則假設表中鄰近的位置存在沒有使用的索引。若是目標位置已經被佔用,映射實現就查看附近的幾個位置找到一個沒有使用的位置。這種方法不須要鏈表節點,所以 Trove 映射和相同的核心集合類相比佔用的內存更少。

    如下是對Map的put和get方法的性能對比測試:

    TObjectIntHashMap intmap = new TObjectIntHashMap();

    HashMap map=new HashMap();

    int loop=300000;

    long start=System.currentTimeMillis();

    for(int i=0;i<loop;i++)

    {

         intmap.put("N"+i, 2);

         int numInStock = intmap.get("N"+i);

    }

    long end=System.currentTimeMillis();

        

    long time=end-start;

        

    Integer intObj=new Integer(2);

    start=System.currentTimeMillis();

    for(int i=0;i<loop;i++)

    {

         map.put("N"+i, intObj);

         Integer numInStockObj=(Integer)map.get("N"+i);

    }

    end=System.currentTimeMillis();

    long time1=end-start;

        

    System.out.println("time:"+time);

    System.out.println("time1:"+time1);

     

    以上代碼某次執行時的輸出結果以下:

     

    time:1375

    time1:1593

  17. Trove集合類和java集合類的性能對比

     

     

    1. IO讀寫規則

    IO讀寫是咱們在實際開發中常常要遇到的功能實現。Java API爲咱們提供了龐大的IO讀寫庫,從實現上來看可分爲早期的基於流的IO庫和新的基於塊的NIO庫,從功能上來看可分爲基於字節操做的IO庫和基於字符操做的IO庫。在這麼龐大的IO庫下,對於同一個IO功能,能夠編寫出多種實現代碼,但須要強調的是,一個設計拙劣的IO代碼可能要比通過精心調整的IO代碼慢上幾倍。爲了使咱們開發的系統能高效運行,咱們就必然面臨一個問題:怎樣編碼才能使IO功能實現能夠性能最優?

    1. 規則描述

    規則1.5.1.1:進行IO讀寫操做時,必須使用緩衝機制

    建議1.5.1.2:從性能角度考慮,應儘可能優先使用字節IO進行讀寫,而避免用字符IO進行讀寫

     

    1. 案例研究

      1. 六種讀寫文件實現方式的性能比較

    對於文件讀寫操做,java API中提供了多種類庫可供咱們選擇,不一樣的選擇和實現方式會產生不一樣的性能結果。如下是六種讀寫文件實現方式的測試代碼:

    //經過NIO實現文件讀寫(方式1)

            FileInputStream fin1 = new FileInputStream("d:/test1.rar");

            FileOutputStream fout1 = new FileOutputStream("d:/e/test1.rar");

            FileChannel fcin = fin1.getChannel();

            FileChannel fcout = fout1.getChannel();

            int fileLength = (int)fcin.size();

            long start=System.currentTimeMillis();

            fcin.transferTo(0, fileLength, fcout);

            fin1.close();

            fout1.close();

            long end = System.currentTimeMillis();

            long time1 = end - start;

            System.out.println("NIO_time1:"+time1);

     

            //經過NIO實現文件讀寫(方式2)

            FileInputStream fin11 = new FileInputStream( "d:/test11.rar" );

            FileOutputStream fout11 = new FileOutputStream( "d:/e/test11.rar" );

            FileChannel fcin11 = fin11.getChannel();

            FileChannel fcout11 = fout11.getChannel();

            ByteBuffer buffer = ByteBuffer.allocate( 512 );

            start=System.currentTimeMillis();

            while (fcin11.read(buffer)!=-1) {

            buffer.flip();

            fcout11.write( buffer );

            buffer.clear();

            }

            fin11.close();

            fout11.close();

            end = System.currentTimeMillis();

            long time11 = end - start;

            System.out.println("NIO_time2:"+time11);

     

    //經過IO進行批量讀寫

            byte[] arr = new byte[512];

            FileInputStream fin3 = new FileInputStream("d:/test3.rar");

            FileOutputStream fout3 = new FileOutputStream("d:/e/test3.rar");

            start = System.currentTimeMillis();

            while (fin3.read(arr) != -1) {

                fout3.write(arr);

            }

            fin3.close();

            fout3.close();

            end = System.currentTimeMillis();

            long time3 = end - start;

            System.out.println("IO_byteArray:" + time3);

     

    //經過buffer IO進行讀寫

            FileInputStream fin4 = new FileInputStream("d:/test4.rar");

            FileOutputStream fout4 = new FileOutputStream("d:/e/test4.rar");

            BufferedInputStream bufferInput=new BufferedInputStream(fin4);

            BufferedOutputStream bufferOutput=new BufferedOutputStream(fout4);

            int c=-1;

            start = System.currentTimeMillis();

            while ((c = bufferInput.read()) != -1) {

                bufferOutput.write(c);

            }

            bufferInput.close();

            bufferOutput.close();

            end = System.currentTimeMillis();

            long time4 = end - start;

            System.out.println("IO_Buffer:"+time4);

     

    //經過字符IO進行讀寫

            FileReader reader=new FileReader("d:/test5.rar");

            FileWriter writer=new FileWriter("d:/e/test5.rar");

            char[] charArr = new char[512];

            start = System.currentTimeMillis();

            while (reader.read(charArr) != -1) {

                writer.write(charArr);

            }

            reader.close();

            writer.close();

            end = System.currentTimeMillis();

            long time5 = end - start;

            System.out.println("IO_char:" + time5);

     

    //直接經過IO進行讀寫(不使用緩衝)

         c = -1;

            FileInputStream fin2 = new FileInputStream("d:/test2.rar");

            FileOutputStream fout2 = new FileOutputStream("d:/e/test2.rar");

            start = System.currentTimeMillis();

            while ((c = fin2.read()) != -1) {

                fout2.write(c);

            }

            fin2.close();

            fout2.close();

            end = System.currentTimeMillis();

            long time2 = end - start;

            System.out.println("IO_noBuffer:"+time2);

     

    以上代碼某次執行的輸出結果以下(讀寫的文件大小爲3M):

    NIO_time1:171

    NIO_time2:250

    IO_byteArray:235

    IO_Buffer:344

    IO_char:515

    IO_noBuffer:10002

  18. 六種讀寫文件實現方式的測試代碼

    經過以上代碼測試,可得出以下結論:

    1. 使用NIO或IO批量讀寫,性能比較好,這三種實現方案的性能應該是至關的;
    2. 使用IO Buffer讀寫文件,性能比NIO、IO批量讀寫性能要差些;但從理論上分析,IO Buffer和IO批量讀寫是等價的,只是多了一些判斷操做,這個能夠從BufferedInputStream的源代碼中能夠看出。
    3. 基於字符的IO讀寫比基於字節的IO讀寫要慢不少,因此咱們應儘可能基於字節IO進行讀寫操做;其緣由是基於字符的IO讀寫多了頻繁的字符轉換操做。另外須要說明的是,一個char用兩個字節保存字符,而byte只須要一個,所以用byte保存字符消耗的內存和須要執行的機器指令更少。更重要的是,用byte避免了進行Unicode轉換。所以,若是可能的話,應儘可能使用byte替代char。例如,若是應用必須支持國際化,則必須使用char;若是從一個ASCII數據源讀取(好比HTTP或MIME頭),或者可以肯定輸入文字老是英文,則程序可使用byte。
    4. 若是直接經過IO進行讀寫,不使用緩衝區的話,會致使嚴重的性能問題;其緣由主要是這種IO讀寫操做須要頻繁的訪問磁盤和調用操做系統底層函數。

     

    1. 數組、集合操做規則

    在java API中,針對數組和集合的操做,專門封裝了兩個類:java.util.Arrays和java.util.Collections。在這兩個工具類中,包含了數組及集合的經常使用操做方法,如拷貝、查找、排序等,這些方法通常來講算法實現上都是很是高效的。可是對工具類中的排序等方法,存在的問題是,這些方法因爲不能從業務數據集合裏面獲取排序數據,所以在實現上就多了不少數據拷貝、克隆等操做,形成的結果是容易生成大量的臨時對象。因此當咱們須要對數組或集合進行拷貝、查找、排序等操做時,對於通常的應用應優先使用Arrays和Collections中提供的方法,可是對於熱點代碼,最好是參考java API中的方法實現,本身開發特定的排序等方法。

     

    1. 規則描述

    建議1.6.1.1:對於數組、集合的拷貝、查找、排序等操做,若是是通常應用,能夠優先採用java.util.Arrays和java.util.Collections中提供的工具方法;可是對於熱點代碼,最好是參考java API中的方法實現,本身開發特定的排序等方法,以減小臨時對象的建立。

    規則1.6.1.2:對於數組的拷貝,請使用System.arraycopy(…)方法

     

    1. 案例研究

      1. 錯誤的數組拷貝方法

    一些對java API不太熟悉的開發人員,每每會犯數組拷貝的錯誤,沒有用Arrays類中提供的工具方法而是本身寫一個很是低效的數組複製方法。如下是相關的代碼示例:

    char[] sourceArr=new char[1000];

    char[] destineArr=new char[1000];

     

    //錯誤的數組拷貝方法

    for(int i=0;i<sourceArr.length;i++)

    {

    destineArr[i]=sourceArr[i];

    }

     

    //正確的數組拷貝方法

    System.arraycopy(sourceArr, 0, destineArr, 0, sourceArr.length);

  19. 數組拷貝的代碼示例

     

    1. JDK的排序算法實現

    在Collections類中實現了排序工具方法,該方法的實現代碼以下:

    //Collections類中的sort方法

    public static void sort(List list) {

        Object a[] = list.toArray(); //新建立了一個數組

        Arrays.sort(a);

        ListIterator i = list.listIterator();

        for (int j=0; j<a.length; j++) {

         i.next();

         i.set(a[j]);

        }

    }

     

    //ArrayList類中的toArray()方法

    public Object[] toArray() {

        Object[] result = new Object[size];

        System.arraycopy(elementData, 0, result, 0, size);

        return result;

    }

     

    //Arrays.sort方法

    public static void sort(Object[] a) {

    Object aux[] = (Object[])a.clone(); //爲了排序,又複製了一個數組

    mergeSort(aux, a, 0, a.length, 0);

    }

  20. JDK的排序算法實現

    分析上面的代碼實現,能夠看出,在一次排序過程當中,該排序算法產生了2個臨時的數組對象,這對於那些動態排序功能(須要根據上報的數據頻繁的進行排序)的實現,性能的影響是不能忽略的。

    1. 內存泄漏防範規則

    和C++同樣,內存泄漏問題也是java程序開發中須要重點關注的性能問題。一些常見的內存泄漏緣由有:

    1. 往框架類或系統類對象(這些對象在系統運行過程當中始終保持存活狀態)中註冊了一些事件監聽器,在使用完後又沒及時清除。
    2. 往集合對象(如ArrayList、Vector、HashMap)中只添加而不刪除元素,並保持對集合對象的引用。
    3. 在一個對象中建立了一個線程,當對象再也不使用時,又沒有關閉該線程。
    4. 在JFrame、JDialog等窗口對象中,沒有處理窗口關閉事件,致使窗口關閉時只是隱藏,而沒有釋放資源。
    5. 對於IO操做,沒有在finally中做對應的關閉動做。
    6. 在重載的finallize()方法中,沒有調用super.finallize()方法。
    7. 使用一個自定義的類裝載器去裝載類,當被裝載的類再也不使用時,仍然保持該類裝載器的引用。

    另外值得一提的是,將一些大的對象定義成靜態的,也會形成相似於內存泄漏的問題。其緣由是若是靜態變量所屬的類是被系統類裝載的,則即便該類再也不使用時也不會被卸載掉,這將致使靜態對象的生存時間可能和系統同樣長久,而無論該對象是否被使用。

    1. 規則描述

    規則1.7.1.1:若是往框架類或者系統類對象中添加了某個對象,那麼當該對象再也不使用時,必須及時清除(這裏的框架類、系統類指的是在系統整個運行過程當中始終存在的對象類,如iView主框架的相關類)

    規則1.7.1.2:當使用本身定義的類裝載器去裝載類時,在被裝載的類再也不使用後,須要保證該類裝載器能夠被垃圾回收

    建議1.7.1.3:儘可能不要將一些大的對象(對象自己比較大或其引用的對象比較多)定義成靜態的

    規則1.7.1.4:若是在一個對象中建立了一個線程,當對象再也不使用時,必須關閉該線程

    建議1.7.1.5:在JFrame、JDialog等窗口對象中,儘可能處理窗口關閉事件並釋放資源

    規則1.7.1.6:在IO操做中,必須定義finally代碼段,並在該代碼段中執行IO關閉操做

     

    1. 案例研究

      1. 裝載的適配類不能卸載致使代碼區溢出

    在某OM系統中,整個系統代碼分爲平臺部分和適配部分。對於適配部分的代碼,平臺框架會經過一個自定義的類裝載器實例DynClassLoader進行裝載。可是當用戶註銷系統回到登陸界面後,因爲系統仍然保持對DynClassLoader實例的引用,致使全部經過DynClassLoader實例裝載的適配類都不能卸載掉。這樣產生的結果是,當用戶從新登陸到第2個、第3個適配版本時,因爲所裝載的適配類所有都不能卸載,使得JVM代碼區的增加超越了設定的上限值,發生內存溢出。關於保持ClassLoader引用會致使全部被該ClassLoader加載的類都不能卸載的緣由,咱們能夠分析一下jdk的ClassLoader代碼,如下是部分代碼片段:

     

    public abstract class ClassLoader {

     

    private static native void registerNatives();

    static {

    registerNatives();

    }

     

    // If initialization succeed this is set to true and security checks will

    // succeed. Otherwise the object is not initialized and the object is

    // useless.

    private boolean initialized = false;

     

    // The parent class loader for delegation

    private ClassLoader parent;

     

    // Hashtable that maps packages to certs

    private Hashtable package2certs = new Hashtable(11);

     

    // Shared among all packages with unsigned classes

    java.security.cert.Certificate[] nocerts;

     

    // The classes loaded by this class loader. The only purpose of this table

    // is to keep the classes from being GC'ed until the loader is GC'ed.

    private Vector classes = new Vector();

     

    // The initiating protection domains for all classes loaded by this loader

    private Set domains = new HashSet();

     

    // Invoked by the VM to record every loaded class with this loader.

    void addClass(Class c) {

    classes.addElement(c);

    }

    ……

  21. JDKClassLoader代碼片段

    從上面的代碼說明咱們能夠知道,只要類裝載器不被垃圾回收掉,則被該類裝載器裝載的全部類都不會被卸載掉。

     

    1. 設計性能規範

      1. 事件派發線程使用規則

    Java的設計目標是靈活、易用和平臺一致性。出於這一目的,在UI設計方面,java將界面的繪製和事件處理統一放在了一個獨立的線程中進行,這個線程就是事件派發線程。因爲事件派發線程只有一個,而且負責了關鍵的界面繪製和界面事件處理,因此若是該線程被阻塞或者處理的業務邏輯太重的話,會致使整個系統響應很慢、甚至發生灰屏現象。這對用戶來講,就是嚴重的性能問題。因此在系統的設計開發中,對派發線程的使用必須格外謹慎。

    1. 規則描述

    規則2.1.1.1:對於非界面的業務邏輯,應放在事件派發線程以外處理,保證事件派發線程處理的邏輯儘量的少;避免在派發線程中執行時間較長、或執行時間具備較大不肯定性(如訪問遠程服務器)的業務邏輯

    建議2.1.1.2:對於高頻度的界面更新事件,最好採用批量定時更新方式代替實時更新方式

     

    1. 案例研究

      1. 跟蹤上報消息表格採用定時更新

    在某OM系統中,出於性能優化,對跟蹤上報消息顯示功能採用了定時刷新機制來批量更新表格數據。當上報消息解析好後,直接將其添加到表格模型中,但不觸發模型更新事件(也即在添加數據時,不調用fireTableRowsInserted、fireTableRowsDeleted等方法)。模型更新事件統一放在一個javax.swing.Timer裏面定時進行觸發(每300毫秒觸發一次)。跟蹤上報消息表格定時更新的處理流程以下圖:

  22. 跟蹤消息上報表格定時刷新處理流程

     

    1. 將非界面處理邏輯移到派發線程以外

    在某OM系統中,有一個上報消息處理業務功能。在優化前,該功能實現將大量業務放到了派發線程中處理(主要有從緩衝區取消息、保存消息到文件、解析消息碼流、將解析結果添加到表格、定時刷新表格界面),其流程實現以下圖:

  23. 優化前的上報消息處理流程

    基於讓派發線程處理儘量少的業務的原則,優化後,經過新增一個業務處理線程,並在該業務處理線程和派發線程之間添加一個表格模型緩衝區的方式,較好的實現了將大部分業務移到派發線程以外處理,最後的結果是派發線程只須要定時刷新表格界面就能夠了。優化後的上報消息處理流程以下圖:

  24. 優化後的上報消息處理流程

    咱們在實際開發中,常常會遇到這樣一個問題:在一個連續的業務處理過程當中,如何將非界面處理的業務邏輯隔離到派發線程以外?根據設計經驗,基本能夠得出這樣一個結論:要想使兩個線程協調工做,必須有一個可操做的共享數據區或對象。在上面的優化案例中,咱們定義了一個表格模型緩衝區來使業務處理線程和派發線程協調工做。咱們還能夠調用javax.swing.SwingUtilities類的invokeAndWait和invokeLater方法,在業務處理線程環境下將一些界面處理邏輯添加到派發線程中進行處理(在這種狀況下,事件派發隊列就是業務處理線程和派發線程之間的共享數據區)。

     

    1. 界面組件設計規則

    在Swing中,全部輕型(lightweight)組件都是經過java的繪圖工具繪製出來的。每一個輕型組件都有本身的paint()方法(默認的paint方法是從JComponent類中繼承過來的)。當顯示組件時,由派發線程調用頂層容器的paint()方法將容器及容器裏面的全部子組件繪製出來。從性能角度講,Swing的界面繪製機制存在如下問題:

    1. 整個界面繪製牽涉到的層次太多(每從新繪製一次界面,有可能要調用上千個方法)。
    2. 在一次界面的繪製過程當中,容易產生大量的臨時對象。能夠經過測試得出,顯示一個JPanel對象(裏面只有一個JLabel對象)會產生7個SunGraphics2D對象。之後每刷新一次界面,就會有7個SunGraphics2D臨時對象產生,而每一個SunGraphics2D對象會佔用192個字節。在一個實際應用系統中,若是表格、樹等界面頻繁刷新的話,每1秒中就有可能產生幾兆的臨時對象。

    Swing輕型組件的繪製流程以下圖:

  25. Swing輕型組件的繪製流程

    從上面的流程圖能夠看出,Swing組件的繪製是一個層層往下的過程,組件首先繪製本身,若是有border則再繪製出border,而後繪製本身的子組件;對於子組件來講,首先繪製本身,而後再又繪製本身的子組件;每一個組件在繪製本身時,須要進行相關的繪圖區域範圍計算。另外,須要指出的是,在java體系中,字符串的顯示也是經過java本身的繪製機制繪製出來的。因此,在字符串的顯示過程當中,也會建立不少臨時對象。

    經過上面的分析,從性能角度上講,Swing的實現並無想象的那麼好。所以在特殊的應用場景下,爲了提升咱們系統的性能,咱們須要,也有必要根據業務處理特色,定義本身的界面繪製機制,甚至開發特定的界面組件。對於像JTable,JTree這樣的界面對象,經過定製和優化,能夠極大的提升其界面繪製的性能。

     

    1. 規則描述

    建議2.2.1.1:爲了提升系統性能,能夠根據業務處理特色,定義本身的界面繪製機制

    建議2.2.1.2:爲了提升系統性能,能夠根據業務處理特色,開發本身的界面組件

     

    1. 案例研究

      1. 實時跟蹤上報消息採用優化表格進行顯示

    在某OM系統中,對實時跟蹤模塊,早期的跟蹤消息表格繪製方式採用的是JDK默認繪製方式,經過BasicTableUI對象實現表中單元格的繪製。JDK的這種方式具備共用性,提供的功能也很是多,但存在的問題是每繪製1個單元格,都要至少拷貝1個Graphics2D臨時對象,同時要調用表格中的swing組件的paint()進行組件的繪製,在單元格組件的繪製過程當中,又建立了Graphics2D臨時對象。

    在後期的性能優化工做中,已經將實時跟蹤上報消息採用優化表格進行顯示。具體優化方法及步驟以下:

    1. 參考JDK的BasicTableUI類,本身開發一個TextOptimizeTableUI類,該類繼承TableUI類。TextOptimizeTableUI類基本和BasicTableUI類相同,區別僅在於TextOptimizeTableUI繪製單元格時是直接調用Graphics2D對象繪製字符串或字符數組,而不是將單元格看成一個Swing組件,調用其paint()方法進行組件的自我繪製。
    2. 爲了配合TextOptimizeTableUI實現繪製,須要定義一個TextOptimizeRenderer類。TextOptimizeRenderer類並不繼承JLabel等組件對象,它只負責傳輸要繪製的數據給TextOptimizeTableUI類。
    3. 客戶程序在使用優化表格時,須要作兩件事情:1)構建TextOptimizeTableUI對象,經過JTable的public void setUI(TableUI  ui)方法設置該對象;2)繼承TextOptimizeRenderer類,重寫public TextOptimizeLabel getOptimizeLabel(JTable table, int row, int column)接口方法,實現本身的renderer,並設置到表格對象中。

    採用優化表格後,表格界面的繪製流程以下:

  26. 優化表格的界面繪製流程

     

    在實際開發中,通常有以下兩種方法能夠定義本身的界面繪製機制:

    1. 編寫本身的UI類,實現組件的具體繪製。這種方法具備較好的通用性,不影響Swing組件的框架結構,實現起來也較爲容易。但缺點是沒有達到最佳的界面性能優化,由於在組件繪製過程當中,仍是通過了不少中間層纔到UI類裏面。這種方法適合比較複雜的界面組件的優化,如JTable、JTree組件。
    2. 重載組件的paint()或paintComponent()方法。這種方法能夠達到最佳的界面性能優化,但對比較複雜的組件,徹底本身實現繪製過程很難,也容易引入不少未知疑難問題。因此該方法比較適合比較簡單的組件,如JPanel、JLabel組件。

    在這裏須要指出的是,只有在充分研究分析了應用場景後,才能嘗試採用定義本身的界面繪製機制來提升系統性能。通常狀況下不建議改變Swing組件已有的繪製機制,一來出於工做量的考慮,二來出於通用性的考慮,再者也避免引入一些未知問題。

     

    1. 開發本身的界面組件:JEdit VS. JTextArea

    在某OM系統中,採用JDK的JTextArea類實現批處理文件的打開、編輯和保存等功能。在使用過程當中發現,當打開比較大的批處理文件(通常是幾兆大小的文本文件)時,常常會致使系統灰屏、CPU佔用率100%,甚至內存溢出現象。通過對JTextArea組件作性能測試,發現當打開比較大的文件時,JTextArea存在臨時對象建立過多和內存佔用過大的問題。如下是JTextArea組件和UltraEdit程序打開一樣大小文件(文件大小爲3.95M,共376792行)時的內存增量數據:

    打開方式

    物理內存增量(M)

    虛擬內存增量(M)

    JTextArea組件

    52.4 

    53.3 

    UltraEdit程序

    3.44 

    1.59 

  27. JTextArea組件和UltralEdit程序打開文件時的內存增量狀況

    經過上面的內存增量數據能夠看出,JDK的JTextArea組件打開4M左右的文件須要佔用50M左右的內存,這在實際應用中是很難知足要求的,和UltralEdit程序比起來,性能要差一個數量級以上。

    JTextArea組件打開文件時內存佔用過大的主要緣由在於其文檔模型(JTextArea使用PlainDocument對象來存儲文本數據和文本結構信息)。總的來看,JTextArea的文檔模型PlainDocument(其實Swing的全部Document對象都存在該問題)具備如下性能問題:

    1. 文本數據用一個GapContent對象來表示(具體文本內容用一個char數組來存儲),而且對每個文本行,都會生成一個javax.swing.text.GapContent$MarkData對象和一個javax.swing.text.GapConent$StickyPosition對象。在上面的性能測試中,文件中有376792行數據,那麼將會生成376792個GapContent$MarkData對象和376792個GapConent$StickyPosition對象。
    2. 文本結構信息用一個Element對象樹來表示。每個文本行用一個Element子節點表示,全部的文本行都在一個Elment根節點下。一樣在上面的性能測試中,將會生成376793個Element對象。
    3. 不可以將一個現有的char數組賦給PainDocument對象,用來做爲文本內容。JTextArea幾乎只能接受String類型的內容。這樣致使的結果是,若是要將一個文本文件中的內容顯示到JTextArea中,那麼一般的作法是首先須要將文本文件的內容以StringBuffer的形式讀出來,而後將StringBuffer轉化爲String對象添加到JTextArea中,在JTextArea的Content中又將String對象轉化爲一個char數組,而後將char數組內容拷貝進來,最終顯示到界面上。能夠看出在這個過程當中,生成了大量的臨時對象。

    幸運的是,在java開源項目Jedit(注意,Jedit聽從GPL協議,不能商用)中,提供了比JTextArea性能好得多的文本編輯組件。爲了解決性能問題,咱們借鑑Jedit文本組件的設計思路,開發了本身的文本編輯組件。該組件相比JTextArea具備如下優勢:

    1. 文本數據只須要用一個char數組來存儲便可,不會針對每個文本行產生一個附加的對象。
    2. 文本結構信息用一個int數組來表示便可。其基於的原理是,系統只須要保存每個文本行的結束位置便可完整記錄整個文本的結構信息。
    3. 定義了單獨的loadFile方法,能夠將文本文件的內容直接讀入到文本編輯組件的內容存儲區中,減小了大量沒必要要的中間環節。

    如下是咱們本身開發的文本編輯組件和JTextArea組件的性能對比數據(將組件都放在一個JFrame中,而後讀取一個1.91M的文本文件,共49431行):

    打開方式

    GC後的OLD區內存佔用狀況(M)

    代碼區內存佔用狀況(M)

    使用JTextArea

    12.653 

    4.686

    使用本身開發的組件

    4.464 

    4.160 

  28. 本身開發的文本組件和JTextArea組件的性能對比數據

     

    1. 業務流程設計規則

    Java語言不像C++語言同樣,能夠在棧上建立對象,隨着函數調用完後對象可以自動被釋放;另外也不能對new出來的對象進行delete。Java語言的這些限制致使了臨時對象問題的存在。咱們在業務流程的設計實現中,從性能上考慮應儘可能保證流程處理的精簡和高效,不然很容易產生臨時對象的問題。

    1. 規則描述

    建議2.3.1.1:對於一些關鍵的業務處理流程,須要儘可能減小中間處理環節,從而避免建立沒必要要的臨時對象

    建議2.3.1.2:在一些關鍵的業務處理流程中,對於必需要用到的對象,能夠採起重用對象機制,重複利用,避免每次都建立

    建議2.3.1.3:對於大多數業務處理來講,臨時對象都不是問題。只有對那些高頻度或大數據量業務處理操做來講,而且經過性能測試證實的確是臨時對象引發了性能問題,才須要進行臨時對象的優化。

     

    1. 案例研究

      1. 跟蹤消息碼流解析經過偏移量方式來進行

    在某OM系統的跟蹤模塊中,須要對實時上報的跟蹤消息碼流進行解析,而後將解析結果顯示到界面表格中。跟蹤消息碼流的解析過程爲:首先從handleMessage方法中接受要解析的消息碼流包(一個大的byte數組,約6K),而後將該消息碼流包分解成具體的跟蹤消息幀;對每個跟蹤消息幀,先解析出消息頭,並根據消息頭裏面的信息校驗消息長度的正確性,而後根據消息頭信息獲取消息體的碼流,最後對消息體的碼流進行解析,根據系統定義顯示相關結果字符串到界面表格中。

    在早期的跟蹤消息碼流解析處理流程中,會產生大量的臨時byte數組對象:把一個消息包分解成消息幀,須要建立許多消息幀byte數組;對消息幀的消息頭進行解析,又須要建立一個消息頭的byte數組;再對消息幀中的消息體進行解析前,又須要建立一個消息體的byte數組。具體流程以下圖所示:

  29. 優化前的跟蹤消息碼流解析處理流程

    在後期的跟蹤模塊性能優化中,針對跟蹤消息碼流的解析流程進行了優化。優化的主要思路是:在消息碼流的整個解析過程當中,再也不建立新的byte數組,而是重用消息碼流包的byte數組;在解析時,經過消息的偏移量定義,從byte數組中取出要解析的數據。具體流程以下圖所示:

  30. 優化後的跟蹤消息碼流解析處理流程

     

    1. 告警瀏覽上報消息解析採用重用對象方式來進行

    在告警瀏覽上報消息處理中,採用相似對象池的方式來重用對象,以減小臨時對象的建立。告警瀏覽上報消息的解析流程以下:

  31. 告警瀏覽上報消息的解析流程

    從上面的流程圖能夠看出,告警瀏覽模塊主要經過ObjPool來重用AlarmRecord對象。每次當須要一個AlarmRecord對象時,都從ObjPool裏面取,若是沒有才新建一個AlarmRecord對象;當AlarmRecord對象再也不使用時,將其從新放到ObjPool中。

     

    1. 界面響應設計規則

    對於桌面客戶端系統來講,界面性能是用戶所關注的一項重要內容。通常界面性能應該包括:界面響應速度(這裏特指界面的建立和顯示時間)、界面刷新速度、界面友好性(這裏特指進度條、鼠標置忙等響應措施)。提升系統界面性能的主要方法包括:

    1. 重用已建立的界面組件,每次數據發生變化時,只更新數據模型,再也不從新建立界面組件。例如,消息詳細解釋窗口就是採用的這種機制,只有在第1次雙擊跟蹤消息時,纔會建立消息詳細解釋窗口,後面再次雙擊跟蹤消息時,只是更新窗口中各界面組件的數據模型就好了。
    2. 經過預加載class文件和保持界面組件提升對話框的彈出速度。一般在網管系統中,因爲對話框比較多,通常都考慮作成通用對話框的模式,藉助配置文件來生成對話框。這種策略的一個問題就是建立和彈出對話框的速度比較慢,其緣由有:加載的類比較多、讀取配置文件須要時間和中間計算環節比較多。針對這種問題的解決方法通常是在系統空閒時間預先作一些前期處理,如預加載class文件、提早讀取配置信息等。在第1次彈出對話框後,後面一直保持對話框引用,以便之後再次彈出。
    3. 提升界面的感知性能,加強界面的友好性。若是一個操做執行須要較長時間(3秒以上),那麼最好彈出明確的進度條提示界面。若是操做執行時間大於1秒,上限在3秒左右,那麼能夠給出非顯要的界面提示,如在窗口的狀態欄給出相關提示。若是估計操做執行須要很長時間時,如大於60秒,則要在執行操做以前就彈出提示選擇界面,讓用戶選擇是否真要執行該操做。彈出提示選擇界面能夠及時給用戶一個反饋,會讓用戶感受須要的時間長是應該的,在這種狀況下他也就不會過多的埋怨系統慢了。另外若是系統啓動較慢(如點擊圖標後,3秒內沒有反應),須要增長顯示啓動畫面。若是系統須要顯示的數據不少(如須要分多屏才能顯示完),那麼能夠採用獲取一部分結果,就立刻顯示一部分結果的方式來縮短響應時間,而不要等到獲取所有數據後,再才顯示出全部結果。通常網頁瀏覽採用的都是這種方式。

     

    1. 規則描述

    建議2.4.1.1:對於用戶頻繁進行開啓、關閉的窗口組件,須要儘可能採起重用機制,用界面隱藏代替界面關閉

    建議2.4.1.2:若是一個操做須要很長時間(如大於60秒),則要在執行操做以前就彈出提示選擇界面,讓用戶選擇是否真要執行該操做

    建議2.4.1.3:若是一個操做須要較長時間(如大於3秒),則最好彈出明確的進度條提示界面

    建議2.4.1.4:若是一個操做比通常操做耗時較長(如大於1秒),那麼能夠給出非顯要的界面提示(如在窗口的狀態欄給出相關提示)

    1. 案例研究

      1. 跟蹤消息詳細解釋窗口採用界面組件重用機制提升彈出速度

    在某OM系統中,消息詳細解釋功能是用戶使用很是頻繁的一個功能。在早期版本的消息詳細解釋子模塊中,每次用戶雙擊一條跟蹤消息時,都會建立整個窗口界面,而後顯示給用戶。這種實現方式使得每次彈出窗口都須要3秒左右,響應速度比較慢。在後期版本改進中,對整個消息詳細解釋模塊進行了重構和優化。優化後,只有第1次彈窗口時,纔會建立界面組件,後面再次彈窗口時,只是更新一下數據模型就能夠了。優化後的窗口彈出速度只須要1秒左右,響應速度明顯提升。優化後的消息詳細解釋窗口顯示流程以下圖:

  32. 優化後的消息詳細解釋窗口顯示流程

     

    1. 系統抗負載能力設計規則

    系統負載能力指的是在應用許可的任務負載下,系統的性能是否知足客戶要求。這裏的系統性能主要包括:在可能的極限負載下,系統是否能夠保持正常運行,不發生崩潰或內存溢出等現象,而且界面能保持響應。從性能角度講,通常隨着執行任務的增長,系統響應時間應該是逐漸增長緩慢,而不是成指數增長的。下圖左圖是成指數增長的響應時間,右圖是平緩增長的響應時間:

    成指數增長的響應時間 平緩增長的響應時間

  33. 系統負載與響應時間關係圖

     

    1. 規則描述

    規則2.5.1.1:若是系統須要運行動態變化的負載,那麼須要保證在可能的極限負載下,系統能夠正常運行

     

    1. 案例研究

      1. 跟蹤採用流控方式保證系統不會發生過載

    在某OM系統中,存在一個跟蹤消息上報的處理流程。當網元的跟蹤消息上報流量太大時,容易致使系統性能嚴重降低,甚至發生灰屏、界面不響應的狀況。爲此,在跟蹤消息處理流程中增長了一個流控機制,該流控機制能夠根據系統資源可用狀況,分級採起不一樣的流控措施,當系統資源充足時,已發生的流控又可自行回覆。流控機制的具體實施方案以下:

    流控方案採用三級流控方式:1)顯示流控:下降界面的刷新頻率,2)存盤流控:消息只存盤不解析碼流,也不顯示在界面,3)過載流控:將當前流控的跟蹤任務的消息緩存清空。具體表現爲:當網元大量上報消息致使CPU使用率持續超過系統指定處理能力上限閾值(如95%)的時候,開始進入顯示流控;進入顯示流控後若是CPU使用率仍然持續超過系統指定處理能力上限閾值而且上報消息的速率持續大於系統指定的基線值(如40條/秒)的時候,開始進入存盤流控;進入存盤流控後若是CPU使用率仍然持續超過系統指定處理能力上限閾值而且客戶端進程的CPU使用率大於45%的時候, 開始進入過載流控。流控的順序是按照:正常情況->顯示流控->存盤流控->過載流控方向進行流控;恢復的時候則是按照流控的逆過程: 恢復過載流控->恢復存盤流控->恢復顯示流控->正常情況方向進行恢復。整個流控機制的流程圖以下:

  34. 跟蹤流控機制的流程圖

     

    1. 多線程設計規則

    對於須要較長時間執行的業務處理,能夠考慮分爲多個相對獨立的併發處理業務,採用多線程機制以縮短總的業務執行時間。如當用戶雙擊一個很大的tmf文件時,能夠在顯示系統界面的過程當中,同時用另外一個線程去解析tmf文件,準備好要顯示的數據。這樣從雙擊文件到顯示出最終數據的時間就會縮短。這種優化方法簡單的說就是:將串行工做變爲並行工做,以縮短整體工做時間;或者是利用系統空閒時間,作一些前期輔助工做,以縮短界面響應時間。

    採用多線程機制提升業務處理速度時,其前提是要能夠將業務處理劃分爲幾個相對獨立的處理邏輯,而後要麼併發執行這些獨立的處理邏輯,要麼將一部分處理邏輯提早到系統空閒時間中執行。另一個問題是要控制好線程間的同步問題(能夠經過調用wait方法或join方法來實現這一點)和相關資源的釋放問題。

    1. 規則描述

    建議2.6.1.1:對於須要較長時間執行的業務處理,能夠考慮採用多線程機制將業務處理劃分爲幾個相對獨立的處理邏輯併發執行,或者將一部分處理邏輯提早或延後到系統空閒時間中執行,以縮短總的業務執行時間

     

    1. 案例研究

      1. 跟蹤消息體碼流過濾採用單獨線程收集碼流數據

    在某OM系統中,有一個跟蹤消息體碼流過濾功能,其要求的業務處理流程以下:

    1. 用戶經過界面輸入要查找的十六進制字符串;
    2. 系統對跟蹤回顧界面上的每一條消息,調用外部接口進行解析,獲取解析後的碼流消息,而後將該消息轉換爲十六進制字符串;
    3. 系統將碼流的十六進制字符串與用戶輸入的十六進制字符串進行比較,若是後者包含前者,則跟蹤回顧界面上的該條消息爲要過濾的消息;
    4. 上面的過濾操做執行完後,系統將全部的要過濾的消息更新到界面上顯示;

    根據要求的業務處理流程,優化前代碼處理邏輯以下圖:

  35. 優化前的跟蹤過濾代碼處理邏輯

    採用上面優化前的處理邏輯,當跟蹤回顧界面中有50000條消息時,執行過濾所花的時間通常須要10秒左右。爲了解決執行過濾所花時間過長的問題,對跟蹤消息體過濾代碼進行了優化,主要的優化思路是採用一個單獨線程來收集碼流數據,優化後的代碼處理邏輯以下圖:

  36. 優化後的跟蹤過濾代碼處理邏輯

    採用上面優化後的處理邏輯,當跟蹤回顧界面中有50000條消息時,執行過濾所花的時間能夠由原來的10秒左右降爲3秒左右。

     

    1. 附錄A:安裝盤壓縮

      1. 背景介紹

    在Java應用系統開發完畢後,須要對全部程序文件進行打包,製做成安裝盤供用戶安裝使用。安裝盤的大小和系統的安裝時間也是用戶比較關注的性能問題。安裝盤越小,用戶從網絡上下載安裝盤所需時間就越短;一樣,安裝時間越短,用戶就能夠在安裝過程當中不用長時間的等待。所以,咱們在製做安裝盤的時候,通常都要採用相關的壓縮算法來對要發佈的程序文件進行壓縮,而不是簡單的打包。對基於java開發的OM系統來講,其程序文件中通常既包括了大量的java class文件、資源文件、也包括大量的DLL文件,爲了使壓縮後的安裝盤儘量的小,咱們須要針對不一樣的文件類型,採用不一樣的壓縮格式。針對java程序文件(以jar格式存在),一種最優的壓縮格式是pack200壓縮格式,而針對其它文件,一種很是高效的壓縮格式是7z壓縮格式。

    對基於java開發的OM系統來講,在實際應用中,能夠先對每一個jar文件採用pack200方式進行壓縮,而後對全部文件進行7z格式的壓縮,實踐證實,這種混合壓縮方式製做的安裝盤壓縮比是很是優的。

    1. Pack200壓縮格式介紹

    Pack壓縮格式最初是SUN公司爲了減少JRE(J2SE v1.4.1 and J2SE1.4.2)安裝盤大小而設計開發的。Pack壓縮格式是JSR200項目,在JDK1.5中已提供實現。

    當前咱們廣泛使用的JAR壓縮方式,是在字節層面對class文件進行的壓縮。Pack壓縮格式是在JAR壓縮方式之上的二次壓縮,它將對JAR裏面的class文件和資源文件進行統一組織,同時去掉那些重複的共享數據結構。Pack壓縮格式對jar文件的壓縮很是高效,通常它能夠將jar文件壓縮到原來的1/7到1/9大小。

    Pack壓縮格式的java實如今jdk1.5中已提供,能夠經過java.util.jar.pack200工具類進行使用。關於Pack壓縮格式的詳細信息能夠從如下地址獲取:

    http://jcp.org/en/jsr/detail?id=200

    jdk1.5 API:java.util.jar.pack200;

    jdk1.5的bin目錄下有pack200.exe和unpack200.exe工具程序,能夠經過命令行實現對jar文件的打包和解包。

     

    1. 7z壓縮格式介紹

    7z是一種新的壓縮格式(聽從LGPL協議,能夠商用),它擁有目前最高的壓縮比。7z格式的主要特徵有:

    1. 公開的結構編輯功能
    2. 最高的壓縮比
    3. 強大的AES-256加密
    4. 可更改和配置壓縮的算法
    5. 最高支持16000000000 GB 的文件壓縮
    6. 以Unicode 爲標準的文件名
    7. 支持固實壓縮
    8. 支持檔案的文件頭壓縮

    LZMA 算法是 7z 格式的默認標準算法。LZMA 算法的主要特徵有:

    1. 高壓縮比
    2. 可變字典大小(最大 4 GB)
    3. 壓縮速度:運行於 2 GHz 的處理器可達到 1 MB/秒
    4. 解壓縮速度:運行於 2 GHz 的處理器可達到 10-20 MB/秒
    5. 較小的解壓縮內存需求(依賴於字典大小)
    6. 較小的解壓縮代碼:約 5 KB
    7. 支持 Pentium 4 的多線程(Hyper-Threading)技術及多處理器

    目前支持7z格式的壓縮軟件有:7-Zip、WinRAR、PowerArchiver、TUGZip、IZArc。關於LZMA壓縮算法的實現,當前已經有多個語言版本的軟件開發工具包及源代碼可供下載,包括:C,C++,C#,Java。關於7z和LZMA的詳細資料能夠從如下網址獲取:

    http://www.7-zip.org/zh-cn/7z.html

    http://www.7-zip.org/zh-cn/sdk.html

    1. 附錄B:性能測試專題

      1. 背景介紹

    在對系統進行性能優化的過程當中,性能測試工做起着相當重要的做用。一方面,在性能優化前,咱們須要經過性能測試找出系統的性能瓶頸所在,作到有目的的優化;另外一方面,咱們在對系統作了一個性能優化方案後,仍然須要經過性能測試來驗證方案的優化效果,對於沒有明顯性能優化效果的方案,咱們通常是不建議給予實施的。

    性能測試是一個比較具體化的工做,須要具體問題具體分析。總的來說,在性能測試過程當中都將面臨兩個問題:一個是性能測試工具的選擇和使用,另一個是性能測試方法即如何設計測試用例和分析測試數據的問題。對於java應用系統來講,目前有多種性能測試工具可供使用,下面將對這些工具一一作一個簡要的介紹。不一樣的性能測試工具所關注的性能測試點是不同的,因此咱們在性能測試過程當中,須要綜合利用這些工具,從不一樣的關注點來對一個系統性能作出全面的評估。另外,下面也將對一些性能測試方法作一個簡要的說明和介紹。

    1. 經常使用的java性能測試工具

      1. Profiler工具介紹

    目前,網絡上有各類各樣的profiler工具,通常最經常使用的是Borland公司的Optimizeit套件。經過Borland公司的profiler工具主要能夠作如下事情:

    1. 檢測和定位系統的內存泄漏狀況;
    2. 觀察java系統堆內存的實時變化狀況;
    3. 分析系統運行過程當中的對象建立狀況;
    4. 觀察java系統類加載實時變化狀況;

    Profiler工具的運行界面以下圖:

  37. Profiler工具的運行界面

     

    1. Visualgc工具介紹

    Visualgc工具是sun公司開發的一個免費的性能測試工具,能夠從sun公司網站上下載。經過visualgc工具主要能夠作如下事情:

    1. 實時查看java系統中的Young區、Old區、Perm區的大小配置狀況和內存使用狀況;
    2. 查看java系統運行過程當中類裝載的數目及類加載的時間;
    3. 查看java系統運行過程當中GC收集的次數及GC所耗時間;

    Visualgc工具的運行界面以下圖:

  38. Visualgc工具的運行界面

     

    1. GC日誌分析工具介紹

    打印和分析GC日誌,是對java系統進行性能測試的一個重要手段。對sun公司的hotspot虛擬機來講,能夠添加相似以下的JVM參數來打印GC日誌信息:

    "-verbose:gc –XX +PrintGCDetails –Xloggc:c:\gclog\log.txt"

    打印出GC日誌後,能夠經過GC日誌分析工具來進行分析,如今網絡上有諸如GCViewer之類的免費工具可供使用,固然也可直接查看和分析GC日誌數據。經過分析GC日誌,能夠作以下事情:

    1. 經過young區的內存變化狀況,來分析系統臨時對象的建立速度狀況;
    2. 分析出垃圾收集的頻率以及對系統帶來的性能影響;
    3. 分析出young區、old區、perm區的內存擴充和收縮狀況;

     

    1. Windows性能檢視器介紹

    Windows性能檢視器是windows操做系統自帶的一個性能測試工具。經過windows性能檢視器,能夠記錄和檢測進程或整個操做系統的資源使用狀況。在windows性能檢視器中,包含大量的性能計數器,下表列出了經常使用的性能計數器名:

    計數器名

    類別

    說明

    等價的任務管理器功能

    Working Set

    Process

    駐留集,當前在實際內存中有多少頁面

    Mem Usage

    Private Bytes

    Process  

    分配的私有虛擬內存總數,即提交的內存

    VM Size 

    Virtual Bytes

    Process

    虛擬地址空間的整體大小,包括共享頁面。由於包含保留的內存,可能比前兩個值大不少

    Page Faults / sec(每秒鐘內的頁面錯誤數)

    Process 

    每秒中出現的平均頁面錯誤數

    連接到 Page Faults(頁面錯誤),顯示頁面錯誤總數

    Committed Bytes(提交的字節數)

    Memory 

    "提交"狀態的虛擬內存總字節數

    Commit Charge:total

    Processor Time 

    Processor 

    進程的CPU佔用率

    CPU Usage 

  39. 經常使用性能計數器列表

     

    性能檢視器的配置界面以下:

  40. 性能檢視器配置界面

     

    1. 如何分析性能測試數據

      1. 檢查測試數據的真實性

    在對測試數據進行正式分析前,首先須要檢查測試數據是否真實可靠。測試時獲得了不可靠的測試數據的主要緣由有:

    1. 在進行測試時,忽略了某一測試操做。如在進行性能測試時,原本測試要求是要開啓10個監控任務,但實際操做時卻只開啓了2個。
    2. 在進行測試時,測試環境發生了變化,和測試用例中規定的測試環境不符。如測試用例中規定跟蹤上報速度是35條/秒,但在測試時,其它人員將測試環境作了更改,將跟蹤上報速度改成了100條/秒。
    3. 測試時沒有考慮操做系統及其它軟件的影響。如在測試某java系統第1次啓動時的持續時間時,尚未等到操做系統徹底穩定下來,就開始了測試。再好比,在測試某java系統的CPU佔用率時,沒有遵照測試環境的要求,無心中開啓了其它的軟件,或者開啓了操做系統中比較特殊的服務等。

    致使測試數據失真的緣由是很是多的,因此在分析測試數據以前須要檢查測試數據的真實性和可靠性。檢查的方法通常有:

    1. 和之前的經驗數據進行比較,若是差異很大,應該考慮從新測試。
    2. 經過對業務代碼進行理論分析,得出一個經驗數據,若是測試數據和經驗數據差異很大,應該考慮從新測試。
    3. 在條件容許的狀況下,儘可能採用屢次測試,去掉誤差較大的測試數據。
    1. 經過Excel對測試數據進行處理

    在數據量不大的狀況下,經過Excel工具進行數據處理是一個比較好的方法(Excel最多支持65536行,256列)。經過Excel工具處理數據的經常使用方法有:

    1. 圖表法。將原始數據繪製成折線圖、數據趨勢線、數據擬合線來進行分析。
    2. 有效值法。對原始數據求平均值、最大值、最小值來進行分析。
    3. 公式法。經過在Excel工具裏面添加特定的公式,對原始數據進行分析。
    1. 分析測試數據的關注點

    性能測試的一個難點就是如何對測試數據進行分析,並找出各類測試結果產生的緣由。對測試數據進行分析時通常關注如下幾個方面:

    1. 查看測試數據是否出現有規律的變化,若是有則分析產生這種規律的緣由。
    2. 查看測試數據是否存在拐點,若是有則分析出現拐點的緣由。
    3. 將不一樣版本的測試數據進行對比,查看數據的變化狀況,分析產生這種變化的緣由。
    1. 對測試項進行理論評估和公式推導

    經過設計和執行測試用例來檢測系統性能是最直接,也是最可靠的方式。但在實際操做中,若是對全部的狀況都進行性能測試,每每工做量是巨大的,而且是得不償失的。好比測試實時跟蹤的一個性能優化方案的效果,首先在上報速度爲200條/秒的狀況下進行測試,發現優化效果明顯。可是立刻你們會存在疑惑:那麼在上報速度爲100條/秒、50條/秒、10條/秒等狀況下,這種優化效果是否依然存在呢?換句話說,若是效果不是很明顯,那麼咱們是否還有優化的必要呢?若是咱們爲了回答這些疑問,針對全部這些狀況都進行測試的話,其工做量將是很是大的。

    實際上,只要改變測試用例中的任何一個測試條件,都將產生一個新的測試用例。所以咱們不可能對全部延伸出來的測試用例都進行測試。解決這個問題的辦法應該採起理論和實踐相結合的方式,首先經過基本測試用例獲得幾組測試數據,而後根據這些測試數據進行理論評估和公式推導,最後根據推導的公式給出當測試條件變化時的預期結果。

    進行理論公式推導的方法是,首先根據業務代碼創建起數據模型,而後將現有的測試數據代入數據模型中,得出可求解的公式。

     

    1. 參考文獻

    序號No.

    文獻編號或出處 Doc No.

    文獻名稱 Doc Title

    1

    機械工業出版社,2003

    Effective Java 中文版

    3

    http://java.sun.com/

    Java Platform Performance: Strategies and Tactics

    4

    O'reilly & Associates, 2001

    Java Performance Tuning

    5 

    http://trove4j.sourceforge.net/

    Trove集合類

    6

    IBM公司的developerworks 中國網站

    性能觀察: Trove 集合類

    7 

    http://www.jedit.org/

    Jedit源代碼

    8 

    http://jcp.org/en/jsr/detail?id=200

    Pack200資料

    9 

    http://www.7-zip.org/zh-cn/7z.html

    7z壓縮格式資料

相關文章
相關標籤/搜索