Java性能優化集錦

 一、通用篇

    “通用篇”討論的問題適合於大多數Java應用。


    1.1 不用new關鍵詞創建類的實例


    用new關鍵詞創建類的實例時,構造函數鏈中的所有構造函數都會被自動調用。但如
果一個對象實現了Cloneable接口,我們可以調用它的clone()方法。clone()方法不會調
用任何類構造函數。


    在使用設計模式(Design Pattern)的場合,如果用Factory模式創建對象,則改用
clone()方法創建新的對象實例非常簡單。例如,下面是Factory模式的一個典型實現:


    public static Credit getNewCredit() {return new Credit();} 
 



    改進後的代碼使用clone()方法,如下所示:


    private static Credit BaseCredit = new Credit();
    public static Credit getNewCredit() {
    return (Credit) BaseCredit.clone();} 
 




    上面的思路對於數組處理同樣很有用。


    1.2 使用非阻塞I/O


    版本較低的JDK不支持非阻塞I/O API。爲避免I/O阻塞,一些應用採用了創建大量線
程的辦法(在較好的情況下,會使用一個緩衝池)。這種技術可以在許多必須支持併發
I/O流的應用中見到,如Web服務器、報價和拍賣應用等。然而,創建Java線程需要相當
可觀的開銷。


    JDK 1.4引入了非阻塞的I/O庫(java.nio)。如果應用要求使用版本較早的JDK,在
這裏有一個支持非阻塞I/O的軟件包。


    1.3 慎用異常


    異常對性能不利。拋出異常首先要創建一個新的對象。Throwable接口的構造函數調
用名爲fillInStackTrace()的本地(Native)方法,fillInStackTrace()方法檢查堆
棧,收集調用跟蹤信息。只要有異常被拋出,VM就必須調整調用堆棧,因爲在處理過程
中創建了一個新的對象。


    異常只能用於錯誤處理,不應該用來控制程序流程。


    1.4 不要重複初始化變量


    默認情況下,調用類的構造函數時, Java會把變量初始化成確定的值:所有的對象
被設置成null,整數變量(byte、short、int、long)設置成0,float和double變量設
置成0.0,邏輯值設置成false。當一個類從另一個類派生時,這一點尤其應該注意,因
爲用new關鍵詞創建一個對象時,構造函數鏈中的所有構造函數都會被自動調用。


    1.5 儘量指定類的final修飾符


    帶有final修飾符的類是不可派生的。在Java核心API中,有許多應用final的例子,
例如java.lang.String。爲String類指定final防止了人們覆蓋length()方法。


    另外,如果指定一個類爲final,則該類所有的方法都是final。Java編譯器會尋找
機會內聯(inline)所有的final方法(這和具體的編譯器實現有關)。此舉能夠使性能
平均提高50%。


    1.6 儘量使用局部變量


    調用方法時傳遞的參數以及在調用中創建的臨時變量都保存在棧(Stack)中,速度
較快。其他變量,如靜態變量、實例變量等,都在堆(Heap)中創建,速度較慢。另
外,依賴於具體的編譯器/JVM,局部變量還可能得到進一步優化。


    1.7 乘法和除法


    考慮下面的代碼:


    for (val = 0; val < 100000; val +=5) 
        { alterX = val * 8; myResult = val * 2; } 
 



    用移位操作替代乘法操作可以極大地提高性能。下面是修改後的代碼:


    for (val = 0; val < 100000; val += 5)
        { alterX = val << 3; myResult = val << 1; } 
 




    修改後的代碼不再做乘以8的操作,而是改用等價的左移3位操作,每左移1位相當於
乘以2。相應地,右移1位操作相當於除以2。值得一提的是,雖然移位操作速度快,但可
能使代碼比較難於理解,所以最好加上一些註釋。

    
    二、JSP、EJB、JDBC    

    前面介紹的改善性能技巧適合於大多數Java應用,接下來要討論的問題適合於使用
JSP、EJB或JDBC的應用。

1. 使用緩衝標記

