熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java高級技術 >> 正文

Java性能優化

2013-11-23 19:58:44  來源: Java高級技術 

  Java語言特別強調准確性但可靠的行為要以性能作為代價這一特點反映在自動收集垃圾嚴格的運行期檢查完整的字節碼檢查以及保守的運行期同步等等方面對一個解釋型的虛擬機來說由於目前有大量平台可供挑選所以進一步阻礙了性能的發揮

  先做完它再逐步完善幸好需要改進的地方通常不會太多(Steve McConnell的《About performance》[])

  本文的宗旨就是指導大家尋找和優化需要完善的那一部分

  .基本方法

  只有正確和完整地檢測了程序後再可著手解決性能方面的問題

  () 在現實環境中檢測程序的性能若符合要求則目標達到若不符合則轉到下一步
  () 尋找最致命的性能瓶頸這也許要求一定的技巧但所有努力都不會白費如簡單地猜測瓶頸所在並試圖進行優化那麼可能是白花時間
  () 運用本附錄介紹的提速技術然後返回步驟

  為使努力不至白費瓶頸的定位是至關重要的一環Donald Knuth[]曾改進過一個程序那個程序把%的時間都花在約%的代碼量上在僅一個工作小時裡他修改了幾行代碼使程序的執行速度倍增此時若將時間繼續投入到剩余代碼的修改上那麼只會得不償失Knuth在編程界有一句名言過早的優化是一切麻煩的根源(Premature optimization is the root of all evil)最明智的做法是抑制過早優化的沖動因為那樣做可能遺漏多種有用的編程技術造成代碼更難理解和操控並需更大的精力進行維護

  .尋找瓶頸

  為找出最影響程序性能的瓶頸可采取下述幾種方法

   安插自己的測試代碼

  插入下述顯式計時代碼對程序進行評測

long start = SystemcurrentTimeMillis();
// 要計時的運算代碼放在這兒
long time = SystemcurrentTimeMillis() start;

  利用Systemoutprintln()讓一種不常用到的方法將累積時間打印到控制台窗口由於一旦出錯編譯器會將其忽略所以可用一個靜態最終布爾值(Static final boolean)打開或關閉計時使代碼能放心留在最終發行的程序裡這樣任何時候都可以拿來應急盡管還可以選用更復雜的評測手段但若僅僅為了量度一個特定任務的執行時間這無疑是最簡便的方法

  SystemcurrentTimeMillis()返回的時間以千分之一秒(毫秒)為單位然而有些系統的時間精度低於毫秒(如Windows PC)所以需要重復n次再將總時間除以n獲得准確的時間

   JDK性能評測[]

  JDK配套提供了一個內建的評測程序能跟蹤花在每個例程上的時間並將評測結果寫入一個文件不幸的是JDK評測器並不穩定它在JDK 中能正常工作但在後續版本中卻非常不穩定

  為運行評測程序請在調用Java解釋器的未優化版本時加上prof選項例如
java_g prof myClass
或加上一個程序片(Applet)
java_g prof sunappletAppletViewer applethtml

  理解評測程序的輸出信息並不容易事實上在JDK 它居然將方法名稱截短為字符所以可能無法區分出某些方法然而若您用的平台確實能支持prof選項那麼可試試Vladimir Bulatov的HyperPorf[]或者Greg White的ProfileViewer來解釋一下結果

   特殊工具

  如果想隨時跟上性能優化工具的潮流最好的方法就是作一些Web站點的常客比如由Jonathan Hardwick制作的Tools for Optimizing Java(Java優化工具)網站
