目 錄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
表2 JTextArea組件和UltralEdit程序打開文件時的內存增量狀況 39
表3 本身開發的文本組件和JTextArea組件的性能對比數據 40
圖目錄 List of Figures
Java編程性能規範
範 圍Scope:
本規範規定了在基於程序性能考慮的狀況下,java語言的系統編碼和系統設計規則。
本規範適用於使用Java語言編程的部門和產品。
簡 介Brief introduction:
本規範從如何提升java系統性能的角度,給出了系統設計和編碼時的重要關注點。在本規範中,針對每個關注點,都從概述、規則描述和案例研究三個方面來展開描述。本規範中的規則描述都是從實際的優化案例和技術研究中提煉出來的,能夠供java編程人員在系統設計和編碼時做爲意見參考。值得一提的是,本文中列舉了大量的實際優化案例(基於JDK1.4環境),這些案例對於java系統的性能優化有着比較高的借鑑價值,能夠供開發人員在性能優化時參考。
關鍵詞Key words:java,性能優化
引用文件:
下列文件中的條款經過本規範的引用而成爲本規範的條款。凡是注日期的引用文件,其隨後全部的修改單(不包括勘誤的內容)或修訂版均不適用於本規範,然而,鼓勵根據本規範達成協議的各方研究是否可以使用這些文件的最新版本。凡是不注日期的引用文件,其最新版本適用於本規範。
序號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 |
性能是軟件產品開發中須要關注的一個重要質量屬性。在咱們產品實現的各個環節,均可能引入不一樣程度的性能問題,包括從最初的軟件構架選擇、到軟件的詳細設計,再到軟件的編碼實現等等。但在本規範中,主要內容是針對java系統,給出一些在軟件詳細設計和編碼實現過程當中須要注意的規則和建議。
即便咱們在產品開發的各個環節都考慮了性能質量屬性,可能仍是不夠的。隨着系統業務功能的不斷增多,用戶要求的不斷提升,或者爲了提升產品的可用性和競爭力,咱們不少狀況下仍是須要對已開發的產品進行一次集中式的專門的性能優化工做。本規範中所提供的大量實際優化案例,可供這種專門的性能優化工做進行參考。
可是在性能優化工做中,咱們也需警戒"過早優化"的問題。咱們的基本指導策略仍是首先讓系統運行起來,再考慮怎麼讓它變得更快。通常只有在咱們證明某部分代碼的確存在一個性能瓶頸的時候,才應進行優化。除非用專門的工具分析瓶頸,不然頗有多是在浪費本身的時間。另外,性能優化的隱含代價會使咱們的代碼變得難於理解和維護,這一點也是須要權衡和關注的。
關於線程同步的處理,通常在編碼過程當中存在如下幾個問題:
從性能角度來說,並非全部的同步方法都影響性能。若是同步方法調用的頻率足夠少,則加同步和不加同步,對性能影響不是很大,甚至能夠忽略。咱們在編寫代碼時,須要對一些重點的地方關注同步問題。
建議1.1.1.1:對於可能被其它代碼頻繁調用的方法,須要關注同步問題
建議1.1.1.2:對於經常使用工具類的方法,須要關注同步問題
建議1.1.1.3:對於不能確認被其它代碼如何調用的方法,須要關注同步問題
在代碼編寫過程當中,很容易犯同步問題的錯誤,不恰當的使用同步機制。咱們在編寫代碼(重點是:可能被頻繁調用的方法、做爲經常使用工具類的方法、不能確認被其它代碼如何調用的方法)時,須要重點關注如下同步問題:
總的來講,咱們不必對全部代碼都關注同步問題,在處理同步問題的性能優化時還須要保證代碼的可讀性和可維護性。因此對於進入維護階段的代碼,咱們不要盲目的進行同步問題的優化,以避免引入一些新的問題。
在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); } ...... |
關於以上代碼,若是不使用線程局部變量的話,通常作法會是首先在Integer類中定義一個char數組的靜態成員變量,而後直接將toString(…)方法加上synchronized關鍵字來進行同步處理。但這種作法咱們能夠想象,當Integer的toString(…)方法被頻繁調用的話,對性能影響是很是大的。
改用線程局部變量後,能夠爲每一個調用的線程單獨維護一個char數組,而且保證每一個線程只使用本身的char數組變量,從而也就不存在須要同步的問題了,所以性能就比直接使用synchronized來進行同步要好不少。其實在JDK的源代碼中,Integer.java、Long.java、Charset.java、StringCoding.java等類中都用到了線程局部變量。
在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. } } …… |
在咱們實際開發的代碼中,有很大一部分代碼都是在作各類字符串操做。常常用到的字符串操做包括:字符串鏈接、字符串比較、字符串大小寫轉換、字符串切分、字符串查找匹配等。咱們在編碼過程當中存在的問題是:每每對於一種字符串操做,有多種處理方式可供選擇,而對於程序中的那些熱點方法,若是選擇了不恰當的字符串處理方式,將會對程序性能產生較大的影響。
規則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:當須要對報文等文本字符串進行分析處理時,請增強檢視,注意算法實現的優化
在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 |
經過上面測試代碼的輸出結果能夠了解兩點:
在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 |
最可怕的不恰當的字符串拼接方式,是在for循環中使用"+"來進行字符串對象拼接,相似以下代碼:
for(int i = 0 ; i < 1024*1024; i++ ) { str += "XXX" ; } |
運行上面代碼的結果是:將致使整個操做系統CPU佔有率長時間達到100%,操做系統長時間(應該在5分鐘以上)幾乎處於不響應狀態。具體緣由很簡單,每次"+"號操做後,都會生成一個新的臨時字符串對象,隨着循環的深刻,建立的臨時字符串對象愈來愈大,執行起來就會愈來愈困難。若是將上述代碼改成StringBuffer來實現拼接,能夠看到程序可以正常運行。
在java中,進行字符串查找匹配時通常有三種實現方式:第一種是調用String對象的indexOf(String str)方法;第二種是調用String對象的matches(String regex)方法;第三種是直接使用正則表達式工具類(包括Pattern類、Matcher類)來實現匹配。這三種實現方式的各自特色以下:
如下是對三種查找匹配實現方式的性能測試代碼:
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
|
在咱們實際開發中,經常須要對字符串進行分割,如將"abc def ghi"按空格分割成三個字符串而後放到一個String數組中。在java中,通常有三種方法能夠實現字符串的分割:
對於以上三種分割方法,其運行效率差異是很大的。採用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]); } …… |
通過測試,假設要分割的字符串是"aaaa,bbbb,cccc,dddd,eeee,ffff",對上述三種算法各調用1000次後,所花的時間大概以下表:
實現算法 |
所花時間(單位:毫秒) |
採用split(String regex)方法進行分割 |
141 |
採用StringTokenizer進行分割 |
46 |
採用自定義方法進行分割 |
16 |
根據上表的性能測試結果可知,採用split(String regex)方法進行分割字符串性能是很低的。因此在實際代碼開發中,若是須要頻繁對字符串進行分割的話,最好不要採用String對象的split(…)方法進行字符串分割,一個比較好的選擇是本身寫一個公用的字符串分割方法。
在實際開發中,會遇到須要對報文等文本字符串進行分析處理的問題,對於不一樣的開發人員,實現文本字符串分析處理的算法會存在較大差別,有的實現會比較高效,有的實現看起來比較簡單但性能卻不好。如下是一個字符串處理算法優化案例:
//優化前的方法實現 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; } |
總結字符串處理的優化案例,通常不高效的字符串處理算法表現爲:
錯誤的作法: intialMessage = intialMessage.substring(index); index = intialMessage.indexOf(DOUBLE_LINE_SEPARATOR);
好的作法: index= intialMessage.indexOf(DOUBLE_LINE_SEPARATOR,index); |
在java語言中,或者說在面嚮對象語言中,對象的建立是既耗時間,又佔用內存。若是在系統運行過程當中建立了大量沒必要要的臨時對象,對性能的影響是比較大的。所以如何避免建立沒必要要的臨時對象,對系統性能的提高有着重要做用。在實際代碼開發中,建立了沒必要要的臨時對象的緣由通常可分爲如下幾種:
建議1.3.1.1:在實現業務處理流程的過程當中,須要考慮臨時對象引發的性能問題,精簡業務處理流程,減小沒必要要的中間環節
建議1.3.1.2:對象的建立應儘可能按需建立,而不是提早建立
建議1.3.1.3:對象的建立應儘可能在for、while等循環外面建立,在循環裏面進行重用
建議1.3.1.4:對於高頻度使用的對象,須要進行單獨優化處理給以重用
在某OM系統中,有一個對跟蹤消息體碼流進行保存的處理流程。在該處理流程中,主機會頻繁的向OM系統上報跟蹤消息,OM系統須要對全部這些上報消息實時進行保存,以便用戶後續根據消息分析和定位問題。在優化前,跟蹤消息體的保存處理流程以下圖所示:
從上面的處理流程能夠看出,每保存一條消息體碼流,就新建了一個ByteBuffer對象和一個byte數組對象,這是沒必要要的浪費,當跟蹤消息上報速度很大時,這種ByteBuffer和byte數組臨時對象的建立對性能影響是很大的。通過對該處理流程改進優化後,新的處理流程以下圖:
優化前和優化後的處理流程的差異在於,優化前每保存一條消息碼流,都要建立一個ByteBuffer對象,而優化後把這一過程給省略了,直接經過IO寫入數據,減小了中間碼流的轉換過程。
在某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(); } |
通過優化後, 報文隱藏密碼方法的代碼以下:
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(); } |
在實際代碼開發中,沒有按需建立對象是一個常常容易犯的錯誤。不少開發人員每每圖一時編碼方便或時間緊張來不及細想,將一些對象提早建立出來,而在後面某些條件分支中又徹底用不到這些對象。
在某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(); //該對象沒有重用 …… } …… |
在上面顯示的代碼中,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 = "";//消息詳細解釋碼流字符串 …… } …… |
雖然優化後的代碼比優化前更加高效,但可讀性要比優化前差些,須要多加一些註釋進行說明。從這個優化方案,能夠總結出如下結論:
在java API的集合框架(Java Collections Framework)中,提供了豐富的集合處理類,包括無序集合(Set集合)、有序集合(List集合)和映射關係集合(Map集合)。在一般狀況下,集合框架提供了足夠的功能供咱們使用,咱們關注的重點是如何選擇這些集合類。但須要指出的是,java API的集合框架也並無想象的那麼完善,它不可能解決全部應用場景下的問題。咱們在使用集合類進行編程時,經常面臨如下問題:
建議1.4.1.1:在代碼開發中,須要根據應用場景合理選擇集合框架中的集合類,應用場景可按單線程和多線程來劃分,也可按頻繁插入、隨機提取等具體操做場景來劃分
建議1.4.1.2:對於熱點代碼,能夠採用特定的集合類來提供系統性能,特定集合類能夠是本身開發,也能夠直接採用Trove這樣的第三方開源類庫
建議1.4.1.3:當須要在方法之間傳遞多個屬性值時,從性能角度考慮,應優先採用結構體,而非ArrayList或Vector等集合類
在某網管系統中,故障查詢數據中的流水號佔用了大量內存。當數據量達到必定數值(通常達到300萬)時,容易形成前臺內存溢出。其緣由主要是流水號的存取是採用集合框架的ArrayList來存取的。因爲ArrayList必須以Object的方式保存內容,所以在保存流水號的時候必須用Integer對象,而不是基本整數類型。在java中一個Integer對象佔用的內存大約爲32個字節,而int類型只佔用4個字節,因此在大數據量狀況下,採用Integer對象比int類型耗費的內存要多得多。針對上面的問題,優化思路是開發一個特定的集合類IntArrayList,該類有兩個特色:
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; } } } |
Trove集合類是一種開放源代碼的java集合包,聽從LGPL協議(能夠商用),提供了java集合類的高效替代品。Trove能夠從http://trove4j.sourceforge.net/處獲取。相對於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 |
IO讀寫是咱們在實際開發中常常要遇到的功能實現。Java API爲咱們提供了龐大的IO讀寫庫,從實現上來看可分爲早期的基於流的IO庫和新的基於塊的NIO庫,從功能上來看可分爲基於字節操做的IO庫和基於字符操做的IO庫。在這麼龐大的IO庫下,對於同一個IO功能,能夠編寫出多種實現代碼,但須要強調的是,一個設計拙劣的IO代碼可能要比通過精心調整的IO代碼慢上幾倍。爲了使咱們開發的系統能高效運行,咱們就必然面臨一個問題:怎樣編碼才能使IO功能實現能夠性能最優?
規則1.5.1.1:進行IO讀寫操做時,必須使用緩衝機制
建議1.5.1.2:從性能角度考慮,應儘可能優先使用字節IO進行讀寫,而避免用字符IO進行讀寫
對於文件讀寫操做,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 |
經過以上代碼測試,可得出以下結論:
在java API中,針對數組和集合的操做,專門封裝了兩個類:java.util.Arrays和java.util.Collections。在這兩個工具類中,包含了數組及集合的經常使用操做方法,如拷貝、查找、排序等,這些方法通常來講算法實現上都是很是高效的。可是對工具類中的排序等方法,存在的問題是,這些方法因爲不能從業務數據集合裏面獲取排序數據,所以在實現上就多了不少數據拷貝、克隆等操做,形成的結果是容易生成大量的臨時對象。因此當咱們須要對數組或集合進行拷貝、查找、排序等操做時,對於通常的應用應優先使用Arrays和Collections中提供的方法,可是對於熱點代碼,最好是參考java API中的方法實現,本身開發特定的排序等方法。
建議1.6.1.1:對於數組、集合的拷貝、查找、排序等操做,若是是通常應用,能夠優先採用java.util.Arrays和java.util.Collections中提供的工具方法;可是對於熱點代碼,最好是參考java API中的方法實現,本身開發特定的排序等方法,以減小臨時對象的建立。
規則1.6.1.2:對於數組的拷貝,請使用System.arraycopy(…)方法
一些對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); |
在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); } |
分析上面的代碼實現,能夠看出,在一次排序過程當中,該排序算法產生了2個臨時的數組對象,這對於那些動態排序功能(須要根據上報的數據頻繁的進行排序)的實現,性能的影響是不能忽略的。
和C++同樣,內存泄漏問題也是java程序開發中須要重點關注的性能問題。一些常見的內存泄漏緣由有:
另外值得一提的是,將一些大的對象定義成靜態的,也會形成相似於內存泄漏的問題。其緣由是若是靜態變量所屬的類是被系統類裝載的,則即便該類再也不使用時也不會被卸載掉,這將致使靜態對象的生存時間可能和系統同樣長久,而無論該對象是否被使用。
規則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關閉操做
在某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); } …… |
從上面的代碼說明咱們能夠知道,只要類裝載器不被垃圾回收掉,則被該類裝載器裝載的全部類都不會被卸載掉。
Java的設計目標是靈活、易用和平臺一致性。出於這一目的,在UI設計方面,java將界面的繪製和事件處理統一放在了一個獨立的線程中進行,這個線程就是事件派發線程。因爲事件派發線程只有一個,而且負責了關鍵的界面繪製和界面事件處理,因此若是該線程被阻塞或者處理的業務邏輯太重的話,會致使整個系統響應很慢、甚至發生灰屏現象。這對用戶來講,就是嚴重的性能問題。因此在系統的設計開發中,對派發線程的使用必須格外謹慎。
規則2.1.1.1:對於非界面的業務邏輯,應放在事件派發線程以外處理,保證事件派發線程處理的邏輯儘量的少;避免在派發線程中執行時間較長、或執行時間具備較大不肯定性(如訪問遠程服務器)的業務邏輯
建議2.1.1.2:對於高頻度的界面更新事件,最好採用批量定時更新方式代替實時更新方式
在某OM系統中,出於性能優化,對跟蹤上報消息顯示功能採用了定時刷新機制來批量更新表格數據。當上報消息解析好後,直接將其添加到表格模型中,但不觸發模型更新事件(也即在添加數據時,不調用fireTableRowsInserted、fireTableRowsDeleted等方法)。模型更新事件統一放在一個javax.swing.Timer裏面定時進行觸發(每300毫秒觸發一次)。跟蹤上報消息表格定時更新的處理流程以下圖:
在某OM系統中,有一個上報消息處理業務功能。在優化前,該功能實現將大量業務放到了派發線程中處理(主要有從緩衝區取消息、保存消息到文件、解析消息碼流、將解析結果添加到表格、定時刷新表格界面),其流程實現以下圖:
基於讓派發線程處理儘量少的業務的原則,優化後,經過新增一個業務處理線程,並在該業務處理線程和派發線程之間添加一個表格模型緩衝區的方式,較好的實現了將大部分業務移到派發線程以外處理,最後的結果是派發線程只須要定時刷新表格界面就能夠了。優化後的上報消息處理流程以下圖:
咱們在實際開發中,常常會遇到這樣一個問題:在一個連續的業務處理過程當中,如何將非界面處理的業務邏輯隔離到派發線程以外?根據設計經驗,基本能夠得出這樣一個結論:要想使兩個線程協調工做,必須有一個可操做的共享數據區或對象。在上面的優化案例中,咱們定義了一個表格模型緩衝區來使業務處理線程和派發線程協調工做。咱們還能夠調用javax.swing.SwingUtilities類的invokeAndWait和invokeLater方法,在業務處理線程環境下將一些界面處理邏輯添加到派發線程中進行處理(在這種狀況下,事件派發隊列就是業務處理線程和派發線程之間的共享數據區)。
在Swing中,全部輕型(lightweight)組件都是經過java的繪圖工具繪製出來的。每一個輕型組件都有本身的paint()方法(默認的paint方法是從JComponent類中繼承過來的)。當顯示組件時,由派發線程調用頂層容器的paint()方法將容器及容器裏面的全部子組件繪製出來。從性能角度講,Swing的界面繪製機制存在如下問題:
Swing輕型組件的繪製流程以下圖:
從上面的流程圖能夠看出,Swing組件的繪製是一個層層往下的過程,組件首先繪製本身,若是有border則再繪製出border,而後繪製本身的子組件;對於子組件來講,首先繪製本身,而後再又繪製本身的子組件;每一個組件在繪製本身時,須要進行相關的繪圖區域範圍計算。另外,須要指出的是,在java體系中,字符串的顯示也是經過java本身的繪製機制繪製出來的。因此,在字符串的顯示過程當中,也會建立不少臨時對象。
經過上面的分析,從性能角度上講,Swing的實現並無想象的那麼好。所以在特殊的應用場景下,爲了提升咱們系統的性能,咱們須要,也有必要根據業務處理特色,定義本身的界面繪製機制,甚至開發特定的界面組件。對於像JTable,JTree這樣的界面對象,經過定製和優化,能夠極大的提升其界面繪製的性能。
建議2.2.1.1:爲了提升系統性能,能夠根據業務處理特色,定義本身的界面繪製機制
建議2.2.1.2:爲了提升系統性能,能夠根據業務處理特色,開發本身的界面組件
在某OM系統中,對實時跟蹤模塊,早期的跟蹤消息表格繪製方式採用的是JDK默認繪製方式,經過BasicTableUI對象實現表中單元格的繪製。JDK的這種方式具備共用性,提供的功能也很是多,但存在的問題是每繪製1個單元格,都要至少拷貝1個Graphics2D臨時對象,同時要調用表格中的swing組件的paint()進行組件的繪製,在單元格組件的繪製過程當中,又建立了Graphics2D臨時對象。
在後期的性能優化工做中,已經將實時跟蹤上報消息採用優化表格進行顯示。具體優化方法及步驟以下:
採用優化表格後,表格界面的繪製流程以下:
在實際開發中,通常有以下兩種方法能夠定義本身的界面繪製機制:
在這裏須要指出的是,只有在充分研究分析了應用場景後,才能嘗試採用定義本身的界面繪製機制來提升系統性能。通常狀況下不建議改變Swing組件已有的繪製機制,一來出於工做量的考慮,二來出於通用性的考慮,再者也避免引入一些未知問題。
在某OM系統中,採用JDK的JTextArea類實現批處理文件的打開、編輯和保存等功能。在使用過程當中發現,當打開比較大的批處理文件(通常是幾兆大小的文本文件)時,常常會致使系統灰屏、CPU佔用率100%,甚至內存溢出現象。通過對JTextArea組件作性能測試,發現當打開比較大的文件時,JTextArea存在臨時對象建立過多和內存佔用過大的問題。如下是JTextArea組件和UltraEdit程序打開一樣大小文件(文件大小爲3.95M,共376792行)時的內存增量數據:
打開方式 |
物理內存增量(M) |
虛擬內存增量(M) |
JTextArea組件 |
52.4 |
53.3 |
UltraEdit程序 |
3.44 |
1.59 |
經過上面的內存增量數據能夠看出,JDK的JTextArea組件打開4M左右的文件須要佔用50M左右的內存,這在實際應用中是很難知足要求的,和UltralEdit程序比起來,性能要差一個數量級以上。
JTextArea組件打開文件時內存佔用過大的主要緣由在於其文檔模型(JTextArea使用PlainDocument對象來存儲文本數據和文本結構信息)。總的來看,JTextArea的文檔模型PlainDocument(其實Swing的全部Document對象都存在該問題)具備如下性能問題:
幸運的是,在java開源項目Jedit(注意,Jedit聽從GPL協議,不能商用)中,提供了比JTextArea性能好得多的文本編輯組件。爲了解決性能問題,咱們借鑑Jedit文本組件的設計思路,開發了本身的文本編輯組件。該組件相比JTextArea具備如下優勢:
如下是咱們本身開發的文本編輯組件和JTextArea組件的性能對比數據(將組件都放在一個JFrame中,而後讀取一個1.91M的文本文件,共49431行):
打開方式 |
GC後的OLD區內存佔用狀況(M) |
代碼區內存佔用狀況(M) |
使用JTextArea |
12.653 |
4.686 |
使用本身開發的組件 |
4.464 |
4.160 |
Java語言不像C++語言同樣,能夠在棧上建立對象,隨着函數調用完後對象可以自動被釋放;另外也不能對new出來的對象進行delete。Java語言的這些限制致使了臨時對象問題的存在。咱們在業務流程的設計實現中,從性能上考慮應儘可能保證流程處理的精簡和高效,不然很容易產生臨時對象的問題。
建議2.3.1.1:對於一些關鍵的業務處理流程,須要儘可能減小中間處理環節,從而避免建立沒必要要的臨時對象
建議2.3.1.2:在一些關鍵的業務處理流程中,對於必需要用到的對象,能夠採起重用對象機制,重複利用,避免每次都建立
建議2.3.1.3:對於大多數業務處理來講,臨時對象都不是問題。只有對那些高頻度或大數據量業務處理操做來講,而且經過性能測試證實的確是臨時對象引發了性能問題,才須要進行臨時對象的優化。
在某OM系統的跟蹤模塊中,須要對實時上報的跟蹤消息碼流進行解析,而後將解析結果顯示到界面表格中。跟蹤消息碼流的解析過程爲:首先從handleMessage方法中接受要解析的消息碼流包(一個大的byte數組,約6K),而後將該消息碼流包分解成具體的跟蹤消息幀;對每個跟蹤消息幀,先解析出消息頭,並根據消息頭裏面的信息校驗消息長度的正確性,而後根據消息頭信息獲取消息體的碼流,最後對消息體的碼流進行解析,根據系統定義顯示相關結果字符串到界面表格中。
在早期的跟蹤消息碼流解析處理流程中,會產生大量的臨時byte數組對象:把一個消息包分解成消息幀,須要建立許多消息幀byte數組;對消息幀的消息頭進行解析,又須要建立一個消息頭的byte數組;再對消息幀中的消息體進行解析前,又須要建立一個消息體的byte數組。具體流程以下圖所示:
在後期的跟蹤模塊性能優化中,針對跟蹤消息碼流的解析流程進行了優化。優化的主要思路是:在消息碼流的整個解析過程當中,再也不建立新的byte數組,而是重用消息碼流包的byte數組;在解析時,經過消息的偏移量定義,從byte數組中取出要解析的數據。具體流程以下圖所示:
在告警瀏覽上報消息處理中,採用相似對象池的方式來重用對象,以減小臨時對象的建立。告警瀏覽上報消息的解析流程以下:
從上面的流程圖能夠看出,告警瀏覽模塊主要經過ObjPool來重用AlarmRecord對象。每次當須要一個AlarmRecord對象時,都從ObjPool裏面取,若是沒有才新建一個AlarmRecord對象;當AlarmRecord對象再也不使用時,將其從新放到ObjPool中。
對於桌面客戶端系統來講,界面性能是用戶所關注的一項重要內容。通常界面性能應該包括:界面響應速度(這裏特指界面的建立和顯示時間)、界面刷新速度、界面友好性(這裏特指進度條、鼠標置忙等響應措施)。提升系統界面性能的主要方法包括:
建議2.4.1.1:對於用戶頻繁進行開啓、關閉的窗口組件,須要儘可能採起重用機制,用界面隱藏代替界面關閉
建議2.4.1.2:若是一個操做須要很長時間(如大於60秒),則要在執行操做以前就彈出提示選擇界面,讓用戶選擇是否真要執行該操做
建議2.4.1.3:若是一個操做須要較長時間(如大於3秒),則最好彈出明確的進度條提示界面
建議2.4.1.4:若是一個操做比通常操做耗時較長(如大於1秒),那麼能夠給出非顯要的界面提示(如在窗口的狀態欄給出相關提示)
在某OM系統中,消息詳細解釋功能是用戶使用很是頻繁的一個功能。在早期版本的消息詳細解釋子模塊中,每次用戶雙擊一條跟蹤消息時,都會建立整個窗口界面,而後顯示給用戶。這種實現方式使得每次彈出窗口都須要3秒左右,響應速度比較慢。在後期版本改進中,對整個消息詳細解釋模塊進行了重構和優化。優化後,只有第1次彈窗口時,纔會建立界面組件,後面再次彈窗口時,只是更新一下數據模型就能夠了。優化後的窗口彈出速度只須要1秒左右,響應速度明顯提升。優化後的消息詳細解釋窗口顯示流程以下圖:
系統負載能力指的是在應用許可的任務負載下,系統的性能是否知足客戶要求。這裏的系統性能主要包括:在可能的極限負載下,系統是否能夠保持正常運行,不發生崩潰或內存溢出等現象,而且界面能保持響應。從性能角度講,通常隨着執行任務的增長,系統響應時間應該是逐漸增長緩慢,而不是成指數增長的。下圖左圖是成指數增長的響應時間,右圖是平緩增長的響應時間:
成指數增長的響應時間 平緩增長的響應時間
規則2.5.1.1:若是系統須要運行動態變化的負載,那麼須要保證在可能的極限負載下,系統能夠正常運行
在某OM系統中,存在一個跟蹤消息上報的處理流程。當網元的跟蹤消息上報流量太大時,容易致使系統性能嚴重降低,甚至發生灰屏、界面不響應的狀況。爲此,在跟蹤消息處理流程中增長了一個流控機制,該流控機制能夠根據系統資源可用狀況,分級採起不一樣的流控措施,當系統資源充足時,已發生的流控又可自行回覆。流控機制的具體實施方案以下:
流控方案採用三級流控方式:1)顯示流控:下降界面的刷新頻率,2)存盤流控:消息只存盤不解析碼流,也不顯示在界面,3)過載流控:將當前流控的跟蹤任務的消息緩存清空。具體表現爲:當網元大量上報消息致使CPU使用率持續超過系統指定處理能力上限閾值(如95%)的時候,開始進入顯示流控;進入顯示流控後若是CPU使用率仍然持續超過系統指定處理能力上限閾值而且上報消息的速率持續大於系統指定的基線值(如40條/秒)的時候,開始進入存盤流控;進入存盤流控後若是CPU使用率仍然持續超過系統指定處理能力上限閾值而且客戶端進程的CPU使用率大於45%的時候, 開始進入過載流控。流控的順序是按照:正常情況->顯示流控->存盤流控->過載流控方向進行流控;恢復的時候則是按照流控的逆過程: 恢復過載流控->恢復存盤流控->恢復顯示流控->正常情況方向進行恢復。整個流控機制的流程圖以下:
對於須要較長時間執行的業務處理,能夠考慮分爲多個相對獨立的併發處理業務,採用多線程機制以縮短總的業務執行時間。如當用戶雙擊一個很大的tmf文件時,能夠在顯示系統界面的過程當中,同時用另外一個線程去解析tmf文件,準備好要顯示的數據。這樣從雙擊文件到顯示出最終數據的時間就會縮短。這種優化方法簡單的說就是:將串行工做變爲並行工做,以縮短整體工做時間;或者是利用系統空閒時間,作一些前期輔助工做,以縮短界面響應時間。
採用多線程機制提升業務處理速度時,其前提是要能夠將業務處理劃分爲幾個相對獨立的處理邏輯,而後要麼併發執行這些獨立的處理邏輯,要麼將一部分處理邏輯提早到系統空閒時間中執行。另一個問題是要控制好線程間的同步問題(能夠經過調用wait方法或join方法來實現這一點)和相關資源的釋放問題。
建議2.6.1.1:對於須要較長時間執行的業務處理,能夠考慮採用多線程機制將業務處理劃分爲幾個相對獨立的處理邏輯併發執行,或者將一部分處理邏輯提早或延後到系統空閒時間中執行,以縮短總的業務執行時間
在某OM系統中,有一個跟蹤消息體碼流過濾功能,其要求的業務處理流程以下:
根據要求的業務處理流程,優化前代碼處理邏輯以下圖:
採用上面優化前的處理邏輯,當跟蹤回顧界面中有50000條消息時,執行過濾所花的時間通常須要10秒左右。爲了解決執行過濾所花時間過長的問題,對跟蹤消息體過濾代碼進行了優化,主要的優化思路是採用一個單獨線程來收集碼流數據,優化後的代碼處理邏輯以下圖:
採用上面優化後的處理邏輯,當跟蹤回顧界面中有50000條消息時,執行過濾所花的時間能夠由原來的10秒左右降爲3秒左右。
在Java應用系統開發完畢後,須要對全部程序文件進行打包,製做成安裝盤供用戶安裝使用。安裝盤的大小和系統的安裝時間也是用戶比較關注的性能問題。安裝盤越小,用戶從網絡上下載安裝盤所需時間就越短;一樣,安裝時間越短,用戶就能夠在安裝過程當中不用長時間的等待。所以,咱們在製做安裝盤的時候,通常都要採用相關的壓縮算法來對要發佈的程序文件進行壓縮,而不是簡單的打包。對基於java開發的OM系統來講,其程序文件中通常既包括了大量的java class文件、資源文件、也包括大量的DLL文件,爲了使壓縮後的安裝盤儘量的小,咱們須要針對不一樣的文件類型,採用不一樣的壓縮格式。針對java程序文件(以jar格式存在),一種最優的壓縮格式是pack200壓縮格式,而針對其它文件,一種很是高效的壓縮格式是7z壓縮格式。
對基於java開發的OM系統來講,在實際應用中,能夠先對每一個jar文件採用pack200方式進行壓縮,而後對全部文件進行7z格式的壓縮,實踐證實,這種混合壓縮方式製做的安裝盤壓縮比是很是優的。
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文件的打包和解包。
7z是一種新的壓縮格式(聽從LGPL協議,能夠商用),它擁有目前最高的壓縮比。7z格式的主要特徵有:
LZMA 算法是 7z 格式的默認標準算法。LZMA 算法的主要特徵有:
目前支持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
在對系統進行性能優化的過程當中,性能測試工做起着相當重要的做用。一方面,在性能優化前,咱們須要經過性能測試找出系統的性能瓶頸所在,作到有目的的優化;另外一方面,咱們在對系統作了一個性能優化方案後,仍然須要經過性能測試來驗證方案的優化效果,對於沒有明顯性能優化效果的方案,咱們通常是不建議給予實施的。
性能測試是一個比較具體化的工做,須要具體問題具體分析。總的來說,在性能測試過程當中都將面臨兩個問題:一個是性能測試工具的選擇和使用,另一個是性能測試方法即如何設計測試用例和分析測試數據的問題。對於java應用系統來講,目前有多種性能測試工具可供使用,下面將對這些工具一一作一個簡要的介紹。不一樣的性能測試工具所關注的性能測試點是不同的,因此咱們在性能測試過程當中,須要綜合利用這些工具,從不一樣的關注點來對一個系統性能作出全面的評估。另外,下面也將對一些性能測試方法作一個簡要的說明和介紹。
目前,網絡上有各類各樣的profiler工具,通常最經常使用的是Borland公司的Optimizeit套件。經過Borland公司的profiler工具主要能夠作如下事情:
Profiler工具的運行界面以下圖:
Visualgc工具是sun公司開發的一個免費的性能測試工具,能夠從sun公司網站上下載。經過visualgc工具主要能夠作如下事情:
Visualgc工具的運行界面以下圖:
打印和分析GC日誌,是對java系統進行性能測試的一個重要手段。對sun公司的hotspot虛擬機來講,能夠添加相似以下的JVM參數來打印GC日誌信息:
"-verbose:gc –XX +PrintGCDetails –Xloggc:c:\gclog\log.txt"
打印出GC日誌後,能夠經過GC日誌分析工具來進行分析,如今網絡上有諸如GCViewer之類的免費工具可供使用,固然也可直接查看和分析GC日誌數據。經過分析GC日誌,能夠作以下事情:
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 |
性能檢視器的配置界面以下:
在對測試數據進行正式分析前,首先須要檢查測試數據是否真實可靠。測試時獲得了不可靠的測試數據的主要緣由有:
致使測試數據失真的緣由是很是多的,因此在分析測試數據以前須要檢查測試數據的真實性和可靠性。檢查的方法通常有:
在數據量不大的狀況下,經過Excel工具進行數據處理是一個比較好的方法(Excel最多支持65536行,256列)。經過Excel工具處理數據的經常使用方法有:
性能測試的一個難點就是如何對測試數據進行分析,並找出各類測試結果產生的緣由。對測試數據進行分析時通常關注如下幾個方面:
經過設計和執行測試用例來檢測系統性能是最直接,也是最可靠的方式。但在實際操做中,若是對全部的狀況都進行性能測試,每每工做量是巨大的,而且是得不償失的。好比測試實時跟蹤的一個性能優化方案的效果,首先在上報速度爲200條/秒的狀況下進行測試,發現優化效果明顯。可是立刻你們會存在疑惑:那麼在上報速度爲100條/秒、50條/秒、10條/秒等狀況下,這種優化效果是否依然存在呢?換句話說,若是效果不是很明顯,那麼咱們是否還有優化的必要呢?若是咱們爲了回答這些疑問,針對全部這些狀況都進行測試的話,其工做量將是很是大的。
實際上,只要改變測試用例中的任何一個測試條件,都將產生一個新的測試用例。所以咱們不可能對全部延伸出來的測試用例都進行測試。解決這個問題的辦法應該採起理論和實踐相結合的方式,首先經過基本測試用例獲得幾組測試數據,而後根據這些測試數據進行理論評估和公式推導,最後根據推導的公式給出當測試條件變化時的預期結果。
進行理論公式推導的方法是,首先根據業務代碼創建起數據模型,而後將現有的測試數據代入數據模型中,得出可求解的公式。
序號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 |
Pack200資料 |
|
9 |
7z壓縮格式資料 |