(代碼級)Java效能的最佳化

來源:互聯網
上載者:User

Java效能的最佳化(上)

 

黃偉峰

Java在九十年代中期出現以後,在贏得讚歎的同時,也引來了一些批評。贏得的讚歎主要是Java的跨平台的操作性,即所謂的”Write Once,Run Anywhere”.但由於Java的效能和運行效率同C相比,仍然有很大的差距,從而引來了很多的批評。
對於伺服器端的應用程式,由於不大涉及到介面設計和程式的頻繁重啟,Java的效能問題看似不大明顯,從而一些Java的技術,如JSP,Se rvlet,EJB等在伺服器端編程方面得到了很大的應用,但實際上,Java的效能問題在伺服器端依然存在。下面我將分四個方面來討論Java的效能和執行效率以及提高J ava效能的一些方法。
一.關於效能的基本知識
1.效能的定義
在我們討論怎樣提高Java的效能之前,我們需要明白“效能“的真正含義。我們一般定義如下五個方面作為評判效能的標準。
1) 運算的效能----哪一個演算法的執行效能最好
2) 記憶體的分配----程式需要分配多少記憶體,運行時的效率和效能最高。
3) 啟動的時間----程式啟動需要多少時間。
4) 程式的延展性-----程式在使用者負載過重的情況下的表現。
5) 效能的認識------使用者怎樣才能認識到程式的效能。
對於不同的應用程式,對效能的要求也不同。例如,大部分的應用程式在啟動時需要較長的時間,從而對啟動時間的要求有所降低;伺服器端的應用程式通常都分配有較大的記憶體空間,所以對記憶體的要求也有所降低。但是,這並不是所這兩方面的效能可以被忽略。其次,演算法的效能對於那些把商務邏輯運用到事務性操作的應用程式來講非常重要。總的來講,對應用程式的要求將決定對各個效能的優先順序。
2.怎樣才能提高JAVA的效能
提高JAVA的效能,一般考慮如下的四個主要方面:
(1) 程式設計的方法和模式
一個良好的設計能提高程式的效能,這一點不僅適用於JAVA,也適用也任何的程式設計語言。因為它充分利用了各種資源,如記憶體,CPU,快取,對象緩衝池及多線程,從而設計出高效能和延展性強的系統。
當然,為了提高程式的效能而改變原來的設計是比較困難的,但是,程式效能的重要性常常要高於設計上帶來的變化。因此,在編程開始之前就應該有一個好的設計模型和方法。
(2) JAVA布署的環境。
JAVA布署的環境就是指用來解釋和執行JAVA位元組碼的技術,一般有如下五種。即解釋指令技術(Interpreter Technology),及時編譯的技術(Just In Time Compilier Technology), 適應性最佳化技術(Adaptive Optimization Technology), 動態最佳化,提前編譯為機器碼的技術(Dynamic Optimization,Ahead Of Time Technology)和編譯為機器碼的技術(Translator Technology).
這些技術一般都通過最佳化執行緒模式,調整堆和棧的大小來最佳化JAVA的效能。在考慮提高JAVA的效能時,首先要找到影響JAVA效能的瓶頸(B ottleNecks),在確認了設計的合理性後,應該調整JAVA布署的環境,通過改變一些參數來提高JAVA應用程式的效能。具體內容見第二節。
(3) JAVA應用程式的實現
當討論應用程式的效能問題時,大多數的程式員都會考慮程式的代碼,這當然是對的,當更重要的是要找到影響程式效能的瓶頸代碼。為了找到這些瓶頸代碼,我們一般會使用一些輔助的工具,如J probe,Optimizit,Vtune以及一些分析的工具如TowerJ Performance等。這些輔助的工具能跟蹤應用程式中執行每個函數或方法所消耗掉的時間,從而改善程式的效能。
(4) 硬體和作業系統
為了提高JAVA應用程式的效能,而採用跟快的CPU和更多的記憶體,並認為這是提高程式效能的唯一方法,但事實並非如此。實踐經驗和事實證明,只有遭到了應用程式效能的瓶頸,從而採取適當得方法,如設計模式,布署的環境,作業系統的調整,才是最有效。
3.程式中通常的效能瓶頸。
所有的應用程式都存在效能瓶頸,為了提高應用程式的效能,就要儘可能的減少程式的瓶頸。以下是在JAVA程式中經常存在的效能瓶頸。