一些應用服務器加入了面向JSP的緩衝標記功能。例如,BEA的WebLogic Server從6.0版
本開始支持這個功能,Open Symphony工程也同樣支持這個功能。JSP緩衝標記既能夠緩
衝頁面片斷,也能夠緩衝整個頁面。當JSP頁面執行時,如果目標片斷已經在緩衝之中,
則生成該片斷的代碼就不用再執行。頁面級緩衝捕獲對指定URL的請求,並緩衝整個結果
頁面。對於購物籃、目錄以及門戶網站的主頁來說,這個功能極其有用。對於這類應
用,頁面級緩衝能夠保存頁面執行的結果,供後繼請求使用。

對於代碼邏輯複雜的頁面,利用緩衝標記提高性能的效果比較明顯;反之,效果可能略
遜一籌。

2. 始終通過會話Bean訪問實體Bean

直接訪問實體Bean不利於性能。當客戶程序遠程訪問實體Bean時,每一個get方法都是一
個遠程調用。訪問實體Bean的會話Bean是本地的,能夠把所有數據組織成一個結構,然
後返回它的值。

用會話Bean封裝對實體Bean的訪問能夠改進事務管理,因爲會話Bean只有在到達事務邊
界時纔會提交。每一個對get方法的直接調用產生一個事務,容器將在每一個實體Bean的
事務之後執行一個“裝入-讀取”操作。一些時候,使用實體Bean會導致程序性能不佳。
如果實體Bean的唯一用途就是提取和更新數據,改成在會話Bean之內利用JDBC訪問數據
庫可以得到更好的性能。

3. 選擇合適的引用機制

在典型的JSP應用系統中,頁頭、頁腳部分往往被抽取出來,然後根據需要引入頁頭、頁
腳。當前,在JSP頁面中引入外部資源的方法主要有兩種:include指令,以及include動
作。

include指令:例如


<%@ include file="copyright.html" %>
 



該指令在編譯時引入指定的資源。在編譯之前,帶有include指令的頁面和指定的資源被
合併成一個文件。被引用的外部資源在編譯時就確定,比運行時才確定資源更高效。 

include動作:例如 


<jsp:include page="copyright.jsp" />
 



該動作引入指定頁面執行後生成的結果。由於它在運行時完成,因此對輸出結果的控制
更加靈活。但時,只有當被引用的內容頻繁地改變時,或者在對主頁面的請求沒有出現
之前,被引用的頁面無法確定時,使用include動作才合算。 

4. 在部署描述器中設置只讀屬性 

實體Bean的部署描述器允許把所有get方法設置成“只讀”。當某個事務單元的工作只包
含執行讀取操作的方法時,設置只讀屬性有利於提高性能,因爲容器不必再執行存儲操
作。



5. 緩衝對EJB Home的訪問 

EJB Home接口通過JNDI名稱查找獲得。這個操作需要相當可觀的開銷。JNDI查找最好放
入Servlet的init()方法裏面。如果應用中多處頻繁地出現EJB訪問,最好創建一個
EJBHomeCache類。EJBHomeCache類一般應該作爲singleton實現。 

6. 爲EJB實現本地接口 

本地接口是EJB 2.0規範新增的內容,它使得Bean能夠避免遠程調用的開銷。請考慮下面
的代碼。 


PayBeanHome home = (PayBeanHome)
javax.rmi.PortableRemoteObject.narrow
(ctx.lookup ("PayBeanHome"), PayBeanHome.class); 
PayBean bean = (PayBean)
javax.rmi.PortableRemoteObject.narrow 
(home.create(), PayBean.class);
 



第一個語句表示我們要尋找Bean的Home接口。這個查找通過JNDI進行,它是一個RMI調
用。然後,我們定位遠程對象,返回代理引用,這也是一個RMI調用。第二個語句示範了
如何創建一個實例,涉及了創建IIOP請求並在網絡上傳輸請求的stub程序,它也是一個
RMI調用。要實現本地接口,我們必須作如下修改: 

方法不能再拋出java.rmi.RemoteException異常,包括從RemoteException派生的異常,
比如TransactionRequiredException、TransactionRolledBackException和
NoSuchObjectException。EJB提供了等價的本地異常,如
TransactionRequiredLocalException、TransactionRolledBackLocalException和
NoSuchObjectLocalException。 

