Java性能優化經驗總結


在JAVA程序中,性能問題的大部分原因並不在於JAVA語言,而是程序中代碼的編寫習慣。良好的編碼習慣非常重要,能夠顯著地提升程序性能;反之一些不好的習慣會極大地影響性能。關於Java代碼優化的文章很多,本文是一個經驗總結文章,通過網上的一些文章和自己多年的使用經驗對Java的使用進行總結。

循環

減少重複計算

在循環中,經常有對長度的計算,如下所示:

for(int i = 0; i < list.size(); i++)

由於在每次循環的時, i < arr.length() 都需要調用 arr.length() 函數,所以此函數實際上是被多次調用了,影響實際的效率,所以可以修改爲:

for(int i = 0, len = list.size(); i < len; i++)

通過在初始化時將長度的值直接賦給變量 len 從而減少了重複計算,提高的計算效率。

基礎知識

使用移位來代替除法操作

在幾乎所有的程序設計語言中(包括Java),除法都是一個很耗時的計算操作,這是因爲除法本身的特性所決定的。但是移位操作 >><< 就相對要快得多。這是因爲移位操作在底層只是對一個變量的各位進行左右的移動,操作很簡單,所以計算性能非常快。因此,在代碼層面,由於是2進制,所以除以(或乘以)2的階乘都可以使用位移操作來完成。

如下所示,

int num = a / 4; 
int num = a / 8;
int num = a * 4; 
int num = a * 8;

可以修改爲:

int num = a >> 2; 
int num = a >> 3;
int num = a << 2; 
int num = a << 3;

這樣計算效率能夠極大地提高。

ArrayList & LinkedList

一句話:隨機讀寫或查詢使用ArrayList,數據添加刪除的操作使用LinkedList。

數組複製使用System.arraycopy()代替通過來循環

因爲System.arraycopy() 要比通過循環來複制數組快的多。

緩衝經常使用對象

所謂的緩衝是指在內存中保持已經生成的數據,並進行再利用,從而減少數據的讀取或生成的時間。舉例來說,有一張幾M大的圖片文件,在使用之前需要從硬盤上加載,而這個加載時間一般幾十微秒。這張圖片需要多次使用,可以保持在內存中,這樣就避免了多次讀取所需要的時間。同樣的,數據庫中的數據,或者需要計算的數據,都可以這樣緩衝。但是這樣也需要付出內存的代價,即緩衝需要佔據一定的內存空間,如果數據量比較大的情況下,在使用緩衝的時候要根據實際情況進行優化。

變量

避免隨意使用靜態變量

當某個對象被定義爲static變量所引用,那麼GC通常是不會回收這個對象所佔有的內存,如

public class A{ 
        private static B b = new B(); 
}

此時靜態變量b的生命週期與A類同步,如果A類不會卸載,那麼b對象會常駐內存,直到程序終止。

多使用局部變量

調用方法時傳遞的參數以及在調用中創建的臨時變量都保存在棧(Stack)中,速度較快;其他變量,如靜態變量、實例變量等,都在堆(Heap)中創建,速度較慢。

避免頻繁創建對象

儘量避免在經常調用的方法,循環中new對象,由於系統不僅要花費時間來創建對象,而且還要花時間對這些對象進行垃圾回收和處理,在我們可以控制的範圍內,最大限度地重用對象,最好能用基本的數據類型或數組來替代對象。

充分利用關鍵字 final

帶有final修飾符的類是不可派生的。在JAVA核心API中,有許多應用final的例子,例如java、lang、String,爲String類指定final防止了使用者覆蓋length()方法。另外,如果一個類是final的,則該類所有方法都是final的。java編譯器會尋找機會內聯(inline)所有的final方法(這和具體的編譯器實現有關),此舉能夠使性能平均提高50%。
如:讓訪問實例內變量的getter/setter方法變成”final:
簡單的getter/setter方法應該被置成final,這會告訴編譯器,這個方法不會被重載,所以,可以變成”inlined”,例子:

class MAF {
    private int _size; 
    public void setSize(int size){
        _size = size; 
    }
}

我們可以在 setSize 方法前追加 final,推薦寫法如下:

