Java程序性能優化Tip

本博客是閱讀<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());
        }
    }
}
相關文章
相關標籤/搜索