瞭解了這些瓶頸後,就可以有針對性的減少這些瓶頸,從而提高JAVA應用程式的效能
4. 提高JAVA程式效能的步驟
為了提高JAVA程式的效能,需要遵循如下的六個步驟。
a) 明確對效能的具體要求
在實施一個項目之前,必須要明確該項目對於程式效能的具體要求,如:這個應用程式要支援5000個並發的使用者,並且回應時間要在5秒鐘之內。但同時也要明白對於效能的要求不應該同對程式的其他要求衝突。
b) 瞭解當前程式的效能
你應該瞭解你的應用程式的效能同項目所要求效能之間的差距。通常的指標是單位時間內的處理數和回應時間,有時還會比較CPU和記憶體的利用率。
c) 找到程式的效能瓶頸
為了發現程式中的效能瓶頸,通常會使用一些分析工具,如:TowerJ Application Performance Analyzer或VTune來察看和剖析器堆棧中各個元素的消耗時間,從而正確的找到並改正引起效能降低的瓶頸代碼,從而提高程式的效能。這些工具還能發現諸如過多的異常處理,記憶體回收等潛在的問題。
d) 採取適當的措施來提高效能
找到了引起程式效能降低的瓶頸代碼後,我們就可以用前面介紹過的提高效能的四個方面,即設計模式,JAVA代碼的實現,布署JAVA的環境和作業系統來提高應用程式的效能。具體內容將在下面的內容中作詳細說明。
e) 只進行某一方面的修改來提高效能
一次只改變可能引起效能降低的某一方面,然後觀察程式的效能是否有所提高,而不應該一次改變多個方面,因為這樣你將不知道到底哪個方面的改變提高了程式的效能,哪個方面沒有,即不能知道程式瓶頸在哪。
f) 返回到步驟c,繼續作類似的工作,一直達到要求的效能為止。

二. JAVA布署的環境和編譯技術
 開發JAVA應用程式時,首先把JAVA的來源程式編譯為與平台無關的位元組碼。這些位元組碼就可以被各種基於JVM的技術所執行。這些技術主要分為兩個大類。即基於解釋的技術和基於提前編譯為本地碼的技術。其如下:

具體可分為如下的五類:  
a) 解釋指令技術
其結構圖和執行過程如下:

 JAVA的編譯器首先把JAVA源檔案編譯為位元組碼。這些位元組碼對於JAVA虛擬機器(JVM)來講就是機器的指令碼。然後,JAVA的解譯器不斷的迴圈取出位元組碼進行解釋並執行。
 這樣做的優點是可以實現JAVA語言的跨平台,同時產生的位元組碼也比較緊湊。JAVA的一些優點,如安全性,動態性都得保持;但缺點是省產生的位元組碼沒有經過什麼最佳化,同全部編譯好的本地碼相比,速度比較慢。
b) 及時編譯技術(Just In Time)
  及時編譯技術是為瞭解決指令解釋技術效率比較低,速度比較慢的情況下提出的,其結構圖如下所示。

其主要變化是在JAVA程式執行之前,又JIT編譯器把JAVA的位元組碼編譯為機器碼。從而在程式運行時直接執行機器碼,而不用對位元組碼進行解釋。同時對代碼也進行了部分的最佳化。
這樣做的優點是大大提高了JAVA程式的效能。同時,由於編譯的結果並不在程式運行間儲存,因此也節約了儲存空間了載入程式的時間;缺點是由於J IT編譯器對所有的代碼都想最佳化,因此也浪費了很多的時間。
IBM和SUN公司都提供了相關的JIT產品。
c) 適應性最佳化技術(Adaptive Optimization Technology)
同JIT技術相比,適應性最佳化技術並不對所有的位元組碼進行最佳化。它會跟蹤程式啟動並執行成個過程,從而發現需要最佳化的代碼,對代碼進行動態最佳化。對最佳化的代碼,採取8 0/20的策略。從理論上講,程式啟動並執行時間越長,代碼就越最佳化。其結構圖如下:

其優點是適應性最佳化技術充分利用了程式執行時的資訊,發行程式的效能瓶頸,從而提高程式的效能;其缺點是在進行最佳化時可能會選擇不當,發而降低了程式的效能。
其主要產品又IBM,SUN的HotSpot.
d) 動態最佳化,提前編譯為機器碼的技術(Dynamic Optimization,Ahead Of Time)
動態最佳化技術充分利用了JAVA源碼編譯,位元組碼編譯,動態編譯和靜態編譯的技術。其輸入時JAVA的原碼或位元組碼,而輸出是經過高度最佳化的可執行代碼和個來動態庫的混合( Window中是DLL檔案,UNIX中是共用庫.a .so檔案)。其結構如下:

其優點是能大大提高程式的效能;缺點是破壞了JAVA的可移植性,也對JAVA的安全帶來了一定的隱患。

