本博客是閱讀<java time and space performance tips>這本小書后整理的讀書筆記性質博客,增加了幾個測試代碼,代碼可以在此下載:java時空間性能優化測試代碼 ,文件StopWatch是一個秒表計時工具類,它的代碼在文末。
1. 時間優化
1.1 標准代碼優化
a. 將循環不變量的計算移出循環
我寫了一個測試例子如下:
import util.StopWatch; /** * 循環優化: * 除了本例中將循環不變量移出循環外,還有將忙循環放在外層 * @author jxqlovejava * */ public class LoopOptimization { public int size() { try { Thread.sleep(200); // 模擬耗時操作 } 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(); // 將循環不變量移出循環 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(); } }
測試結果如下:
slowLoop任務耗時(毫秒):2204
optimizeLoop任務耗時(毫秒):211
可以很清楚地看到不提出循環不變量比提出循環不變量要慢10倍,在循環次數越大並且循環不變量的計算越耗時的情況下,這種優化會越明顯。
b. 避免重復計算
這條太常見,不舉例了
c. 盡量減少數組索引訪問次數,數組索引訪問比一般的變量訪問要慢得多
數組索引訪問比如int i = array[0];需要進行一次數組索引訪問(和數組索引訪問需要檢查索引是否越界有關系吧)。這條Tip經過我的測試發現效果不是很明顯(但的確有一些時間性能提升),可能在數組是大數組、循環次數比較多的情況下更明顯。測試代碼如下:
import util.StopWatch; /** * 數組索引訪問優化,尤其針對多維數組 * 這條優化技巧對時間性能提升不太明顯,而且可能降低代碼可讀性 * @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 } }; // 二維數組 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,這樣編譯器就可以將它們內聯並且在編譯時就預先計算好它們的值
e. 用switch-case替代冗長的if-else-if
測試代碼如下,但優化效果不明顯:
import util.StopWatch; /** * 優化效果不明顯 * @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. 如果冗長的if-else-if無法被switch-case替換,那么可以使用查表法優化
1.2 域和變量優化
a. 訪問局部變量和方法參數比訪問實例變量和類變量要快得多
b. 在嵌套的語句塊內部或者循環內部生命變量並沒有什么運行時開銷,所以應該盡量將變量聲明得越本地化(local)越好,這甚至會有助於編譯器優化你的程序,也提高了代碼可讀性
1.3 字符串操作優化
a. 避免頻繁地通過+運算符進行字符串拼接(老生常談),因為它會不斷地生成新字符串對象,而生成字符串對象不僅耗時而且耗內存(一些OOM錯誤是由這種場景導致的)。而要使用StringBuilder的append方法
b. 但對於這種String s = "hello" + " world"; 編譯器會幫我們優化成String s = "hello world";實際上只生成了一個字符串對象"hello world",所以這種沒關系
c. 避免頻繁地對字符串對象調用substring和indexOf方法
1.4 常量數組優化
a. 避免在方法內部聲明一個只包含常量的數組,應該把數組提為全局常量數組,這樣可以避免每次方法調用都生成數組對象的時間開銷
b. 對於一些耗時的運算比如除法運算、MOD運算、Log運算,可以采用預先計算值來優化
1.5 方法優化
a. 被private final static修飾的方法運行更快
b. 如果確定一個類的方法不需要被子類重寫,那么將方法用final修飾,這樣更快
c. 盡量使用接口作為方法參數或者其他地方,而不是接口的具體實現,這樣也更快
1.6 排序和查找優化
a. 除非數組或者鏈表元素很少,否則不要使用選擇排序、冒泡排序和插入排序。使用堆排序、歸並排序和快速排序。
b. 更推薦的做法是使用JDK標准API內置的排序方法,時間復雜度為O(nlog(n))
對數組排序用Arrays.sort(它的實現代碼使用改良的快速排序算法,不會占用額外內存空間,但是不穩定)
對鏈表排序用Collections.sort(穩定算法,但會使用額外內存空間)
c. 避免對數組和鏈表進行線性查找,除非你明確知道要查找的次數很少或者數組和鏈表長度很短
對於數組使用Arrays.binarySearch,但前提是數組已經有序,並且數組如包含多個要查找的元素,不能保證返回哪一個的index
對於鏈表使用Collections.binarySearch,前提也是鏈表已經有序
使用哈希查找:HashSet<T>、HashMap<K, V>等
使用二叉查找樹:TreeSet<T>和TreeMap<K, V>,一般要提供一個Comparator作為構造函數參數,如果不提供則按照自然順序排序
1.7 Exception優化
a. new Exception(...)會構建一個異常堆棧路徑,非常耗費時間和空間,尤其是在遞歸調用的時候。創建異常對象一般比創建普通對象要慢30-100倍。自定義異常類時,層級不要太多。
b. 可以通過重寫Exception類的fillInStackTrace方法而避免過長堆棧路徑的生成
class MyException extends Exception { /** * */ private static final long serialVersionUID = -1515205444433997458L; public Throwable fillInStackTrace() { return this; } }
c. 所以有節制地使用異常,不要將異常用於控制流程、終止循環等。只將異常用於意外和錯誤場景(文件找不到、非法輸入格式等)。盡量復用之前創建的異常對象。
1.8 集合類優化
a. 如果使用HashSet或者HashMap,確保key對象有一個快速合理的hashCode實現,並且要遵守hashCode和equals實現規約
b. 如果使用TreeSet<T>或者TreeMap<K, V>,確保key對象有一個快速合理的compareTo實現;或者在創建TreeSet<T>或者TreeMap<K, V>時顯式提供一個Comparator<T>
c. 對鏈表遍歷優先使用迭代器遍歷或者for(T x: lst),for(T x: lst)隱式地使用了迭代器來遍歷鏈表。而對於數組遍歷優先使用索引訪問:for(int i = 0; i < array.length; i++)
d. 避免頻繁調用LinkedList<T>或ArrayList<T>的remove(Object o)方法,它們會進行線性查找
e. 避免頻繁調用LinkedList<T>的add(int i, T x)和remove(int i)方法,它們會執行線性查找來確定索引為i的元素
f. 最好避免遺留的集合類如Vector、Hashtable和Stack,因為它們的所有方法都用synchronized修飾,每個方法調用都必須先獲得對象內置鎖,增加了運行時開銷。如果確實需要一個同步的集合,使用synchronziedCollection以及其他類似方法,或者使用ConcurrentHashMap
1.9 IO優化
a. 使用緩沖輸入和輸出(BufferedReader、BufferedWriter、BufferedInputStream和BufferedOutputStream)可以提升IO速度20倍的樣子,我以前寫過一個讀取大文件(9M多,64位Mac系統,8G內存)的代碼測試例子,如下:
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("一次性讀取到字節數組+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("一次性讀取到字節數組"); 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逐字節讀取"); 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分批讀取到字節數組"); 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逐字節讀取"); sw.start(); in = new FileInputStream(filePath); int c; while ((c = in.read()) != -1); in.close(); sw.end(); sw.printEclapseDetail(); } }
結果如下:
一次性讀取到字節數組+BufferedReader任務耗時(毫秒):121 一次性讀取到字節數組任務耗時(毫秒):23 BufferedInputStream逐字節讀取任務耗時(毫秒):408 BufferedInputStream+DataInputStream分批讀取到字節數組任務耗時(毫秒):147 FileInputStream逐字節讀取任務耗時(毫秒):38122
b. 將文件壓縮后存到磁盤,這樣讀取時更快,雖然會耗費額外的CPU來進行解壓縮。網絡傳輸時也盡量壓縮后傳輸。Java中壓縮有關的類:ZipInputStream、ZipOutputStream、GZIPInputStream和GZIPOutputStream
1.10 對象創建優化
a. 如果程序使用很多空間(內存),它一般也將耗費更多的時間:對象分配和垃圾回收需要耗費時間、使用過多內存可能導致不能很好利用CPU緩存甚至可能需要使用虛存(訪問磁盤而不是RAM)。而且根據JVM的垃圾回收器的不同,使用太多內存可能導致長時間的回收停頓,這對於交互式系統和實時應用是不能忍受的。
b. 對象創建需要耗費時間(分配內存、初始化、垃圾回收等),所以避免不必要的對象創建。但是記住不要輕易引入對象池除非確實有必要。大部分情況,使用對象池僅僅會導致代碼量增加和維護代價增大,並且對象池可能引入一些微妙的問題
c. 不要創建一些不會被使用到的對象
1.11 數組批量操作優化
數組批量操作比對數組進行for循環要快得多,部分原因在於數組批量操作只需進行一次邊界檢查,而對數組進行for循環,每一次循環都必須檢查邊界。
a. System.arrayCopy(src, si, dst, di, n) 從源數組src拷貝片段[si...si+n-1]到目標數組dst[di...di+n-1]
b. boolean Arrays.equals(arr1, arr2) 返回true,當且僅當arr1和arr2的長度相等並且元素一一對象相等(equals)
c. void Arrays.fill(arr, x) 將數組arr的所有元素設置為x
d. void Arrays.fill(arr, i, j x) 將數組arr的[i..j-1]索引處的元素設置為x
e. int Arrays.hashCode(arr) 基於數組的元素計算數組的hashcode
1.12 科學計算優化
Colt(http://acs.lbl.gov/software/colt/)是一個科學計算開源庫,可以用於線性代數、稀疏和緊湊矩陣、數據分析統計,隨機數生成,數組算法,代數函數和復數等。
1.13 反射優化
a. 通過反射創建對象、訪問屬性、調用方法比一般的創建對象、訪問屬性和調用方法要慢得多
b. 訪問權限檢查(反射調用private方法或者反射訪問private屬性時會進行訪問權限檢查,需要通過setAccessible(true)來達到目的)可能會讓反射調用方法更慢,可以通過將方法聲明為public來比避免一些開銷。這樣做之后可以提高8倍。
1.14 編譯器和JVM平台優化
a. Sun公司的HotSpot Client JVM會進行一些代碼優化,但一般將快速啟動放在主動優化之前進行考慮
b. Sun公司的HotSpot Server JVM(-server選項,Windows平台無效)會進行一些主動優化,但可能帶來更長的啟動延遲
c. IBM的JVM也會進行一些主動優化
d. J2ME和一些手持設備(如PDA)不包含JIT編譯,很可能不會進行任何優化
1.15 Profile
2. 空間優化
2.1 堆(對象)和棧(方法參數、局部變量等)。堆被所有線程共享,但棧被每個線程獨享
2.2 空間消耗的三個重要方面是:Allocation Rate(分配頻率)、Retention(保留率)和Fragmentation(內存碎片)
Allocation Rate是程序創建新對象的頻率,頻率越高耗費的時間和空間越多。
Retention是存活的堆數據數量。這個值越高需要耗費越多的空間和時間(垃圾回收器執行分配和去分配工作時需要進行更多的管理工作)
Fragmentation:內存碎片是指小塊無法使用的內存。如果一直持續創建大對象,可能會引起過多的內存碎片。從而需要更多的時間分配內存(因為要查找一個足夠大的連續可用內存塊),並且會浪費更多的空間因為內存碎片無法被利用。當然某些GC算法可以避免過多內存碎片的產生,但相應的算法代價也較高。
2.3 內存泄露
2.4 垃圾回收器的種類(分代收集、標記清除、引用計數、增量收集、壓縮...)對Allocation Rate、Retention和Fragmentation的時間空間消耗影響很大
2.5 對象延遲創建
附上StopWatch計時工具類:
/** * 秒表類,用於計算執行時間 * 注意該類是非線程安全的 * @author jxqlovejava * */ public class StopWatch { private static final String DEFAULT_TASK_NAME = "defaultTask"; private String taskName; private long start, end; private boolean hasStarted, hasEnded; // 時間單位枚舉:毫秒、秒和分鍾 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("調用StopWatch的end()方法之前請先調用start()方法"); } end = System.currentTimeMillis(); hasEnded = true; } public void clear() { this.start = 0; this.end = 0; this.hasStarted = false; this.hasEnded = false; } /** * 獲取總耗時,單位為毫秒 * @return 消耗的時間,單位為毫秒 */ public long getEclapsedMillis() { if(!hasEnded) { throw new IllegalOperationException("請先調用end()方法"); } return (end-start); } /** * 獲取總耗時,單位為秒 * @return 消耗的時間,單位為秒 */ public long getElapsedSeconds() { return this.getEclapsedMillis() / 1000; } /** * 獲取總耗時,單位為分鍾 * @return 消耗的時間,單位為分鍾 */ 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; } /** * 輸出任務耗時情況,單位默認為毫秒 */ public void printEclapseDetail() { this.printEclapseDetail(TimeUnit.MILLI); } /** * 輸出任務耗時情況,可以指定毫秒、秒和分鍾三種時間單位 * @param timeUnit 時間單位 */ public void printEclapseDetail(TimeUnit timeUnit) { switch(timeUnit) { case MILLI: System.out.println(this.getTaskName() + "任務耗時(毫秒):" + this.getEclapsedMillis()); break; case SECOND: System.out.println(this.getTaskName() + "任務耗時(秒):" + this.getElapsedSeconds()); break; case MINUTE: System.out.println(this.getTaskName() + "任務耗時(分鍾):" + this.getElapsedMinutes()); break; default: System.out.println(this.getTaskName() + "任務耗時(毫秒):" + this.getEclapsedMillis()); } } }