所有數據和返回值都通過引用的方式傳遞,而不是傳遞值。本地接口必須在EJB部署的機
器上使用。簡而言之,客戶程序和提供服務的組件必須在同一個JVM上運行。如果Bean實
現了本地接口,則其引用不可串行化。 

7. 生成主鍵

在EJB之內生成主鍵有許多途徑,下面分析了幾種常見的辦法以及它們的特點。利用數據
庫內建的標識機制(SQL Server的IDENTITY或Oracle的SEQUENCE)。這種方法的缺點是
EJB可移植性差。由實體Bean自己計算主鍵值(比如做增量操作)。它的缺點是要求事務
可串行化,而且速度也較慢。

利用NTP之類的時鐘服務。這要求有面向特定平臺的本地代碼,從而把Bean固定到了特定
的OS之上。另外,它還導致了這樣一種可能,即在多CPU的服務器上,同一個毫秒之內生
成了兩個主鍵。借鑑Microsoft的思路,在Bean中創建一個GUID。然而,如果不求助於
JNI,Java不能確定網卡的MAC地址;如果使用JNI,則程序就要依賴於特定的OS。

還有其他幾種辦法,但這些辦法同樣都有各自的侷限。似乎只有一個答案比較理想:結
合運用RMI和JNDI。先通過RMI註冊把RMI遠程對象綁定到JNDI樹。客戶程序通過JNDI進行
查找。下面是一個例子:


