posts - 188,comments - 176,trackbacks - 0

          Java性能優(yōu)化技巧集錦

            可供程序利用的資源是有限的,優(yōu)化的目的就是讓程序用盡可能少的資源完成預(yù)定的任務(wù)。

            可供程序利用的資源(內(nèi)存、CPU時(shí)間、網(wǎng)絡(luò)帶寬等)是有限的,優(yōu)化的目的就是讓程序用盡可能少的資源完成預(yù)定的任務(wù)。優(yōu)化通常包含兩方面的內(nèi)容:減小代碼的體積,提高代碼的運(yùn)行效率。本文討論的主要是如何提高代碼的效率。

            一、通用篇

            “通用篇”討論的問(wèn)題適合于大多數(shù)Java應(yīng)用。

            1.1 不用new關(guān)鍵詞創(chuàng)建類的實(shí)例

            用new關(guān)鍵詞創(chuàng)建類的實(shí)例時(shí),構(gòu)造函數(shù)鏈中的所有構(gòu)造函數(shù)都會(huì)被自動(dòng)調(diào)用。但如果一個(gè)對(duì)象實(shí)現(xiàn)了Cloneable接口,我們可以調(diào)用它的clone()方法。clone()方法不會(huì)調(diào)用任何類構(gòu)造函數(shù)。

            在使用設(shè)計(jì)模式(Design Pattern)的場(chǎng)合,如果用Factory模式創(chuàng)建對(duì)象,則改用clone()方法創(chuàng)建新的對(duì)象實(shí)例非常簡(jiǎn)單。例如,下面是Factory模式的一個(gè)典型實(shí)現(xiàn):

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

            改進(jìn)后的代碼使用clone()方法,如下所示:

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

            上面的思路對(duì)于數(shù)組處理同樣很有用。

            1.2 使用非阻塞I/O

            版本較低的JDK不支持非阻塞I/O API。為避免I/O阻塞,一些應(yīng)用采用了創(chuàng)建大量線程的辦法(在較好的情況下,會(huì)使用一個(gè)緩沖池)。這種技術(shù)可以在許多必須支持并發(fā)I/O流的應(yīng)用中見(jiàn)到,如Web服務(wù)器、報(bào)價(jià)和拍賣應(yīng)用等。然而,創(chuàng)建Java線程需要相當(dāng)可觀的開(kāi)銷。

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

            1.3 慎用異常

            異常對(duì)性能不利。拋出異常首先要?jiǎng)?chuàng)建一個(gè)新的對(duì)象。Throwable接口的構(gòu)造函數(shù)調(diào)用名為fillInStackTrace()的本地(Native)方法,fillInStackTrace()方法檢查堆棧,收集調(diào)用跟蹤信息。只要有異常被拋出,VM就必須調(diào)整調(diào)用堆棧,因?yàn)樵谔幚磉^(guò)程中創(chuàng)建了一個(gè)新的對(duì)象。

            異常只能用于錯(cuò)誤處理,不應(yīng)該用來(lái)控制程序流程。

            1.4 不要重復(fù)初始化變量

            默認(rèn)情況下,調(diào)用類的構(gòu)造函數(shù)時(shí), Java會(huì)把變量初始化成確定的值:所有的對(duì)象被設(shè)置成null,整數(shù)變量(byte、short、int、long)設(shè)置成0,float和double變量設(shè)置成0.0,邏輯值設(shè)置成false。當(dāng)一個(gè)類從另一個(gè)類派生時(shí),這一點(diǎn)尤其應(yīng)該注意,因?yàn)橛胣ew關(guān)鍵詞創(chuàng)建一個(gè)對(duì)象時(shí),構(gòu)造函數(shù)鏈中的所有構(gòu)造函數(shù)都會(huì)被自動(dòng)調(diào)用。

            1.5 盡量指定類的final修飾符

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

            另外,如果指定一個(gè)類為final,則該類所有的方法都是final。Java編譯器會(huì)尋找機(jī)會(huì)內(nèi)聯(lián)(inline)所有的final方法(這和具體的編譯器實(shí)現(xiàn)有關(guān))。此舉能夠使性能平均提高50%。

            1.6 盡量使用局部變量

            調(diào)用方法時(shí)傳遞的參數(shù)以及在調(diào)用中創(chuàng)建的臨時(shí)變量都保存在棧(Stack)中,速度較快。其他變量,如靜態(tài)變量、實(shí)例變量等,都在堆(Heap)中創(chuàng)建,速度較慢。另外,依賴于具體的編譯器/JVM,局部變量還可能得到進(jìn)一步優(yōu)化。請(qǐng)參見(jiàn)《盡可能使用堆棧變量》。

            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的操作,而是改用等價(jià)的左移3位操作,每左移1位相當(dāng)于乘以2。相應(yīng)地,右移1位操作相當(dāng)于除以2。值得一提的是,雖然移位操作速度快,但可能使代碼比較難于理解,所以最好加上一些注釋。

            二、J2EE篇

            前面介紹的改善性能技巧適合于大多數(shù)Java應(yīng)用,接下來(lái)要討論的問(wèn)題適合于使用JSP、EJB或JDBC的應(yīng)用。

            2.1 使用緩沖標(biāo)記

            一些應(yīng)用服務(wù)器加入了面向JSP的緩沖標(biāo)記功能。例如,BEA的WebLogic Server從6.0版本開(kāi)始支持這個(gè)功能,Open Symphony工程也同樣支持這個(gè)功能。JSP緩沖標(biāo)記既能夠緩沖頁(yè)面片斷,也能夠緩沖整個(gè)頁(yè)面。當(dāng)JSP頁(yè)面執(zhí)行時(shí),如果目標(biāo)片斷已經(jīng)在緩沖之中,則生成該片斷的代碼就不用再執(zhí)行。頁(yè)面級(jí)緩沖捕獲對(duì)指定URL的請(qǐng)求,并緩沖整個(gè)結(jié)果頁(yè)面。對(duì)于購(gòu)物籃、目錄以及門戶網(wǎng)站的主頁(yè)來(lái)說(shuō),這個(gè)功能極其有用。對(duì)于這類應(yīng)用,頁(yè)面級(jí)緩沖能夠保存頁(yè)面執(zhí)行的結(jié)果,供后繼請(qǐng)求使用。

            對(duì)于代碼邏輯復(fù)雜的頁(yè)面,利用緩沖標(biāo)記提高性能的效果比較明顯;反之,效果可能略遜一籌。

            2.2 始終通過(guò)會(huì)話Bean訪問(wèn)實(shí)體Bean

            直接訪問(wèn)實(shí)體Bean不利于性能。當(dāng)客戶程序遠(yuǎn)程訪問(wèn)實(shí)體Bean時(shí),每一個(gè)get方法都是一個(gè)遠(yuǎn)程調(diào)用。訪問(wèn)實(shí)體Bean的會(huì)話Bean是本地的,能夠把所有數(shù)據(jù)組織成一個(gè)結(jié)構(gòu),然后返回它的值。

            用會(huì)話Bean封裝對(duì)實(shí)體Bean的訪問(wèn)能夠改進(jìn)事務(wù)管理,因?yàn)闀?huì)話Bean只有在到達(dá)事務(wù)邊界時(shí)才會(huì)提交。每一個(gè)對(duì)get方法的直接調(diào)用產(chǎn)生一個(gè)事務(wù),容器將在每一個(gè)實(shí)體Bean的事務(wù)之后執(zhí)行一個(gè)“裝入-讀取”操作。

            一些時(shí)候,使用實(shí)體Bean會(huì)導(dǎo)致程序性能不佳。如果實(shí)體Bean的唯一用途就是提取和更新數(shù)據(jù),改成在會(huì)話Bean之內(nèi)利用JDBC訪問(wèn)數(shù)據(jù)庫(kù)可以得到更好的性能。

            2.3 選擇合適的引用機(jī)制

            在典型的JSP應(yīng)用系統(tǒng)中,頁(yè)頭、頁(yè)腳部分往往被抽取出來(lái),然后根據(jù)需要引入頁(yè)頭、頁(yè)腳。當(dāng)前,在JSP頁(yè)面中引入外部資源的方法主要有兩種:include指令,以及include動(dòng)作。

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

            include動(dòng)作:例如。該動(dòng)作引入指定頁(yè)面執(zhí)行后生成的結(jié)果。由于它在運(yùn)行時(shí)完成,因此對(duì)輸出結(jié)果的控制更加靈活。但時(shí),只有當(dāng)被引用的內(nèi)容頻繁地改變時(shí),或者在對(duì)主頁(yè)面的請(qǐng)求沒(méi)有出現(xiàn)之前,被引用的頁(yè)面無(wú)法確定時(shí),使用include動(dòng)作才合算。

            2.4 在部署描述器中設(shè)置只讀屬性

            實(shí)體Bean的部署描述器允許把所有g(shù)et方法設(shè)置成“只讀”。當(dāng)某個(gè)事務(wù)單元的工作只包含執(zhí)行讀取操作的方法時(shí),設(shè)置只讀屬性有利于提高性能,因?yàn)槿萜鞑槐卦賵?zhí)行存儲(chǔ)操作。

            2.5 緩沖對(duì)EJB Home的訪問(wèn)

            EJB Home接口通過(guò)JNDI名稱查找獲得。這個(gè)操作需要相當(dāng)可觀的開(kāi)銷。JNDI查找最好放入Servlet的init()方法里面。如果應(yīng)用中多處頻繁地出現(xiàn)EJB訪問(wèn),最好創(chuàng)建一個(gè)EJBHomeCache類。EJBHomeCache類一般應(yīng)該作為singleton實(shí)現(xiàn)。

            2.6 為EJB實(shí)現(xiàn)本地接口

            本地接口是EJB 2.0規(guī)范新增的內(nèi)容,它使得Bean能夠避免遠(yuǎn)程調(diào)用的開(kāi)銷。請(qǐng)考慮下面的代碼。

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

            第一個(gè)語(yǔ)句表示我們要尋找Bean的Home接口。這個(gè)查找通過(guò)JNDI進(jìn)行,它是一個(gè)RMI調(diào)用。然后,我們定位遠(yuǎn)程對(duì)象,返回代理引用,這也是一個(gè)RMI調(diào)用。第二個(gè)語(yǔ)句示范了如何創(chuàng)建一個(gè)實(shí)例,涉及了創(chuàng)建IIOP請(qǐng)求并在網(wǎng)絡(luò)上傳輸請(qǐng)求的stub程序,它也是一個(gè)RMI調(diào)用。

            要實(shí)現(xiàn)本地接口,我們必須作如下修改:

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

            所有數(shù)據(jù)和返回值都通過(guò)引用的方式傳遞,而不是傳遞值。

            本地接口必須在EJB部署的機(jī)器上使用。簡(jiǎn)而言之,客戶程序和提供服務(wù)的組件必須在同一個(gè)JVM上運(yùn)行。

            如果Bean實(shí)現(xiàn)了本地接口,則其引用不可串行化。

            2.7 生成主鍵

            在EJB之內(nèi)生成主鍵有許多途徑,下面分析了幾種常見(jiàn)的辦法以及它們的特點(diǎn)。

            利用數(shù)據(jù)庫(kù)內(nèi)建的標(biāo)識(shí)機(jī)制(SQL Server的IDENTITY或Oracle的SEQUENCE)。這種方法的缺點(diǎn)是EJB可移植性差。

            由實(shí)體Bean自己計(jì)算主鍵值(比如做增量操作)。它的缺點(diǎn)是要求事務(wù)可串行化,而且速度也較慢。

            利用NTP之類的時(shí)鐘服務(wù)。這要求有面向特定平臺(tái)的本地代碼,從而把Bean固定到了特定的OS之上。另外,它還導(dǎo)致了這樣一種可能,即在多CPU的服務(wù)器上,同一個(gè)毫秒之內(nèi)生成了兩個(gè)主鍵。

            借鑒Microsoft的思路,在Bean中創(chuàng)建一個(gè)GUID。然而,如果不求助于JNI,Java不能確定網(wǎng)卡的MAC地址;如果使用JNI,則程序就要依賴于特定的OS。

            還有其他幾種辦法,但這些辦法同樣都有各自的局限。似乎只有一個(gè)答案比較理想:結(jié)合運(yùn)用RMI和JNDI。先通過(guò)RMI注冊(cè)把RMI遠(yuǎn)程對(duì)象綁定到JNDI樹(shù)。客戶程序通過(guò)JNDI進(jìn)行查找。下面是一個(gè)例子:

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

            2.8 及時(shí)清除不再需要的會(huì)話

            為了清除不再活動(dòng)的會(huì)話,許多應(yīng)用服務(wù)器都有默認(rèn)的會(huì)話超時(shí)時(shí)間,一般為30分鐘。當(dāng)應(yīng)用服務(wù)器需要保存更多會(huì)話時(shí),如果內(nèi)存容量不足,操作系統(tǒng)會(huì)把部分內(nèi)存數(shù)據(jù)轉(zhuǎn)移到磁盤,應(yīng)用服務(wù)器也可能根據(jù)“最近最頻繁使用”(Most Recently Used)算法把部分不活躍的會(huì)話轉(zhuǎn)儲(chǔ)到磁盤,甚至可能拋出“內(nèi)存不足”異常。在大規(guī)模系統(tǒng)中,串行化會(huì)話的代價(jià)是很昂貴的。當(dāng)會(huì)話不再需要時(shí),應(yīng)當(dāng)及時(shí)調(diào)用HttpSession.invalidate()方法清除會(huì)話。HttpSession.invalidate()方法通常可以在應(yīng)用的退出頁(yè)面調(diào)用。

            2.9 在JSP頁(yè)面中關(guān)閉無(wú)用的會(huì)話

            對(duì)于那些無(wú)需跟蹤會(huì)話狀態(tài)的頁(yè)面,關(guān)閉自動(dòng)創(chuàng)建的會(huì)話可以節(jié)省一些資源。使用如下page指令:

          <%@ page session="false"%>

            2.10 Servlet與內(nèi)存使用

            許多開(kāi)發(fā)者隨意地把大量信息保存到用戶會(huì)話之中。一些時(shí)候,保存在會(huì)話中的對(duì)象沒(méi)有及時(shí)地被垃圾回收機(jī)制回收。從性能上看,典型的癥狀是用戶感到系統(tǒng)周期性地變慢,卻又不能把原因歸于任何一個(gè)具體的組件。如果監(jiān)視JVM的堆空間,它的表現(xiàn)是內(nèi)存占用不正常地大起大落。

            解決這類內(nèi)存問(wèn)題主要有二種辦法。第一種辦法是,在所有作用范圍為會(huì)話的Bean中實(shí)現(xiàn)HttpSessionBindingListener接口。這樣,只要實(shí)現(xiàn)valueUnbound()方法,就可以顯式地釋放Bean使用的資源。

            另外一種辦法就是盡快地把會(huì)話作廢。大多數(shù)應(yīng)用服務(wù)器都有設(shè)置會(huì)話作廢間隔時(shí)間的選項(xiàng)。另外,也可以用編程的方式調(diào)用會(huì)話的setMaxInactiveInterval()方法,該方法用來(lái)設(shè)定在作廢會(huì)話之前,Servlet容器允許的客戶請(qǐng)求的最大間隔時(shí)間,以秒計(jì)。

            2.11 HTTP Keep-Alive

            Keep-Alive功能使客戶端到服務(wù)器端的連接持續(xù)有效,當(dāng)出現(xiàn)對(duì)服務(wù)器的后繼請(qǐng)求時(shí),Keep-Alive功能避免了建立或者重新建立連接。市場(chǎng)上的大部分Web服務(wù)器,包括iPlanet、IIS和Apache,都支持HTTP Keep-Alive。對(duì)于提供靜態(tài)內(nèi)容的網(wǎng)站來(lái)說(shuō),這個(gè)功能通常很有用。但是,對(duì)于負(fù)擔(dān)較重的網(wǎng)站來(lái)說(shuō),這里存在另外一個(gè)問(wèn)題:雖然為客戶保留打開(kāi)的連接有一定的好處,但它同樣影響了性能,因?yàn)樵谔幚頃和F陂g,本來(lái)可以釋放的資源仍舊被占用。當(dāng)Web服務(wù)器和應(yīng)用服務(wù)器在同一臺(tái)機(jī)器上運(yùn)行時(shí),Keep-Alive功能對(duì)資源利用的影響尤其突出。

            2.12 JDBC與Unicode

            想必你已經(jīng)了解一些使用JDBC時(shí)提高性能的措施,比如利用連接池、正確地選擇存儲(chǔ)過(guò)程和直接執(zhí)行的SQL、從結(jié)果集刪除多余的列、預(yù)先編譯SQL語(yǔ)句,等等。

            除了這些顯而易見(jiàn)的選擇之外,另一個(gè)提高性能的好選擇可能就是把所有的字符數(shù)據(jù)都保存為Unicode(代碼頁(yè)13488)。Java以Unicode形式處理所有數(shù)據(jù),因此,數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序不必再執(zhí)行轉(zhuǎn)換過(guò)程。但應(yīng)該記住:如果采用這種方式,數(shù)據(jù)庫(kù)會(huì)變得更大,因?yàn)槊總€(gè)Unicode字符需要2個(gè)字節(jié)存儲(chǔ)空間。另外,如果有其他非Unicode的程序訪問(wèn)數(shù)據(jù)庫(kù),性能問(wèn)題仍舊會(huì)出現(xiàn),因?yàn)檫@時(shí)數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序仍舊必須執(zhí)行轉(zhuǎn)換過(guò)程。

            2.13 JDBC與I/O

            如果應(yīng)用程序需要訪問(wèn)一個(gè)規(guī)模很大的數(shù)據(jù)集,則應(yīng)當(dāng)考慮使用塊提取方式。默認(rèn)情況下,JDBC每次提取32行數(shù)據(jù)。舉例來(lái)說(shuō),假設(shè)我們要遍歷一個(gè)5000行的記錄集,JDBC必須調(diào)用數(shù)據(jù)庫(kù)157次才能提取到全部數(shù)據(jù)。如果把塊大小改成512,則調(diào)用數(shù)據(jù)庫(kù)的次數(shù)將減少到10次。

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

            1.14 內(nèi)存數(shù)據(jù)庫(kù)
           
            許多應(yīng)用需要以用戶為單位在會(huì)話對(duì)象中保存相當(dāng)數(shù)量的數(shù)據(jù),典型的應(yīng)用如購(gòu)物籃和目錄等。由于這類數(shù)據(jù)可以按照行/列的形式組織,因此,許多應(yīng)用創(chuàng)建了龐大的Vector或HashMap。在會(huì)話中保存這類數(shù)據(jù)極大地限制了應(yīng)用的可伸縮性,因?yàn)榉?wù)器擁有的內(nèi)存至少必須達(dá)到每個(gè)會(huì)話占用的內(nèi)存數(shù)量乘以并發(fā)用戶最大數(shù)量,它不僅使服務(wù)器價(jià)格昂貴,而且垃圾收集的時(shí)間間隔也可能延長(zhǎng)到難以忍受的程度。

            一些人把購(gòu)物籃/目錄功能轉(zhuǎn)移到數(shù)據(jù)庫(kù)層,在一定程度上提高了可伸縮性。然而,把這部分功能放到數(shù)據(jù)庫(kù)層也存在問(wèn)題,且問(wèn)題的根源與大多數(shù)關(guān)系數(shù)據(jù)庫(kù)系統(tǒng)的體系結(jié)構(gòu)有關(guān)。對(duì)于關(guān)系數(shù)據(jù)庫(kù)來(lái)說(shuō),運(yùn)行時(shí)的重要原則之一是確保所有的寫入操作穩(wěn)定、可靠,因而,所有的性能問(wèn)題都與物理上把數(shù)據(jù)寫入磁盤的能力有關(guān)。關(guān)系數(shù)據(jù)庫(kù)力圖減少I/O操作,特別是對(duì)于讀操作,但實(shí)現(xiàn)該目標(biāo)的主要途徑只是執(zhí)行一套實(shí)現(xiàn)緩沖機(jī)制的復(fù)雜算法,而這正是數(shù)據(jù)庫(kù)層第一號(hào)性能瓶頸通常總是CPU的主要原因。

            一種替代傳統(tǒng)關(guān)系數(shù)據(jù)庫(kù)的方案是,使用在內(nèi)存中運(yùn)行的數(shù)據(jù)庫(kù)(In-memory Database),例如TimesTen。內(nèi)存數(shù)據(jù)庫(kù)的出發(fā)點(diǎn)是允許數(shù)據(jù)臨時(shí)地寫入,但這些數(shù)據(jù)不必永久地保存到磁盤上,所有的操作都在內(nèi)存中進(jìn)行。這樣,內(nèi)存數(shù)據(jù)庫(kù)不需要復(fù)雜的算法來(lái)減少I/O操作,而且可以采用比較簡(jiǎn)單的加鎖機(jī)制,因而速度很快。

          轉(zhuǎn)http://passersby23.blogchina.com/

          posted on 2007-05-24 10:58 cheng 閱讀(207) 評(píng)論(0)  編輯  收藏 所屬分類: JBS
          主站蜘蛛池模板: 贵州省| 宝山区| 屏边| 屯门区| 电白县| 随州市| 邓州市| 清徐县| 四子王旗| 永登县| 德安县| 永吉县| 潮州市| 武冈市| 大姚县| 西城区| 枣阳市| 蒙自县| 阿拉尔市| 瓦房店市| 柳州市| 荔浦县| 烟台市| 宁阳县| 武穴市| 永平县| 肥东县| 马鞍山市| 利辛县| 康平县| 东山县| 禄丰县| 河津市| 积石山| 南投市| 全南县| 宽城| 济宁市| 深泽县| 团风县| 兰州市|