class DAF_fixed {
    private int _size; 
    final public void setSize (int size) {     
        _size = size; 
    } 
}

避免重複初始化變量

默認情況下,調用類的構造函數時,java會把變量初始化成確定的值,所有的對象被設置成null,整數變量設置成0,float和double變量設置成0.0,邏輯值設置成false。當一個類從另一個類派生時,這一點尤其應該注意,因爲用new關鍵字創建一個對象時,構造函數鏈中的所有構造函數都會被自動調用。
這裏有個注意,給成員變量設置初始值但需要調用其他方法的時候,最好放在一個方法。比如initXXX()中,因爲直接調用某方法賦值可能會因爲類尚未初始化而拋空指針異常,如:public int state = this.getState()。

在合適的場合使用單例

使用單例可以讓一個比較重的類只生成一個對象,從而減輕加載的負擔,縮短加載的時間,提高加載的效率,但並不是所有地方都適用於單例,簡單來說,單例主要適用於以下三個方面:
第一,控制資源的使用,通過線程同步來控制資源的併發訪問;
第二,控制實例的產生,以達到節約資源的目的;
第三,控制數據共享,在不建立直接關聯的條件下,讓多個不相關的進程或線程之間實現通信。

其他

避免在循環中使用Try/Catch語句

應把Try/Catch放在循環最外層,因爲Error是獲取系統錯誤的類,或者說是虛擬機錯誤的類。不是所有的錯誤Exception都能獲取到的,虛擬機報錯Exception就獲取不到,必須用Error獲取。

慎用異常

當創建一個異常時,需要收集一個棧跟蹤(stack track),這個棧跟蹤用於描述異常是在何處創建的。構建這些棧跟蹤時需要爲運行時棧做一份快照,這一部分開銷很大。所以在使用異常時,建議在可能的情況下使用if進行判斷,從而減少異常的過度使用。

設置StringBuffer初始化容量以明顯提升性能

StringBuffer的默認容量爲16,當StringBuffer的容量達到最大容量時,它會將自身容量增加到當前的2倍+2,也就是2*n+2。當StringBuffer到達它的最大容量,就會創建一個新的對象數組,然後複製舊的對象數組,造成額外的時間浪費。所以,當數據量較大時,建議在初始化StringBuffer時設置一個不小於實際數據大小的初始化容量值以減少其增加長度所需要的額外時間開銷。

線程安全使用StringBuilder,線程不安全使用StringBuffer

java.lang.StringBuffer 是一個線程安全的可變字符序列,所以當多線程間可能有操作衝突的時候,使用此類。而StringBuilder是線程不安全的但是性能要優於StringBuffer(一般來說有10%-15%),所以在單線程或多線程無操作衝突時使用。

使用靜態方法

如果你沒有必要去訪問對象的外部,那麼就使你的方法成爲靜態方法。它會被更快地調用,因爲它不需要一個虛擬函數導向表。這同時也是一個很好的實踐,因爲它告訴你如何區分方法的性質,調用這個方法不會改變對象的狀態。

在finally塊中釋放資源

在使用try-catch-finally模型中,如果需要釋放資源,最好在finally塊中去做。這是因爲無論程序執行的結果如何,finally塊總是會執行的,從而保證資源的正確關閉。

指定StringBuffer的初始容量

StringBuffer 提供了一個帶有指定容量的構造函數,如 StringBuffer buffer = new StringBuffer(1000); 指定了初始化大小爲1000個字節。由於StringBuilder的初始大小是16字節,所以如果不指定初始化大小的話,在使用過程中需要不斷分配更多空間,從而花費更多額外的時間(多次分配比一次分配耗時多)。所以在能夠預測數據長度的情況下,提前指定其初始容量

儘量減少二維數組的使用???

二維數據佔用的內存空間比一維數組多得多,大概10倍以上。

字符串操作

對頻繁的字符串操作建議使用StringBuffer

由於字符串是固定不變的,所以在進行連接時,會創建大量的中間變量。因爲建議使用StringBuilder來代替,因爲它是可變長的,性能要好得多。

避免頻繁使用split

由於split支持正則表達式,所以效率不高。在需要頻繁調用的場合,建議使用自定義的函數,或者可以考慮使用第三方,如 Apache 的 StringUtils.split(string,char) 函數。