Java效能的最佳化(下)  

 

黃偉峰 
 
三.最佳化JAVA程式設計和編碼,提高JAVA程式效能的一些方法。
通過使用一些前面介紹過的輔助性工具來找到程式中的瓶頸,然後就可以對瓶頸部分的代碼進行最佳化。一般有兩種方案:即最佳化代碼或更改設計方法。我們一般會選擇後者,因為不去調用以下代碼要比調用一些最佳化的代碼更能提高程式的效能。而一個設計良好的程式能夠精簡代碼,從而提高效能。
下面將提供一些在JAVA程式的設計和編碼中,為了能夠提高JAVA程式的效能,而經常採用的一些方法和技巧。
1.對象的產生和大小的調整。
JAVA程式設計中一個普遍的問題就是沒有好好的利用JAVA語言本身提供的函數,從而常常會產生大量的對象(或執行個體)。由於系統不僅要花時間產生對象,以後可能還需花時間對這些對象進行記憶體回收和處理。因此,產生過多的對象將會給程式的效能帶來很大的影響。
例1:關於String ,StringBuffer,+和append
JAVA語言提供了對於String類型變數的操作。但如果使用不當,會給程式的效能帶來影響。如下面的語句:
String name=new String(“HuangWeiFeng”);
System.out.println(name+”is my name”);
看似已經很精簡了,其實並非如此。為了產生二進位的代碼,要進行如下的步驟和操作。
(1) 產生新的字串 new String(STR_1);
(2) 複製該字串。
(3) 載入字串常量”HuangWeiFeng”(STR_2);
(4) 調用字串的構架器(Constructor);
(5) 儲存該字串到數組中(從位置0開始)
(6) 從java.io.PrintStream類中得到靜態out變數
(7) 產生新的字串緩衝變數new StringBuffer(STR_BUF_1);
(8) 複製該字串緩衝變數
(9) 調用字串緩衝的構架器(Constructor);
(10) 儲存該字串緩衝到數組中(從位置1開始)
(11) 以STR_1為參數,調用字串緩衝(StringBuffer)類中的append方法。
(12) 載入字串常量”is my name”(STR_3);
(13) 以STR_3為參數,調用字串緩衝(StringBuffer)類中的append方法。
(14) 對於STR_BUF_1執行toString命令。
(15) 調用out變數中的println方法,輸出結果。
由此可以看出,這兩行簡單的代碼,就產生了STR_1,STR_2,STR_3,STR_4和STR_BUF_1五個物件變數。這些產生的類的執行個體一般都存放在堆中。堆要對所有類的超類,類的執行個體進行初始化,同時還要調用類極其每個超類的構架器。而這些操作都是非常消耗系統資源的。因此,對對象的產生進行限制,是完全有必要的。
經修改,上面的代碼可以用如下的代碼來替換。
StringBuffer name=new StringBuffer(“HuangWeiFeng”);
System.out.println(name.append(“is my name.”).toString());
系統將進行如下的操作。
(1) 產生新的字串緩衝變數new StringBuffer(STR_BUF_1);
(2) 複製該字串緩衝變數
(3) 載入字串常量”HuangWeiFeng”(STR_1);
(4) 調用字串緩衝的構架器(Constructor);
(5) 儲存該字串緩衝到數組中(從位置1開始)
(6) 從java.io.PrintStream類中得到靜態out變數
(7) 載入STR_BUF_1;
(8) 載入字串常量”is my name”(STR_2);
(9) 以STR_2為參數,調用字串緩衝(StringBuffer)執行個體中的append方法。
(10) 對於STR_BUF_1執行toString命令。(STR_3)
(11)調用out變數中的println方法,輸出結果。
由此可以看出,經過改進後的代碼只產生了四個物件變數:STR_1,STR_2,STR_3和STR_BUF_1.你可能覺得少產生一個對象不會對程式的效能有很大的提高。但下面的程式碼片段2 的執行速度將是程式碼片段1的2倍。因為程式碼片段1產生了八個對象,而程式碼片段2隻產生了四個對象。
程式碼片段1:
String name= new StringBuffer(“HuangWeiFeng”);
name+=”is my”;
name+=”name”;
程式碼片段2:
StringBuffer name=new StringBuffer(“HuangWeiFeng”);
name.append(“is my”);
name.append(“name.”).toString();
因此,充分的利用JAVA提供的庫函數來最佳化程式,對提高JAVA程式的效能時非常重要的.其注意點主要有如下幾方面;
(1) 儘可能的使用靜態變數(Static Class Variables)
如果類中的變數不會隨他的執行個體而變化,就可以定義為靜態變數,從而使他所有的執行個體都共用這個變數。
例:
public class foo
{
SomeObject so=new SomeObject();
}
就可以定義為:
public class foo
{
static SomeObject so=new SomeObject();
}
(2) 不要對已產生的對象作過多的改變。
對於一些類(如:String類)來講,寧願在重建一個新的對象執行個體,而不應該修改已經產生的對象執行個體。
例:
String name=”Huang”;
name=”Wei”;
name=”Feng”;
上述代碼產生了三個String類型的對象執行個體。而前兩個馬上就需要系統進行記憶體回收處理。如果要對字串進行串連的操作,效能將得更差。因為系統將不得為此產生更多得臨時變數。如上例1 所示。
(3) 產生對象時,要分配給它合理的空間和大小
JAVA中的很多類都有它的預設的空間分配大小。對於StringBuffer類來講,預設的分配空間大小是16個字元。如果在程式中使用StringBu ffer的空間大小不是16個字元,那麼就必須進行正確的初始化。
(4) 避免產生不太使用或生命週期短的對象或變數。
對於這種情況,因該定義一個對象緩衝池。以為管理一個對象緩衝池的開銷要比頻繁的產生和回收對象的開銷小的多。
(5) 只在對象作用範圍內進行初始化。
JAVA允許在代碼的任何地方定義和初始化對象。這樣,就可以只在對象作用的範圍內進行初始化。從而節約系統的開銷。
例:
SomeObject so=new SomeObject();
If(x==1) then
{
Foo=so.getXX();
}
可以修改為:
if(x==1) then
{
SomeObject so=new SomeObject();
Foo=so.getXX();
}
2.異常(Exceptions)
JAVA語言中提供了try/catch來發方便使用者捕捉異常,進行異常的處理。但是如果使用不當,也會給JAVA程式的效能帶來影響。因此,要注意以下兩點。
(1) 避免對應用程式的邏輯使用try/catch
如果可以用if,while等邏輯語句來處理,那麼就儘可能的不用try/catch語句
(2) 重用異常
在必須要進行異常的處理時,要儘可能的重用已經存在的異常對象。以為在異常的處理中,產生一個異常對象要消耗掉大部分的時間。
3. 線程(Threading)
一個高效能的應用程式中一般都會用到線程。因為線程能充分利用系統的資源。在其他線程因為等待硬碟或網路讀寫而 時,程式能繼續處理和運行。但是對線程運用不當,也會影響程式的效能。
例2:正確使用Vector類
Vector主要用來儲存各種類型的對象(包括相同類型和不同類型的對象)。但是在一些情況下使用會給程式帶來效能上的影響。這主要是由V ector類的兩個特點所決定的。第一,Vector提供了線程的安全保護功能。即使Vector類中的許多方法同步。但是如果你已經確認你的應用程式是單線程,這些方法的同步就完全不必要了。第二,在V ector尋找儲存的各種對象時,常常要花很多的時間進行類型的匹配。而當這些對象都是同一類型時,這些匹配就完全不必要了。因此,有必要設計一個單線程的,儲存特定類型對象的類或集合來替代V ector類.用來替換的程式如下(StringVector.java):
public class StringVector
{
private String [] data;
private int count;
public StringVector() { this(10); // default size is 10 }
public StringVector(int initialSize)
{
data = new String[initialSize];
}
public void add(String str)
{
// ignore null strings
if(str == null) { return; }
ensureCapacity(count + 1);
data[count++] = str;
}

private void ensureCapacity(int minCapacity)
{
int oldCapacity = data.length;
if (minCapacity > oldCapacity)
{
String oldData[] = data;
int newCapacity = oldCapacity * 2;
data = new String[newCapacity];
System.arraycopy(oldData, 0, data, 0, count);
}
}
public void remove(String str)
{
if(str == null) { return // ignore null str }
for(int i = 0; i < count; i++)
{
// check for a match
if(data[i].equals(str))
{
System.arraycopy(data,i+1,data,i,count-1); // copy data
// allow previously valid array element be gc'd
data[--count] = null;
return;
}
}
}
public final String getStringAt(int index) {
if(index < 0) { return null; }
else if(index > count)
{
return null; // index is > # strings
}
else { return data[index]; // index is good }
}
/* * * * * * * * * * * * * * * *StringVector.java * * * * * * * * * * * * * * * * */
因此,代碼:
Vector Strings=new Vector();
Strings.add(“One”);
Strings.add(“Two”);
String Second=(String)Strings.elementAt(1);
可以用如下的代碼替換:
StringVector Strings=new StringVector();
Strings.add(“One”);
Strings.add(“Two”);
String Second=Strings.getStringAt(1);
這樣就可以通過最佳化線程來提高JAVA程式的效能。用於測試的程式如下(TestCollection.java):
import java.util.Vector;
public class TestCollection
{
public static void main(String args [])
{
TestCollection collect = new TestCollection();
if(args.length == 0)
{
System.out.println(
"Usage: java TestCollection [ vector | stringvector ]");
System.exit(1);
}
if(args[0].equals("vector"))
{
Vector store = new Vector();
long start = System.currentTimeMillis();
for(int i = 0; i < 1000000; i++)
{
store.addElement("string");
}
long finish = System.currentTimeMillis();
System.out.println((finish-start));
start = System.currentTimeMillis();
for(int i = 0; i < 1000000; i++)
{
String result = (String)store.elementAt(i);
}
finish = System.currentTimeMillis();
System.out.println((finish-start));
}
else if(args[0].equals("stringvector"))
{
StringVector store = new StringVector();
long start = System.currentTimeMillis();
for(int i = 0; i < 1000000; i++) { store.add("string"); }
long finish = System.currentTimeMillis();
System.out.println((finish-start));
start = System.currentTimeMillis();
for(int i = 0; i < 1000000; i++) {
String result = store.getStringAt(i);
}
finish = System.currentTimeMillis();
System.out.println((finish-start));
}
}
}
/* * * * * * * * * * * * * * * *TestCollection.java * * * * * * * * * * * * * * * * */
測試的結果如下(假設標準的時間為1,越小效能越好):

關於線程的操作,要注意如下幾個方面。
(1) 防止過多的同步
如上所示,不必要的同步常常會造成程式效能的下降。因此,如果程式是單線程,則一定不要使用同步。
(2) 同步方法而不要同步整個程式碼片段
   對某個方法或函數進行同步比對整個程式碼片段進行同步的效能要好。
(3) 對每個對象使用多”鎖”的機制來增大並發。
一般每個對象都只有一個”鎖”,這就表明如果兩個線程執行一個對象的兩個不同的同步方法時,會發生”死結”。即使這兩個方法並不共用任何資源。為了避免這個問題,可以對一個對象實行”多鎖”的機制。如下所示:
class foo
{
private static int var1;
private static Object lock1=new Object();
private static int var2;
private static Object lock2=new Object();
public static void increment1()
{
synchronized(lock1)
{
var1++;
}
}
public static void increment2()
{
synchronized(lock2)
{
var2++;
}
}
}
4.輸入和輸出(I/O)
輸入和輸出包括很多方面,但涉及最多的是對硬碟,網路或資料庫的讀寫操作。對於讀寫操作,又分為有緩衝和沒有緩衝的;對於資料庫的操作,又可以有多種類型的J DBC磁碟機可以選擇。但無論怎樣,都會給程式的效能帶來影響。因此,需要注意如下幾點:
(1) 使用輸入輸出緩衝
   儘可能的多使用緩衝。但如果要經常對緩衝進行重新整理(flush),則建議不要使用緩衝。
(2) 輸出資料流(Output Stream)和Unicode字串
   當時用Output Stream和Unicode字串時,Write類的開銷比較大。因為它要實現Unicode到位元組(byte)的轉換.因此,如果可能的話,在使用Write類之前就實現轉換或用O utputStream類代替Writer類來使用。
(3) 當需序列化時使用transient
   當序列化一個類或對象時,對於那些原子類型(atomic)或可以重建的原素要表識為transient類型。這樣就不用每一次都進行序列化。如果這些序列化的對象要在網路上傳輸,這一小小的改變對效能會有很大的提高。  
(4) 使用快取(Cache)
   對於那些經常要使用而又不大變化的對象或資料,可以把它儲存在快取中。這樣就可以提高訪問的速度。這一點對於從資料庫中返回的結果集尤其重要。
(5) 使用速度快的JDBC磁碟機(Driver)
   JAVA對訪問資料庫提供了四種方法。這其中有兩種是JDBC磁碟機。一種是用JAVA外包的本地磁碟機;另一種是完全的JAVA磁碟機。具體要使用哪一種得根據J AVA布署的環境和應用程式本身來定。
5.一些其他的經驗和技巧
(1) 使用局部變數
(2) 避免在同一個類中動過調用函數或方法(get或set)來設定或調用變數。
(3) 避免在迴圈中產生同一個變數或調用同一個函數(參數變數也一樣)
(4) 儘可能的使用static,final,private等關鍵字
(5) 當複製大量資料時,使用System.arraycopy()命令。  

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.