public class keyGenerator
extends UnicastRemoteObject implements
Remote { private static long KeyValue = System.currentTimeMillis();
public static synchronized long getKey()
throws RemoteException { return KeyValue++; }
 



8. 及時清除不再需要的會話 

爲了清除不再活動的會話,許多應用服務器都有默認的會話超時時間,一般爲30分鐘。
當應用服務器需要保存更多會話時,如果內存容量不足,操作系統會把部分內存數據轉
移到磁盤,應用服務器也可能根據“最近最頻繁使用”(Most Recently Used)算法把
部分不活躍的會話轉儲到磁盤,甚至可能拋出“內存不足”異常。在大規模系統中,串
行化會話的代價是很昂貴的。當會話不再需要時,應當及時調用
HttpSession.invalidate()方法清除會話。HttpSession.invalidate()方法通常可以在
應用的退出頁面調用。 

9. 在JSP頁面中關閉無用的會話 

對於那些無需跟蹤會話狀態的頁面,關閉自動創建的會話可以節省一些資源。使用如下
page指令: 


<%@ page session="false"%>
 



10. Servlet與內存使用 

許多開發者隨意地把大量信息保存到用戶會話之中。一些時候,保存在會話中的對象沒
有及時地被垃圾回收機制回收。從性能上看,典型的症狀是用戶感到系統週期性地變
慢,卻又不能把原因歸於任何一個具體的組件。如果監視JVM的堆空間,它的表現是內存
佔用不正常地大起大落。解決這類內存問題主要有二種辦法。第一種辦法是,在所有作
用範圍爲會話的Bean中實現HttpSessionBindingListener接口。這樣,只要實現
valueUnbound()方法,就可以顯式地釋放Bean使用的資源。 

另外一種辦法就是儘快地把會話作廢。大多數應用服務器都有設置會話作廢間隔時間的
選項。另外,也可以用編程的方式調用會話的setMaxInactiveInterval()方法,該方法
用來設定在作廢會話之前,Servlet容器允許的客戶請求的最大間隔時間,以秒計算。 

11. HTTP Keep-Alive 

Keep-Alive功能使客戶端到服務器端的連接持續有效,當出現對服務器的後繼請求時,
Keep-Alive功能避免了建立或者重新建立連接。市場上的大部分Web服務器,包括
iPlanet、IIS和Apache,都支持HTTP Keep-Alive。對於提供靜態內容的網站來說,這個
功能通常很有用。但是,對於負擔較重的網站來說,這裏存在另外一個問題:雖然爲客
戶保留打開的連接有一定的好處,但它同樣影響了性能,因爲在處理暫停期間,本來可
以釋放的資源仍舊被佔用。當Web服務器和應用服務器在同一臺機器上運行時,Keep-
Alive功能對資源利用的影響尤其突出。 

12. JDBC與Unicode 

想必你已經瞭解一些使用JDBC時提高性能的措施,比如利用連接池、正確地選擇存儲過
程和直接執行的SQL、從結果集刪除多餘的列、預先編譯SQL語句,等等。除了這些顯而
易見的選擇之外,另一個提高性能的好選擇可能就是把所有的字符數據都保存爲Unicode
(代碼頁13488)。Java以Unicode形式處理所有數據,因此,數據庫驅動程序不必再執
行轉換過程。但應該記住:如果採用這種方式,數據庫會變得更大,因爲每個Unicode字
符需要2個字節存儲空間。另外,如果有其他非Unicode的程序訪問數據庫,性能問題仍
舊會出現,因爲這時數據庫驅動程序仍舊必須執行轉換過程。
13. JDBC與I/O

如果應用程序需要訪問一個規模很大的數據集,則應當考慮使用塊提取方式。默認情況
下,JDBC每次提取32行數據。舉例來說,假設我們要遍歷一個5000行的記錄集,JDBC必
須調用數據庫157次才能提取到全部數據。如果把塊大小改成512,則調用數據庫的次數
將減少到10次。在一些情形下這種技術無效。例如,如果使用可滾動的記錄集,或者在
查詢中指定了FOR UPDATE,則塊操作方式不再有效。

14. 內存數據庫

許多應用需要以用戶爲單位在會話對象中保存相當數量的數據,典型的應用如購物籃和
目錄等。由於這類數據可以按照行/列的形式組織,因此,許多應用創建了龐大的Vector
或HashMap。在會話中保存這類數據極大地限制了應用的可伸縮性,因爲服務器擁有的內
存至少必須達到每個會話佔用的內存數量乘以併發用戶最大數量,它不僅使服務器價格
昂貴,而且垃圾收集的時間間隔也可能延長到難以忍受的程度。

一些人把購物籃/目錄功能轉移到數據庫層,在一定程度上提高了可伸縮性。然而,把這
部分功能放到數據庫層也存在問題,且問題的根源與大多數關係數據庫系統的體系結構
有關。對於關係數據庫來說,運行時的重要原則之一是確保所有的寫入操作穩定、可
靠,因而,所有的性能問題都與物理上把數據寫入磁盤的能力有關。關係數據庫力圖減
少I/O操作,特別是對於讀操作,但實現該目標的主要途徑只是執行一套實現緩衝機制的
複雜算法,而這正是數據庫層第一號性能瓶頸通常總是CPU的主要原因。

一種替代傳統關係數據庫的方案是,使用在內存中運行的數據庫(In-memory 
Database),例如TimesTen。內存數據庫的出發點是允許數據臨時地寫入,但這些數據
不必永久地保存到磁盤上,所有的操作都在內存中進行。這樣,內存數據庫不需要複雜
的算法來減少I/O操作,而且可以採用比較簡單的加鎖機制,因而速度很快。

    三、圖形界面應用

這一篇中介紹的內容適合於圖形用戶界面的應用(Applet和普通應用),要用到AWT或
Swing。 1. 用JAR壓縮類文件

Java檔案文件(JAR文件)是根據JavaBean標準壓縮的文件,是發佈JavaBean組件的主要
方式和推薦方式。JAR檔案有助於減少文件體積,縮短下載時間。例如,它有助於Applet
提高啓動速度。一個JAR文件可以包含一個或者多個相關的Bean以及支持文件,比如圖
形、聲音、HTML和其他資源。要在HTML/JSP文件中指定JAR文件,只需在Applet標記中加
入ARCHIVE = "name.jar"聲明。

2. 提示Applet裝入進程

你是否看到過使用Applet的網站,注意到在應該運行Applet的地方出現了一個佔位符?
當Applet的下載時間較長時,會發生什麼事情?最大的可能就是用戶掉頭離去。在這種
情況下,顯示一個Applet正在下載的信息無疑有助於鼓勵用戶繼續等待。下面我們來看
看一種具體的實現方法。首先創建一個很小的Applet,該Applet負責在後臺下載正式的
Applet:


import java.applet.Applet;
import java.applet.AppletStub;
import java.awt.Label;
import java.awt.Graphics;
import java.awt.GridLayout;
public class PreLoader extends Applet implements Runnable, AppletStub {
String largeAppletName;
Label label;
public void init() {
// 要求裝載的正式Applet
largeAppletName = getParameter("applet");// “請稍等”提示信息
label = new Label("請稍等..." + largeAppletName);
add(label);
}
public void run(){
try 
{
// 獲得待裝載Applet的類
Class largeAppletClass = Class.forName(largeAppletName);
// 創建待裝載Applet的實例
Applet largeApplet = (Applet)largeAppletClass.newInstance();
// 設置該Applet的Stub程序
largeApplet.setStub(this);
// 取消“請稍等”信息
remove(label);
// 設置佈局
setLayout(new GridLayout(1, 0));
add(largeApplet);
// 顯示正式的Applet
largeApplet.init();
largeApplet.start();
}
catch (Exception ex)
{
// 顯示錯誤信息
label.setText("不能裝入指定的Applet");
}
// 刷新屏幕
validate();
}
public void appletResize(int width, int height)
{
// 把appletResize調用從stub程序傳遞到Applet
resize(width, height);
}
}
 





編譯後的代碼小於2K,下載速度很快。代碼中有幾個地方值得注意。首先,PreLoader實
現了AppletStub接口。一般地,Applet從調用者判斷自己的codebase。在本例中,我們
必須調用setStub()告訴Applet到哪裏提取這個信息。另一個值得注意的地方是,
AppletStub接口包含許多和Applet類一樣的方法,但appletResize()方法除外。這裏我
們把對appletResize()方法的調用傳遞給了resize()方法。 

3. 在畫出圖形之前預先裝入它 

ImageObserver接口可用來接收圖形裝入的提示信息。ImageObserver接口只有一個方法
imageUpdate(),能夠用一次repaint()操作在屏幕上畫出圖形。下面提供了一個例子。 


public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) 
{
if ((flags & ALLBITS) !=0 {
repaint();
}
else if (flags & (ERROR |ABORT )) != 0) {
error = true;
// 文件沒有找到,考慮顯示一個佔位符
repaint();
}
return (flags & (ALLBITS | ERROR| ABORT)) == 0;
}
 