http://wwwcscmuedu/~jch/java/toolshtml

   性能評測的技巧

  ■由於評測時要用到系統時鐘所以當時不要運行其他任何進程或應用程序以免影響測試結果

  ■如對自己的程序進行了修改並試圖(至少在開發平台上)改善它的性能那麼在修改前後應分別測試一下代碼的執行時間

  ■盡量在完全一致的環境中進行每一次時間測試

  ■如果可能應設計一個不依賴任何用戶輸入的測試避免用戶的不同反應導致結果出現誤差

  .提速方法

  現在關鍵的性能瓶頸應已隔離出來接下來可對其應用兩種類型的優化常規手段以及依賴Java語言

   常規手段

  通常一個有效的提速方法是用更現實的方式重新定義程序例如在《Programming Pearls》(編程拾貝)一書中[]Bentley利用了一段小說數據描寫它可以生成速度非常快而且非常精簡的拼寫檢查器從而介紹了Doug McIlroy對英語語言的表述除此以外與其他方法相比更好的算法也許能帶來更大的性能提升——特別是在數據集的尺寸越來越大的時候欲了解這些常規手段的詳情請參考本附錄末尾的一般書籍清單

   依賴語言的方法

  為進行客觀的分析最好明確掌握各種運算的執行時間這樣一來得到的結果可獨立於當前使用的計算機——通過除以花在本地賦值上的時間最後得到的就是標准時間

 運算  示例  標准時間  本地賦值  i=n;    實例賦值  thisi=n;    int增值  i++;      byte增值  b++;    short增值  s++;    float增值  f++;    double增值  d++;    空循環  while(true) n++;    三元表達式  (x<) ?x : x    算術調用  Mathabs(x);    數組賦值  a[] = n;    long增值  l++;    方法調用  funct();    throw或catch異常  try{ throw e; }或catch(e){}    同步方法調用  synchMehod();    新建對象  new Object();    新建數組  new int[];  


   ProNetscape 及JDK 這些相對時間向大家揭示出新建對象和數組會造成最沉重的開銷同步會造成比較沉重的開銷而一次不同步的方法調用會造成適度的開銷參考資源[]和[]為大家總結了測量用程序片的Web地址可到自己的機器上運行它們

   特殊情況

  ■ 字串的開銷字串連接運算符+看似簡單但實際需要消耗大量系統資源編譯器可高效地連接字串但變量字串卻要求可觀的處理器時間例如假設s和t是字串變量
