Java性能優化技巧集錦(二)

二、J2EE篇

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

  2.1 使用緩衝標記

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

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

  2.2 始終通過會話Bean訪問實體Bean

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

  用會話Bean封裝對實體Bean的訪問能夠改進事務管理,因爲會話Bean只有在到達事務邊界時纔會提交。每一個對get方法的直接調用產生一個事務,容器將在每一個實體Bean的事務之後執行一個“裝入-讀取”操作。

  一些時候,使用實體Bean會導致程序性能不佳。如果實體Bean的唯一用途就是提取和更新數據,改成在會話Bean之內利用JDBC訪問數據庫可以得到更好的性能。

  2.3 選擇合適的引用機制

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

  include指令:例如<%@ include file="copyright.html" %>。該指令在編譯時引入指定的資源。在編譯之前,帶有include指令的頁面和指定的資源被合併成一個文件。被引用的外部資源在編譯時就確定,比運行時才確定資源更高效。

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

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

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

  2.5 緩衝對EJB Home的訪問

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

  2.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實現了本地接口,則其引用不可串行化。

  2.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++; }

  2.8 及時清除不再需要的會話

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

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

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

<%@ page session="false"%>

  2.10 Servlet與內存使用

  許多開發者隨意地把大量信息保存到用戶會話之中。一些時候,保存在會話中的對象沒有及時地被垃圾回收機制回收。從性能上看,典型的症狀是用戶感到系統週期性地變慢,卻又不能把原因歸於任何一個具體的組件。如果監視JVM的堆空間,它的表現是內存佔用不正常地大起大落。

  解決這類內存問題主要有二種辦法。第一種辦法是,在所有作用範圍爲會話的Bean中實現HttpSessionBindingListener接口。這樣,只要實現valueUnbound()方法,就可以顯式地釋放Bean使用的資源。

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

  2.11 HTTP Keep-Alive

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

  2.12 JDBC與Unicode

  想必你已經瞭解一些使用JDBC時提高性能的措施,比如利用連接池、正確地選擇存儲過程和直接執行的SQL、從結果集刪除多餘的列、預先編譯SQL語句,等等。

  除了這些顯而易見的選擇之外,另一個提高性能的好選擇可能就是把所有的字符數據都保存爲Unicode(代碼頁13488)。Java以Unicode形式處理所有數據,因此,數據庫驅動程序不必再執行轉換過程。但應該記住:如果採用這種方式,數據庫會變得更大,因爲每個Unicode字符需要2個字節存儲空間。另外,如果有其他非Unicode的程序訪問數據庫,性能問題仍舊會出現,因爲這時數據庫驅動程序仍舊必須執行轉換過程。

  2.13 JDBC與I/O

  如果應用程序需要訪問一個規模很大的數據集,則應當考慮使用塊提取方式。默認情況下,JDBC每次提取32行數據。舉例來說,假設我們要遍歷一個5000行的記錄集,JDBC必須調用數據庫157次才能提取到全部數據。如果把塊大小改成512,則調用數據庫的次數將減少到10次。

  在一些情形下這種技術無效。例如,如果使用可滾動的記錄集,或者在查詢中指定了FOR UPDATE,則塊操作方式不再有效。

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

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

  一種替代傳統關係數據庫的方案是,使用在內存中運行的數據庫(In-memory Database),例如TimesTen。內存數據庫的出發點是允許數據臨時地寫入,但這些數據不必永久地保存到磁盤上,所有的操作都在內存中進行。這樣,內存數據庫不需要複雜的算法來減少I/O操作,而且可以採用比較簡單的加鎖機制,因而速度很快。
發佈了35 篇原創文章 · 獲贊 1 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章