當圖形信息可用時,imageUpdate()方法被調用。如果需要進一步更新,該方法返回
true;如果所需信息已經得到,該方法返回false。 

4. 覆蓋update方法 

update()方法的默認動作是清除屏幕,然後調用paint()方法。如果使用默認的update()
方法,頻繁使用圖形的應用可能出現顯示閃爍現象。要避免在paint()調用之前的屏幕清
除操作,只需按照如下方式覆蓋update()方法: 


public void update(Graphics g) {
paint(g);
}
 



更理想的方案是:覆蓋update(),只重畫屏幕上發生變化的區域,如下所示: 


public void update(Graphics g) {
g.clipRect(x, y, w, h);
paint(g);
}
 
5. 延遲重畫操作

對於圖形用戶界面的應用來說,性能低下的主要原因往往可以歸結爲重畫屏幕的效率低
下。當用戶改變窗口大小或者滾動一個窗口時,這一點通常可以很明顯地觀察到。改變
窗口大小或者滾動屏幕之類的操作導致重畫屏幕事件大量地、快速地生成,甚至超過了
相關代碼的執行速度。對付這個問題最好的辦法是忽略所有“遲到”的事件。

建議在這裏引入一個數毫秒的時差,即如果我們立即接收到了另一個重畫事件,可以停
止處理當前事件轉而處理最後一個收到的重畫事件;否則,我們繼續進行當前的重畫過
程。

如果事件要啓動一項耗時的工作,分離出一個工作線程是一種較好的處理方式;否則,
一些部件可能被“凍結”,因爲每次只能處理一個事件。下面提供了一個事件處理的簡
單例子,但經過擴展後它可以用來控制工作線程。