Systemoutprintln(heading + s + trailer + t);

  上述語句要求新建一個StringBuffer(字串緩沖)追加自變量然後用toString()將結果轉換回一個字串因此無論磁盤空間還是處理器時間都會受到嚴重消耗若准備追加多個字串則可考慮直接使用一個字串緩沖——特別是能在一個循環裡重復利用它的時候通過在每次循環裡禁止新建一個字串緩沖可節省單位的對象創建時間(如前所述)利用substring()以及其他字串方法可進一步地改善性能如果可行字符數組的速度甚至能夠更快也要注意由於同步的關系所以StringTokenizer會造成較大的開銷

  ■ 同步在JDK解釋器中調用同步方法通常會比調用不同步方法慢經JIT編譯器處理後這一性能上的差距提升到倍(注意前表總結的時間顯示出要慢倍)所以要盡可能避免使用同步方法——若不能避免方法的同步也要比代碼塊的同步稍快一些

  ■ 重復利用對象要花很長的時間來新建一個對象(根據前表總結的時間對象的新建時間是賦值時間的而新建一個小數組的時間是賦值時間的倍)因此最明智的做法是保存和更新老對象的字段而不是創建一個新對象例如不要在自己的paint()方法中新建一個Font對象相反應將其聲明成實例對象再初始化一次在這以後可在paint()裡需要的時候隨時進行更新參見Bentley編著的《編程拾貝》p[]

  ■ 異常只有在不正常的情況下才應放棄異常處理模塊什麼才叫不正常呢?這通常是指程序遇到了問題而這一般是不願見到的所以性能不再成為優先考慮的目標進行優化時將小的trycatch塊合並到一起由於這些塊將代碼分割成小的各自獨立的片斷所以會妨礙編譯器進行優化另一方面若過份熱衷於刪除異常處理模塊也可能造成代碼健壯程度的下降

  ■ 散列處理首先Java 的標准散列表(Hashtable)類需要造型以及特別消耗系統資源的同步處理(單位的賦值時間)其次早期的JDK庫不能自動決定最佳的表格尺寸最後散列函數應針對實際使用項(Key)的特征設計考慮到所有這些原因我們可特別設計一個散列類令其與特定的應用程序配合從而改善常規散列表的性能注意Java 集合庫的散列映射(HashMap)具有更大的靈活性而且不會自動同步

  ■ 方法內嵌只有在方法屬於final(最終)private(專用)或static(靜態)的情況下Java編譯器才能內嵌這個方法而且某些情況下還要求它絕對不可以有局部變量若代碼花大量時間調用一個不含上述任何屬性的方法那麼請考慮為其編寫一個final版本

  ■ I/O應盡可能使用緩沖否則最終也許就是一次僅輸入/輸出一個字節的惡果注意JDK 的I/O類采用了大量同步措施所以若使用象readFully()這樣的一個大批量調用然後由自己解釋數據就可獲得更佳的性能也要注意Java readerwriter類已針對性能進行了優化

  ■ 造型和實例造型會耗去個單位的賦值時間開銷更大的甚至要求上溯繼承(遺傳)結構其他高代價的操作會損失和恢復更低層結構的能力

  ■ 圖形利用剪切技術減少在repaint()中的工作量倍增緩沖區提高接收速度同時利用圖形壓縮技術縮短下載時間來自JavaWorld的Java Applets以及來自Sun的Performing Animation是兩個很好的教程請記著使用最貼切的命令例如為根據一系列點畫一個多邊形和drawLine()相比drawPolygon()的速度要快得多如必須畫一條單像素粗細的直線drawLine(xyxy)的速度比fillRect(xy)快

  ■ 使用API類盡量使用來自Java API的類因為它們本身已針對機器的性能進行了優化這是用Java難於達到的比如在復制任意長度的一個數組時arraryCopy()比使用循環的速度快得多

  ■ 替換API類有些時候API類提供了比我們希望更多的功能相應的執行時間也會增加因此可定做特別的版本讓它做更少的事情但可更快地運行例如假定一個應用程序需要一個容器來保存大量數組為加快執行速度可將原來的Vector(矢量)替換成更快的動態對象數組

   常規修改

  下面是加快Java程序關鍵部分執行速度的一些常規操作建議(注意對比修改前後的測試結果)

   修改成  理由  接口  抽象類(只需一個父時)  接口的多個繼承會妨礙性能的優化  非本地或數組循環變量  本地循環變量  根據前表的耗時比較一次實例整數賦值的時間是本地整數賦值時間的但數組賦值的時間是本地整數賦值的
 鏈接列表(固定尺寸)  保存丟棄的鏈接項目或將列表替換成一個循環數組(大致知道尺寸)  每新建一個對象都相當於本地賦值參考重復利用對象(下一節)Van Wyk[] p以及Bentley[] p  x/(或的任意次冪)  X>>(或的任意次冪)  使用更快的硬件指令

 
  2 其他建議

  ■ 將重復的常數計算移至關鍵循環之外——比如計算固定長度緩沖區的bufferlength

  ■ static final(靜態最終)常數有助於編譯器優化程序

  ■ 實現固定長度的循環

  ■ 使用javac的優化選項O它通過內嵌staticfinal以及private方法從而優化編譯過的代碼注意類的長度可能會增加(只對JDK 而言——更早的版本也許不能執行字節查證)新型的Justintime(JIT)編譯器會動態加速代碼

  ■ 盡可能地將計數減至——這使用了一個特殊的JVM字節碼


From:http://tw.wingwit.com/Article/program/Java/gj/201311/27766.html
    推薦文章
    Copyright © 2005-2013 電腦知識網 Computer Knowledge   All rights reserved.