本博客是閱讀<java time and space performance tips>這本小書後整理的讀書筆記性質博客,增長了幾個測試代碼,代碼能夠在此下載:java時空間性能優化測試代碼 ,文件StopWatch是一個秒錶計時工具類,它的代碼在文末。java
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. 避免重複計算app
這條太常見,不舉例了函數
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一次性讀取到字節數組任務耗時(毫秒):23BufferedInputStream逐字節讀取任務耗時(毫秒):408BufferedInputStream+DataInputStream分批讀取到字節數組任務耗時(毫秒):147FileInputStream逐字節讀取任務耗時(毫秒):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()); } } }