public static void runOnce(String id, final long milliseconds) {
synchronized(e_queue) {
// e_queue: 所有事件的集合
if (!e_queue.containsKey(id)) {
e_queue.put(token, new LastOne());
}
}
final LastOne lastOne = (LastOne) e_queue.get(token);
final long time = System.currentTimeMillis(); // 獲得當前時間
lastOne.time = time;
(new Thread() {public void run() {
if (milliseconds > 0) {
try {Thread.sleep(milliseconds);} // 暫停線程
catch (Exception ex) {}
}
synchronized(lastOne.running) { // 等待上一事件結束
if (lastOne.time != time) // 只處理最後一個事件
return;
}
}}).start();
}
private static Hashtable e_queue = new Hashtable(); private static class 
LastOne {
public long time=0;
public Object running = new Object();
}
 





6. 使用雙緩衝區 

在屏幕之外的緩衝區繪圖,完成後立即把整個圖形顯示出來。由於有兩個緩衝區,所以
程序可以來回切換。這樣,我們可以用一個低優先級的線程負責畫圖,使得程序能夠利
用空閒的CPU時間執行其他任務。下面的僞代碼片斷示範了這種技術。 


Graphics myGraphics;
Image myOffscreenImage = createImage(size().width, size().height);
Graphics offscreenGraphics = myOffscreenImage.getGraphics();
offscreenGraphics.drawImage(img, 50, 50, this);
myGraphics.drawImage(myOffscreenImage, 0, 0, this);
 



7. 使用BufferedImage 

Java JDK 1.2使用了一個軟顯示設備,使得文本在不同的平臺上看起來相似。爲實現這
個功能,Java必須直接處理構成文字的像素。由於這種技術要在內存中大量地進行位復
制操作,早期的JDK在使用這種技術時性能不佳。爲解決這個問題而提出的Java標準實現
了一種新的圖形類型,即BufferedImage。BufferedImage子類描述的圖形帶有一個可訪
問的圖形數據緩衝區。一個BufferedImage包含一個ColorModel和一組光柵圖形數據。這
個類一般使用RGB(紅、綠、藍)顏色模型,但也可以處理灰度級圖形。它的構造函數很
簡單,如下所示: 


public BufferedImage (int width, int height, int imageType)
 



ImageType允許我們指定要緩衝的是什麼類型的圖形,比如5-位RGB、8-位RGB、灰度級
等。



8. 使用VolatileImage 

許多硬件平臺和它們的操作系統都提供基本的硬件加速支持。例如,硬件加速一般提供
矩形填充功能,和利用CPU完成同一任務相比,硬件加速的效率更高。由於硬件加速分離
了一部分工作,允許多個工作流併發進行,從而緩解了對CPU和系統總線的壓力,使得應
用能夠運行得更快。利用VolatileImage可以創建硬件加速的圖形以及管理圖形的內容。
由於它直接利用低層平臺的能力,性能的改善程度主要取決於系統使用的圖形適配器。
VolatileImage的內容隨時可能丟失,也即它是“不穩定的(volatile)”。因此,在使
用圖形之前,最好檢查一下它的內容是否丟失。VolatileImage有兩個能夠檢查內容是否
丟失的方法: 


public abstract int validate(GraphicsConfiguration gc);public abstract 
Boolean contentsLost();
 



每次從VolatileImage對象複製內容或者寫入VolatileImage時,應該調用validate()方
法。contentsLost()方法告訴我們,自從最後一次validate()調用之後,圖形的內容是
否丟失。雖然VolatileImage是一個抽象類,但不要從它這裏派生子類。VolatileImage
應該通過Component.createVolatileImage()或者
GraphicsConfiguration.createCompatibleVolatileImage()方法創建。 

9. 使用Window Blitting 

進行滾動操作時,所有可見的內容一般都要重畫,從而導致大量不必要的重畫工作。許
多操作系統的圖形子系統,包括WIN32 GDI、MacOS和X/Windows,都支持Window 
Blitting技術。Window Blitting技術直接在屏幕緩衝區中把圖形移到新的位置,只重畫
新出現的區域。要在Swing應用中使用Window Blitting技術,設置方法如下: 


setScrollMode(int mode);
 



在大多數應用中,使用這種技術能夠提高滾動速度。只有在一種情形下,Window 
Blitting會導致性能降低,即應用在後臺進行滾動操作。如果是用戶在滾動一個應用,
那麼它總是在前臺,無需擔心任何負面影響。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章