多線程

儘量使用 HashMap, ArrayList

雖然 HashTable和Vector是線程安全的,但是由於同步機制需要額外開銷,所以在單線程或多線程但無競爭訪問時,推薦使用線程不安全的HashMap和ArrayList。

----------------------- 以下內容未整理 --------------------------

其他

以下舉幾個實用優化的例子
一、避免在循環條件中使用複雜表達式
在不做編譯優化的情況下,在循環中,循環條件會被反覆計算,如果不使用複雜表達式,而使循環條件值不變的話,程序將會運行的更快。例子:


    import java.util.Vector; 

    class CEL { 
        void method (Vector vector) { 
            for (int i = 0; i < vector.size (); i++) 
            // Violation ; 
            // ... 
        } 
    }

    更正:

    class CEL_fixed { 

        void method (Vector vector) { 
            int size = vector.size ();
                for (int i = 0; i < size; i++) ; 
                // ... 
        } 
    }

二、爲’Vectors’ 和 'Hashtables’定義初始大小
JVM爲Vector擴充大小的時候需要重新創建一個更大的數組,將原原先數組中的內容複製過來,最後,原先的數組再被回收。可見Vector容量的擴大是一個頗費時間的事。
通常,默認的10個元素大小是不夠的。你最好能準確的估計你所需要的最佳大小。例子:

    import java.util.Vector; 

    public class DIC { 
        
        public void addObjects (Object[] o) { 
            // if length > 10, Vector needs to expand 
            for (int i = 0; i< o.length;i++) { 
                v.add(o); // capacity before it can add more elements.     
            } 
         } 
        public Vector v = new Vector(); 
        // no initialCapacity. 
    }

更正:
自己設定初始大小。

    public Vector v = new Vector(20); 
    public Hashtable hash = new Hashtable(10);

三、在finally塊中關閉Stream
程序中使用到的資源應當被釋放,以避免資源泄漏。這最好在finally塊中去做。不管程序執行的結果如何,finally塊總是會執行的,以確保資源的正確關閉。
四、使用’System.arraycopy ()'代替通過來循環複製數組
例子:

    public class IRB { 
        void method () { 
            int[] array1 = new int [100]; 
                for (int i = 0; i < array1.length; i++) { 
                    array1 [i] = i; 
                } 
            int[] array2 = new int [100]; 
                for (int i = 0; i < array2.length; i++) {     
                    array2 [i] = array1 [i]; // Violation 
                }
        } 

    }
更正:
    public class IRB { 

        void method () { 
            
            int[] array1 = new int [100]; 
                for (int i = 0; i < array1.length; i++) { 
                    array1 [i] = i; 
                } 
            int[] array2 = new int [100]; 
            System.arraycopy(array1, 0, array2, 0, 100); 
        } 
    }

五、讓訪問實例內變量的getter/setter方法變成”final”

簡單的getter/setter方法應該被置成final,這會告訴編譯器,這個方法不會被重載,所以,可以變成”inlined”,例子:

    class MAF { 
        public void setSize (int size) { 

            _size = size; 
        } 
        private int _size; 
    }
更正:
    class DAF_fixed { 
        
        final public void setSize (int size) { 

            _size = size; 
        } 

        private int _size; 
     }

七、在字符串相加的時候,使用 ’ ’ 代替 " ",如果該字符串只有一個字符的話
例子:


    public class STR { 

        public void method(String s) { 
            String string = s + "d" // violation. string = "abc" + "d" // violation. 
        } 
    }

更正:
將一個字符的字符串替換成’ ’

    public class STR { 
        public void method(String s) { 
            String string = s + 'd' string = "abc" + 'd' 
            } 
    }

以上僅是Java方面編程時的性能優化,性能優化大部分都是在時間、效率、代碼結構層次等方面的權衡,各有利弊,不要把上面內容當成教條,或許有些對我們實際工作適用,有些不適用,還望根據實際工作場景進行取捨,活學活用,變通爲宜。
·END·

參考文獻

[1] 猿校長, Java性能優化的50個細節(珍藏版, https://www.cnblogs.com/MrZhangxd/p/11332944.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章