本博客是閱讀<java time and space performance tips>這本小書后整理的讀書筆記性質(zhì)博客,增加了幾個(gè)測(cè)試代碼,代碼可以在此下載:java時(shí)空間性能優(yōu)化測(cè)試代碼 ,文件StopWatch是一個(gè)秒表計(jì)時(shí)工具類,它的代碼在文末。 1. 時(shí)間優(yōu)化 1.1 標(biāo)準(zhǔn)代碼優(yōu)化 a. 將循環(huán)不變量的計(jì)算移出循環(huán) 我寫了一個(gè)測(cè)試?yán)尤缦拢?/p> import util.StopWatch; /** * 循環(huán)優(yōu)化: * 除了本例中將循環(huán)不變量移出循環(huán)外,還有將忙循環(huán)放在外層 * @author jxqlovejava * */ public class LoopOptimization { public int size() { try { Thread.sleep(200); // 模擬耗時(shí)操作 } catch(InterruptedException ie) { } return 10; } public void slowLoop() { StopWatch sw = new StopWatch("slowLoop"); sw.start(); for(int i = 0; i < size(); i++); sw.end(); sw.printEclapseDetail(); } public void optimizeLoop() { StopWatch sw = new StopWatch("optimizeLoop"); sw.start(); // 將循環(huán)不變量移出循環(huán) for(int i = 0, stop = size(); i < stop; i++); sw.end(); sw.printEclapseDetail(); } public static void main(String[] args) { LoopOptimization loopOptimization = new LoopOptimization(); loopOptimization.slowLoop(); loopOptimization.optimizeLoop(); } } 測(cè)試結(jié)果如下: slowLoop任務(wù)耗時(shí)(毫秒):2204 optimizeLoop任務(wù)耗時(shí)(毫秒):211 可以很清楚地看到不提出循環(huán)不變量比提出循環(huán)不變量要慢10倍,在循環(huán)次數(shù)越大并且循環(huán)不變量的計(jì)算越耗時(shí)的情況下,這種優(yōu)化會(huì)越明顯。 b. 避免重復(fù)計(jì)算 這條太常見,不舉例了 c. 盡量減少數(shù)組索引訪問次數(shù),數(shù)組索引訪問比一般的變量訪問要慢得多 數(shù)組索引訪問比如int i = array[0];需要進(jìn)行一次數(shù)組索引訪問(和數(shù)組索引訪問需要檢查索引是否越界有關(guān)系吧)。這條Tip經(jīng)過我的測(cè)試發(fā)現(xiàn)效果不是很明顯(但的確有一些時(shí)間性能提升),可能在數(shù)組是大數(shù)組、循環(huán)次數(shù)比較多的情況下更明顯。測(cè)試代碼如下: import util.StopWatch; /** * 數(shù)組索引訪問優(yōu)化,尤其針對(duì)多維數(shù)組 * 這條優(yōu)化技巧對(duì)時(shí)間性能提升不太明顯,而且可能降低代碼可讀性 * @author jxqlovejava * */ public class ArrayIndexAccessOptimization { private static final int m = 9; // 9行 private static final int n = 9; // 9列 private static final int[][] array = { { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, { 11, 12, 13, 14, 15, 16, 17, 18, 19 }, { 21, 22, 23, 24, 25, 26, 27, 28, 29 }, { 31, 32, 33, 34, 35, 36, 37, 38, 39 }, { 41, 42, 43, 44, 45, 46, 47, 48, 49 }, { 51, 52, 53, 54, 55, 56, 57, 58, 59 }, { 61, 62, 63, 64, 65, 66, 67, 68, 69 }, { 71, 72, 73, 74, 75, 76, 77, 78, 79 }, { 81, 82, 83, 84, 85, 86, 87, 88, 89 }, { 91, 92, 93, 94, 95, 96, 97, 98, 99 } }; // 二維數(shù)組 public void slowArrayAccess() { StopWatch sw = new StopWatch("slowArrayAccess"); sw.start(); for(int k = 0; k < 10000000; k++) { int[] rowSum = new int[m]; for(int i = 0; i < m; i++) { for(int j = 0; j < n; j++) { rowSum[i] += array[i][j]; } } } sw.end(); sw.printEclapseDetail(); } public void optimizeArrayAccess() { StopWatch sw = new StopWatch("optimizeArrayAccess"); sw.start(); for(int k = 0; k < 10000000; k++) { int[] rowSum = new int[n]; for(int i = 0; i < m; i++) { int[] arrI = array[i]; int sum = 0; for(int j = 0; j < n; j++) { sum += arrI[j]; } rowSum[i] = sum; } } sw.end(); sw.printEclapseDetail(); } public static void main(String[] args) { ArrayIndexAccessOptimization arrayIndexAccessOpt = new ArrayIndexAccessOptimization(); arrayIndexAccessOpt.slowArrayAccess(); arrayIndexAccessOpt.optimizeArrayAccess(); } } d. 將常量聲明為final static或者final,這樣編譯器就可以將它們內(nèi)聯(lián)并且在編譯時(shí)就預(yù)先計(jì)算好它們的值 e. 用switch-case替代冗長(zhǎng)的if-else-if 測(cè)試代碼如下,但優(yōu)化效果不明顯: import util.StopWatch; /** * 優(yōu)化效果不明顯 * @author jxqlovejava * */ public class IfElseOptimization { public void slowIfElse() { StopWatch sw = new StopWatch("slowIfElse"); sw.start(); for(int k = 0; k < 2000000000; k++) { int i = 9; if(i == 0) { } else if(i == 1) { } else if(i == 2) { } else if(i == 3) { } else if(i == 4) { } else if(i == 5) { } else if(i == 6) { } else if(i == 7) { } else if(i == 8) { } else if(i == 9) { } } sw.end(); sw.printEclapseDetail(); } public void optimizeIfElse() { StopWatch sw = new StopWatch("optimizeIfElse"); sw.start(); for(int k = 0; k < 2000000000; k++) { int i = 9; switch(i) { case 0: break; case 1: break; case 2: break; case 3: break; case 4: break; case 5: break; case 6: break; case 7: break; case 8: break; case 9: break; default: } } sw.end(); sw.printEclapseDetail(); } public static void main(String[] args) { IfElseOptimization ifElseOpt = new IfElseOptimization(); ifElseOpt.slowIfElse(); ifElseOpt.optimizeIfElse(); } } f. 如果冗長(zhǎng)的if-else-if無法被switch-case替換,那么可以使用查表法優(yōu)化
1.2 域和變量?jī)?yōu)化 a. 訪問局部變量和方法參數(shù)比訪問實(shí)例變量和類變量要快得多 b. 在嵌套的語(yǔ)句塊內(nèi)部或者循環(huán)內(nèi)部生命變量并沒有什么運(yùn)行時(shí)開銷,所以應(yīng)該盡量將變量聲明得越本地化(local)越好,這甚至?xí)兄诰幾g器優(yōu)化你的程序,也提高了代碼可讀性
1.3 字符串操作優(yōu)化 a. 避免頻繁地通過+運(yùn)算符進(jìn)行字符串拼接(老生常談),因?yàn)樗鼤?huì)不斷地生成新字符串對(duì)象,而生成字符串對(duì)象不僅耗時(shí)而且耗內(nèi)存(一些OOM錯(cuò)誤是由這種場(chǎng)景導(dǎo)致的)。而要使用StringBuilder的append方法 b. 但對(duì)于這種String s = "hello" + " world"; 編譯器會(huì)幫我們優(yōu)化成String s = "hello world";實(shí)際上只生成了一個(gè)字符串對(duì)象"hello world",所以這種沒關(guān)系
1.4 常量數(shù)組優(yōu)化 a. 避免在方法內(nèi)部聲明一個(gè)只包含常量的數(shù)組,應(yīng)該把數(shù)組提為全局常量數(shù)組,這樣可以避免每次方法調(diào)用都生成數(shù)組對(duì)象的時(shí)間開銷 b. 對(duì)于一些耗時(shí)的運(yùn)算比如除法運(yùn)算、MOD運(yùn)算、Log運(yùn)算,可以采用預(yù)先計(jì)算值來優(yōu)化
1.5 方法優(yōu)化 a. 被private final static修飾的方法運(yùn)行更快
1.6 排序和查找優(yōu)化 a. 除非數(shù)組或者鏈表元素很少,否則不要使用選擇排序、冒泡排序和插入排序。使用堆排序、歸并排序和快速排序。 b. 更推薦的做法是使用JDK標(biāo)準(zhǔn)API內(nèi)置的排序方法,時(shí)間復(fù)雜度為O(nlog(n))
1.7 Exception優(yōu)化 a. new Exception(...)會(huì)構(gòu)建一個(gè)異常堆棧路徑,非常耗費(fèi)時(shí)間和空間,尤其是在遞歸調(diào)用的時(shí)候。創(chuàng)建異常對(duì)象一般比創(chuàng)建普通對(duì)象要慢30-100倍。自定義異常類時(shí),層級(jí)不要太多。 b. 可以通過重寫Exception類的fillInStackTrace方法而避免過長(zhǎng)堆棧路徑的生成 class MyException extends Exception { /** * */ private static final long serialVersionUID = -1515205444433997458L; public Throwable fillInStackTrace() { return this; } } c. 所以有節(jié)制地使用異常,不要將異常用于控制流程、終止循環(huán)等。只將異常用于意外和錯(cuò)誤場(chǎng)景(文件找不到、非法輸入格式等)。盡量復(fù)用之前創(chuàng)建的異常對(duì)象。
1.8 集合類優(yōu)化 a. 如果使用HashSet或者HashMap,確保key對(duì)象有一個(gè)快速合理的hashCode實(shí)現(xiàn),并且要遵守hashCode和equals實(shí)現(xiàn)規(guī)約 d. 避免頻繁調(diào)用LinkedList<T>或ArrayList<T>的remove(Object o)方法,它們會(huì)進(jìn)行線性查找 f. 最好避免遺留的集合類如Vector、Hashtable和Stack,因?yàn)樗鼈兊乃蟹椒ǘ加胹ynchronized修飾,每個(gè)方法調(diào)用都必須先獲得對(duì)象內(nèi)置鎖,增加了運(yùn)行時(shí)開銷。如果確實(shí)需要一個(gè)同步的集合,使用synchronziedCollection以及其他類似方法,或者使用ConcurrentHashMap
1.9 IO優(yōu)化 a. 使用緩沖輸入和輸出(BufferedReader、BufferedWriter、BufferedInputStream和BufferedOutputStream)可以提升IO速度20倍的樣子,我以前寫過一個(gè)讀取大文件(9M多,64位Mac系統(tǒng),8G內(nèi)存)的代碼測(cè)試?yán)?,如下?/p> import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import util.StopWatch; public class ReadFileDemos { public static void main(String[] args) throws IOException { String filePath = "C:\\Users\\jxqlovejava\\workspace\\PerformanceOptimization\\test.txt"; InputStream in = null; BufferedInputStream bis = null; File file = null; StopWatch sw = new StopWatch(); sw.clear(); sw.setTaskName("一次性讀取到字節(jié)數(shù)組+BufferedReader"); sw.start(); file = new File(filePath); in = new FileInputStream(filePath); BufferedReader br = new BufferedReader(new InputStreamReader(in)); char[] charBuf = new char[(int) file.length()]; br.read(charBuf); br.close(); in.close(); sw.end(); sw.printEclapseDetail(); sw.clear(); sw.setTaskName("一次性讀取到字節(jié)數(shù)組"); sw.start(); in = new FileInputStream(filePath); byte[] buf = new byte[in.available()]; in.read(buf);// read(byte[] buf)方法重載 in.close(); for (byte c : buf) { } sw.end(); sw.printEclapseDetail(); sw.clear(); sw.setTaskName("BufferedInputStream逐字節(jié)讀取"); sw.start(); in = new FileInputStream(filePath); bis = new BufferedInputStream(in); int b; while ((b = bis.read()) != -1); in.close(); bis.close(); sw.end(); sw.printEclapseDetail(); sw.clear(); sw.setTaskName("BufferedInputStream+DataInputStream分批讀取到字節(jié)數(shù)組"); sw.start(); in = new FileInputStream(filePath); bis = new BufferedInputStream(in); DataInputStream dis = new DataInputStream(bis); byte[] buf2 = new byte[1024*4]; // 4k per buffer int len = -1; StringBuffer sb = new StringBuffer(); while((len=dis.read(buf2)) != -1 ) { // response.getOutputStream().write(b, 0, len); sb.append(new String(buf2)); } dis.close(); bis.close(); in.close(); sw.end(); sw.printEclapseDetail(); sw.clear(); sw.setTaskName("FileInputStream逐字節(jié)讀取"); sw.start(); in = new FileInputStream(filePath); int c; while ((c = in.read()) != -1); in.close(); sw.end(); sw.printEclapseDetail(); } } 結(jié)果如下: 一次性讀取到字節(jié)數(shù)組+BufferedReader任務(wù)耗時(shí)(毫秒):121 一次性讀取到字節(jié)數(shù)組任務(wù)耗時(shí)(毫秒):23 BufferedInputStream逐字節(jié)讀取任務(wù)耗時(shí)(毫秒):408 BufferedInputStream+DataInputStream分批讀取到字節(jié)數(shù)組任務(wù)耗時(shí)(毫秒):147 FileInputStream逐字節(jié)讀取任務(wù)耗時(shí)(毫秒):38122 b. 將文件壓縮后存到磁盤,這樣讀取時(shí)更快,雖然會(huì)耗費(fèi)額外的CPU來進(jìn)行解壓縮。網(wǎng)絡(luò)傳輸時(shí)也盡量壓縮后傳輸。Java中壓縮有關(guān)的類:ZipInputStream、ZipOutputStream、GZIPInputStream和GZIPOutputStream
1.10 對(duì)象創(chuàng)建優(yōu)化 a. 如果程序使用很多空間(內(nèi)存),它一般也將耗費(fèi)更多的時(shí)間:對(duì)象分配和垃圾回收需要耗費(fèi)時(shí)間、使用過多內(nèi)存可能導(dǎo)致不能很好利用CPU緩存甚至可能需要使用虛存(訪問磁盤而不是RAM)。而且根據(jù)JVM的垃圾回收器的不同,使用太多內(nèi)存可能導(dǎo)致長(zhǎng)時(shí)間的回收停頓,這對(duì)于交互式系統(tǒng)和實(shí)時(shí)應(yīng)用是不能忍受的。 b. 對(duì)象創(chuàng)建需要耗費(fèi)時(shí)間(分配內(nèi)存、初始化、垃圾回收等),所以避免不必要的對(duì)象創(chuàng)建。但是記住不要輕易引入對(duì)象池除非確實(shí)有必要。大部分情況,使用對(duì)象池僅僅會(huì)導(dǎo)致代碼量增加和維護(hù)代價(jià)增大,并且對(duì)象池可能引入一些微妙的問題 c. 不要?jiǎng)?chuàng)建一些不會(huì)被使用到的對(duì)象
1.11 數(shù)組批量操作優(yōu)化 數(shù)組批量操作比對(duì)數(shù)組進(jìn)行for循環(huán)要快得多,部分原因在于數(shù)組批量操作只需進(jìn)行一次邊界檢查,而對(duì)數(shù)組進(jìn)行for循環(huán),每一次循環(huán)都必須檢查邊界。 a. System.arrayCopy(src, si, dst, di, n) 從源數(shù)組src拷貝片段[si...si+n-1]到目標(biāo)數(shù)組dst[di...di+n-1] b. boolean Arrays.equals(arr1, arr2) 返回true,當(dāng)且僅當(dāng)arr1和arr2的長(zhǎng)度相等并且元素一一對(duì)象相等(equals) c. void Arrays.fill(arr, x) 將數(shù)組arr的所有元素設(shè)置為x d. void Arrays.fill(arr, i, j x) 將數(shù)組arr的[i..j-1]索引處的元素設(shè)置為x e. int Arrays.hashCode(arr) 基于數(shù)組的元素計(jì)算數(shù)組的hashcode
1.12 科學(xué)計(jì)算優(yōu)化 Colt(http://acs./software/colt/)是一個(gè)科學(xué)計(jì)算開源庫(kù),可以用于線性代數(shù)、稀疏和緊湊矩陣、數(shù)據(jù)分析統(tǒng)計(jì),隨機(jī)數(shù)生成,數(shù)組算法,代數(shù)函數(shù)和復(fù)數(shù)等。
1.13 反射優(yōu)化 a. 通過反射創(chuàng)建對(duì)象、訪問屬性、調(diào)用方法比一般的創(chuàng)建對(duì)象、訪問屬性和調(diào)用方法要慢得多 b. 訪問權(quán)限檢查(反射調(diào)用private方法或者反射訪問private屬性時(shí)會(huì)進(jìn)行訪問權(quán)限檢查,需要通過setAccessible(true)來達(dá)到目的)可能會(huì)讓反射調(diào)用方法更慢,可以通過將方法聲明為public來比避免一些開銷。這樣做之后可以提高8倍。
1.14 編譯器和JVM平臺(tái)優(yōu)化 a. Sun公司的HotSpot Client JVM會(huì)進(jìn)行一些代碼優(yōu)化,但一般將快速啟動(dòng)放在主動(dòng)優(yōu)化之前進(jìn)行考慮 b. Sun公司的HotSpot Server JVM(-server選項(xiàng),Windows平臺(tái)無效)會(huì)進(jìn)行一些主動(dòng)優(yōu)化,但可能帶來更長(zhǎng)的啟動(dòng)延遲 c. IBM的JVM也會(huì)進(jìn)行一些主動(dòng)優(yōu)化 d. J2ME和一些手持設(shè)備(如PDA)不包含JIT編譯,很可能不會(huì)進(jìn)行任何優(yōu)化
1.15 Profile
2. 空間優(yōu)化 2.1 堆(對(duì)象)和棧(方法參數(shù)、局部變量等)。堆被所有線程共享,但棧被每個(gè)線程獨(dú)享
2.2 空間消耗的三個(gè)重要方面是:Allocation Rate(分配頻率)、Retention(保留率)和Fragmentation(內(nèi)存碎片)
附上StopWatch計(jì)時(shí)工具類: /** * 秒表類,用于計(jì)算執(zhí)行時(shí)間 * 注意該類是非線程安全的 * @author jxqlovejava * */ public class StopWatch { private static final String DEFAULT_TASK_NAME = "defaultTask"; private String taskName; private long start, end; private boolean hasStarted, hasEnded; // 時(shí)間單位枚舉:毫秒、秒和分鐘 public enum TimeUnit { MILLI, SECOND, MINUTE } public StopWatch() { this(DEFAULT_TASK_NAME); } public StopWatch(String taskName) { this.taskName = StringUtil.isEmpty(taskName) ? DEFAULT_TASK_NAME : taskName; } public void start() { start = System.currentTimeMillis(); hasStarted = true; } public void end() { if(!hasStarted) { throw new IllegalOperationException("調(diào)用StopWatch的end()方法之前請(qǐng)先調(diào)用start()方法"); } end = System.currentTimeMillis(); hasEnded = true; } public void clear() { this.start = 0; this.end = 0; this.hasStarted = false; this.hasEnded = false; } /** * 獲取總耗時(shí),單位為毫秒 * @return 消耗的時(shí)間,單位為毫秒 */ public long getEclapsedMillis() { if(!hasEnded) { throw new IllegalOperationException("請(qǐng)先調(diào)用end()方法"); } return (end-start); } /** * 獲取總耗時(shí),單位為秒 * @return 消耗的時(shí)間,單位為秒 */ public long getElapsedSeconds() { return this.getEclapsedMillis() / 1000; } /** * 獲取總耗時(shí),單位為分鐘 * @return 消耗的時(shí)間,單位為分鐘 */ public long getElapsedMinutes() { return this.getEclapsedMillis() / (1000*60); } public void setTaskName(String taskName) { this.taskName = StringUtil.isEmpty(taskName) ? DEFAULT_TASK_NAME : taskName; } public String getTaskName() { return this.taskName; } /** * 輸出任務(wù)耗時(shí)情況,單位默認(rèn)為毫秒 */ public void printEclapseDetail() { this.printEclapseDetail(TimeUnit.MILLI); } /** * 輸出任務(wù)耗時(shí)情況,可以指定毫秒、秒和分鐘三種時(shí)間單位 * @param timeUnit 時(shí)間單位 */ public void printEclapseDetail(TimeUnit timeUnit) { switch(timeUnit) { case MILLI: System.out.println(this.getTaskName() + "任務(wù)耗時(shí)(毫秒):" + this.getEclapsedMillis()); break; case SECOND: System.out.println(this.getTaskName() + "任務(wù)耗時(shí)(秒):" + this.getElapsedSeconds()); break; case MINUTE: System.out.println(this.getTaskName() + "任務(wù)耗時(shí)(分鐘):" + this.getElapsedMinutes()); break; default: System.out.println(this.getTaskName() + "任務(wù)耗時(shí)(毫秒):" + this.getEclapsedMillis()); } } } |
|