Java性能時間與空間消耗
一、減少時間消耗
- 標准代碼優化
(1) 將循環不變量的計算移出循環
| 例如:for (int i=0; i<size()*2; i++) { ... } ------> for (int i=0, stop=size()*2; i<stop; i++) { ... } |
(2) 避免重復計算
| 例如:if (birds.elementAt(i).isGrower()) ... if (birds.elementAt(i).isPullet()) ... ------> Bird bird = birds.elementAt(i); if (bird.isGrower()) ... if (bird.isPullet()) ... |
(3) 減少數據索引訪問次數,它比一般變量訪問要慢的多,尤其是在大數組和循環次數比較多的情況下
| 例如:double[] rowsum = new double[n]; for (int i=0; i<n; i++) for (int j=0; j<m; j++) rowsum[i] += arr[i][j]; -----------> double[] rowsum = new double[n]; for (int i=0; i<n; i++) { double[] arri = arr[i]; double sum = 0.0; for (int j=0; j<m; j++) sum += arri[j]; rowsum[i] = sum; } |
(4) 將常量聲明為final static 或 final ,以便編譯器可以將它們內聯且在編譯時預先計算好它們的值
| 例如:public final string BASE_NAME = “WH”; public final static string BASE_NAME = “WH”; |
(5) 用switch - case替代if-else-if,如果不能替代,通常用一個final static HashMap替代它
| 例如:int i = 9; if(i == 0) { } else if(i == 1) {…… } else if(i == 2) {…… } -------------> int i = 9; switch(i) { case 0: break; case 1: break; case 2: break; default: } |
(6) 不要使用另類的寫法,來完成一個while循環
| 例如:int year = 0; double sum = 200.0; double[] balance = new double[100]; while ((balance[year++] = sum *= 1.05) < 1000.0); |
2.作用域和變量優化
(1) 訪問局部變量、方法參數要比實例變量、類變量快的多
(2) 在嵌套語句塊內部和循環體內部聲明變量,沒有運行時的開銷,所以盡量將變量越本地化越好,有助於編譯器優化程序,同時也提高了代碼的可讀性
3.字符串優化
(1) 避免通過“+”建立字符串的多次拼接(消耗內存),盡量使用StringBuilder拼接字符串
| 例如:String s = ""; for (int i=0; i<n; i++) { s += "#" + i; } --------------> StringBuilder sbuf = new StringBuilder(); for (int i=0; i<n; i++) { sbuf.append("#").append(i); } String s = sbuf.toString(); |
(2) 下面的情況和StringBuilder 一樣,編譯器會自動把字符串串起來
| 例如:String s = "(" + x + ", " + y + ")"; |
(3) 避免頻繁地對字符串對象調用substring和index索引方法
4.常量數組優化
(1) 避免在方法內部聲明一個只包含常量的數組,應該把數組提為全局常量數組,這樣可以避免每次方法調用都生成數組對象的時間開銷
| 例如:public static int monthdays(int y, int m) { int[] monthlengths = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; return m == 2 && leapyear(y) ? 29 : monthlengths[m-1]; } ------------> private final static int[] monthlengths = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; public static int monthdays(int y, int m) { return m == 2 && leapyear(y) ? 29 : monthlengths[m-1]; } |
(2) 對一些耗時的運算,可以采用預先處理數組的內容來優化
| 例如:private final static double[] logFac = new double[100]; static { double logRes = 0.0; for (int i=1, stop=logFac.length; i<stop; i++) logFac[i] = logRes += Math.log(i); } public static double logBinom(int n, int k) { return logFac[n] - logFac[n-k] - logFac[k]; } |
5.方法優化
(1) 被private final static修飾的方法運行更快(根據實際業務使用)
(2) 如果確定一個類的方法不需要被子類重寫,那么將方法用final修飾,這樣更快
| 例如:class Foo { private int size; ... public final int getSize() { return size; } } |
(3) 使用接口作為方法參數來代替它的實現類,這樣更快
6.排序和查找優化
(1) 除非數組或者鏈表元素很少,否則不要使用選擇排序、冒泡排序和插入排序。而要使用堆排序(對於數組)、歸並排序(鏈表)和快速排序(對於數組)。
(2) 更推薦的做法是使用JDK標准API內置的排序方法,時間復雜度為O(nlog(n))
對數組排序用Arrays.sort(它的實現代碼使用改良的快速排序算法,不會占用額外內存空間,但是不穩定)
對鏈表排序用Collections.sort(穩定算法,但會使用額外內存空間)
(3) 避免對數組和鏈表進行線性查找,除非你明確知道要查找的次數很少或者數組和鏈表長度很短
對於數組使用Arrays.binarySearch,但前提是數組已經有序,並且數組如包含多個要查找的元素,不能保證返回哪一個的index
對於鏈表使用Collections.binarySearch,前提也是鏈表已經有序
使用哈希查找:HashSet<T>、HashMap<K, V>等
使用二叉查找樹:TreeSet<T>和TreeMap<K, V>,一般要提供一個Comparator作為構造函數參數,如果不提供則按照自然順序排序
7.異常優化
(1) new Exception(...)會構建一個異常堆棧路徑,非常耗費時間和空間,尤其是在遞歸調用的時候。創建異常對象一般比創建普通對象要慢30-100倍。自定義異常類時,層級不要太多。或者使用try-catch塊拋出異常更快。
(2) 可以通過重寫Exception類的fillInStackTrace方法而避免過長堆棧路徑的生成
| 例如:class MyException extends Exception { public Throwable fillInStackTrace() { return this; } } |
(3) 所以有節制地使用異常,不要將異常用於控制流程、終止循環等。只將異常用於意外和錯誤場景(文件找不到、非法輸入格式等)。盡量復用之前創建的異常對象。即,根據具體的情況需要時,再去自定義使用異常。
8.集合類優化
(1) 如果使用HashSet或者HashMap,確保key對象有一個快速合理的hashCode實現,並且要遵守hashCode和equals實現規約。
| 例如:class Person { private String name; private int id; Person(String name,int id) { this.name = name; this.id = id; } public void setName(String name){ …… } public String getName(){ …… } public void setId(int id){ …… } public int getId(){ …… } public int hashCode(){ return name.hashCode()+id; } public boolean equals(Object obj){ if(obj instanceof Person){ Person p = (Person)obj; return(name.equals(p.name) && id == p.id); } return super.equals(obj); } } |
(2) 如果使用TreeSet<T>或者TreeMap<K, V>,確保key對象有一個快速合理的compareTo實現;或者在創建TreeSet<T>或者TreeMap<K, V>時顯式提供一個Comparator<T>
| 例如:public class test_treeset { @SuppressWarnings("unchecked") public static void main(String[] args) { Set ts = new TreeSet(); ts.add(new Teacher("zhangsan", 1)); ts.add(new Teacher("lisi", 2)); ts.add(new Teacher("wangmazi", 3)); ts.add(new Teacher("wangwu",4)); ts.add(new Teacher("mazi", 3)); Iterator it = ts.iterator(); while (it.hasNext()) { System.out.println(it.next()); } } } class Teacher implements Comparable { int num; String name; Teacher(String name, int num) { this.num = num; this.name = name; } public String toString() { return "學號:" + num + "\t\t姓名:" + name; } //o中存放時的紅黑二叉樹中的節點,從根節點開始比較 public int compareTo(Object o) { Teacher ss = (Teacher) o; int result = num < ss.num ? 1 : (num == ss.num ? 0 : -1);//降序 //int result = num > ss.num ? 1 : (num == ss.num ? 0 : -1);//升序 if (result == 0) { result = name.compareTo(ss.name); } return result; } } |
(3) 對鏈表遍歷優先使用迭代器遍歷或者for(T x: lst),for(T x: lst)隱式地使用了迭代器來遍歷鏈表。而對於數組遍歷優先使用索引訪問:for(int i = 0; i < array.length; i++)
| 例如:int size = lst.size(); for (int i=0; i<size; i++) System.out.println(lst.get(i)); -----------> for (T x : lst) System.out.println(x); |
(4) 避免頻繁調用LinkedList<T>或ArrayList<T>的remove(Object o)方法,它們會進行線性查找
(5) 避免頻繁調用LinkedList<T>的add(int i, T x)和remove(int i)方法,它們會執行線性查找來確定索引為i的元素
(6) 最好避免遺留的集合類如Vector、Hashtable和Stack,因為它們的所有方法都用synchronized修飾,每個方法調用都必須先獲得對象內置鎖,增加了運行時開銷。如果確實需要一個同步的集合,使用synchronziedCollection以及其他類似方法,或者使用ConcurrentHashMap
(7) 集合類只能存儲引用類型數據,例如,Integer、Double、Float……
9.輸入輸出優化(IO)
(1) 使用緩沖輸入和輸出(BufferedReader、BufferedWriter、BufferedInputStream和BufferedOutputStream)可以提升IO速度
| 例如:BufferedReader br = new BufferedReader(new InputStreamReader(in)); |
(2) 將文件壓縮后存到磁盤,這樣讀取時更快,雖然會耗費額外的CPU來進行解壓縮。網絡傳輸時也盡量壓縮后傳輸。Java中壓縮有關的類:ZipInputStream、ZipOutputStream、GZIPInputStream和GZIPOutputStream
10.空間和對象創建優化
(1) 如果程序使用很多空間(內存),它一般也將耗費更多的時間:對象分配和垃圾回收需要耗費時間、使用過多內存可能導致不能很好利用CPU緩存甚至可能需要使用虛存(訪問磁盤而不是RAM)。而且根據JVM的垃圾回收器的不同,使用太多內存可能導致長時間的回收停頓,這對於交互式系統和實時應用是不能忍受的。
(2) 對象創建需要耗費時間(分配內存、初始化、垃圾回收等),所以避免不必要的對象創建。但是記住不要輕易引入對象池除非確實有必要。大部分情況,使用對象池僅僅會導致代碼量增加和維護代價增大,並且對象池可能引入一些微妙的問題。
(3) 不要創建一些不會被使用到的對象
11.數組批量化操作優化
數組批量操作比對數組進行for循環要快得多,部分原因在於數組批量操作只需進行一次邊界檢查,而對數組進行for循環,每一次循環都必須檢查邊界。
(1) System.arrayCopy(src, si, dst, di, n) 從源數組src拷貝片段[si...si+n-1]到目標數組dst[di...di+n-1]
(2) boolean Arrays.equals(arr1, arr2) 返回true,當且僅當arr1和arr2的長度相等並且元素一一對象相等(equals)
(3) void Arrays.fill(arr, x) 將數組arr的所有元素設置為x
(4) void Arrays.fill(arr, i, j x) 將數組arr的[i..j-1]索引處的元素設置為x
(5) int Arrays.hashCode(arr) 基於數組的元素計算數組的hashcode
* 12. 科學計算優化
Colt(http://acs.lbl.gov/software/colt/)是一個科學計算開源庫,可以用於線性代數、稀疏和緊湊矩陣、數據分析統計,隨機數生成,數組算法,代數函數和復數等。
*13.反射優化
(1) 通過反射創建對象、訪問屬性、調用方法比一般的創建對象、訪問屬性和調用方法要慢得多
(2) 訪問權限檢查(反射調用private方法或者反射訪問private屬性時會進行訪問權限檢查,需要通過setAccessible(true)來達到目的)可能會讓反射調用方法更慢,可以通過將方法聲明為public來比避免一些開銷。這樣做之后可以提高8倍。
14.編譯器和jVM平台優化
(1) Sun公司的HotSpot Client JVM會進行一些代碼優化,但一般將快速啟動放在主動優化之前進行考慮
(2) Sun公司的HotSpot Server JVM(-server選項,Windows平台無效)會進行一些主動優化,但可能帶來更長的啟動延遲
(3) IBM的JVM也會進行一些主動優化
(4) J2ME和一些手持設備(如PDA)不包含JIT編譯,很可能不會進行任何優化
二、減少空間消耗
1.堆(對象)和棧(方法參數、局部變量等)。堆被所有線程共享,但棧被每個線程獨享
2.空間消耗的三個重要方面是:Allocation Rate(分配頻率)、Retention(保留率)和Fragmentation(內存碎片)
Allocation Rate是程序創建新對象的頻率,頻率越高耗費的時間和空間越多。
Retention是存活的堆數據數量。這個值越高需要耗費越多的空間和時間(垃圾回收器執行分配和去分配工作時需要進行更多的管理工作)
Fragmentation:內存碎片是指小塊無法使用的內存。如果一直持續創建大對象,可能會引起過多的內存碎片。從而需要更多的時間分配內存(因為要查找一個足夠大的連續可用內存塊),並且會浪費更多的空間因為內存碎片無法被利用。當然某些GC算法可以避免過多內存碎片的產生,但相應的算法代價也較高。
3.內存泄漏(程序泄漏引起的)
4.垃圾回收器的種類(分代收集、標記清除、引用計數、增量收集、壓縮...)對Allocation Rate、Retention和Fragmentation的時間空間消耗影響很大
5.確保所有的對象之間的常量字段是靜態的
| 例如: public class Car { ImageIcon symbol = new ImageIcon("porsche.gif"); ... } ---------------> public class Car { final static ImageIcon symbol = new ImageIcon("porsche.gif"); ... } |
6.對象延遲創建,當不確定某個類是否將會被使用,那這個類里面的實例對象需要時再創建
| 例如: public class Car { private Button button = new JButton(); public Car() { ... initialize button ... } public final JButton getButton() { return button; } } ——————————> public class Car { private Button button = null; public Car() { ... } public final JButton getButton() { if (button == null) { // button not yet created, so create it button = new JButton(); ... initialize button ... } return button; } } |
