j2ee|程式|技巧|效能|最佳化 應用J2EE平台開發的系統的效能是系統使用者和開發人員都關注的問題,本文從伺服器端編程時應注意的幾個方面討論代碼對效能的影響,並總結一些解決的建議。
關鍵詞:效能,Java,J2EE,EJB,Servlet,JDBC
一、概要
Java 2 Platform, Enterprise Edition (J2EE)是當前很多商業應用系統使用的開發平台,該技術提供了一個基於組件的方法來設計、開發、裝配和部署企業級應用程式。J2EE平台提供了一個多層結構的分布式的應用程式模型,可以更快地開發和發布的新的應用解決方案。
J2EE是一種技術規範,定義了整個標準的應用開發體繫結構和一個部署環境,應用開發人員開發時只要專註於具體商業邏輯和商業商務規則的實現上,而其他的諸如事務、持久化、安全等系統開發問題可以由應用程式容器或者伺服器處理,開發完成後,就可以方便地部署到實現規範的應用伺服器中。
作為網路上的商業應用系統,同時訪問的人數是很多的,在大量訪問的情況下,過多的資源請求和有限的伺服器資源(記憶體、CPU時間、網路頻寬等)之間就會出現矛盾,應用系統的效能就顯得很重要了,有時正確的代碼並不能保證項目的成功,效能往往是最後決定一個項目是否成功關鍵。
本文主要從效能的角度出發,討論J2EE伺服器端的代碼效能最佳化和提升。
二、常見的Java 編程
J2EE語言基礎是Java,常用的Java代碼問題對應用系統的效能影響,下面討論了一些應該注意方面。
·使用StringBuffer代替String
當處理字串的相加時,常見的寫法是:..
String str1 = "Hello";
String str2 = "welcome to world";
String str3 = str1 + ", " + str2 +"!";
System.out.println(str3);
很多人都知道,這樣的代碼效率是很低的,因為String是用來儲存字串常量的,如果要執行“+”的操作,系統會產生一些臨時的對象,並對這些對象進行管理,造成不必要的開銷。
如果字串有串連的操作,替代的做法是用StringBuffer類的append方法,它的預設建構函式和append的實現是:
public StringBuffer() { // 建構函式
this(16); // 預設容量16}
public synchronized StringBuffer append(String str) {
if (str == null) {
str = String.valueOf(str);
}
int len =str.length();
int newcount = count + len;
if(newcount > value.length)
expandCapacity(newcount);
// 擴充容量
str.getChars(0, len, value, count);
count = newcount;
return this;
}
當字串的大小超過預設16時,代碼實現了容量的擴充,為了避免對象的重新擴充其容量,更好的寫法為:
StringBuffer buffer = new StringBuffer(30);
// 分配指定的大小。
buffer.append("hello");
buffer.append(",");
buffer.append("welcometo world!");
String str = buffer.toString();
·產生對象時,分配合理的空間和大小
Java中的很多類都有它的預設的空間分配大小,對於一些有大小的對象的初始化,應該預計對象的大小,然後使用進行初始化,上面的例子也說明了這個問題,StringBuffer建立時,我們指定了它的大小。
另外的一個例子是Vector,當聲明Vector vect=new Vector()時,系統調用:
public Vector() {// 預設建構函式
this(10); // 容量是 10;
}
預設分配10個對象大小容量。當執行add方法時,可以看到具體實現為:..
public synchronized boolean add(Object o) {
modCount++;
ensureCapacityHelper(elementCount+1);
elementData[elementCount++] =o;
return true;
}
private void ensureCapacityHelper(int minCapacity) {
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (capacityIncrement > 0) ? (oldCapacity + capacityIncrement) :
(oldCapacity * 2);
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
elementData = new Object[newCapacity];
System.arraycopy(oldData, 0, elementData, 0, elementCount);
}
}
我們可以看到,當Vector大小超過原來的大小時,一些代碼的目的就是為了做容量的擴充,在預Crowdsourced Security Testing道該Vector大小的話,可以指定其大小,避免容量擴充的開銷,如知道Vector大小為100時,初始化是就可以象這樣。
Vector vect =.. new Vector(100);
·最佳化迴圈體
迴圈是比較重複啟動並執行地方,如果迴圈次數很大,迴圈體內不好的代碼對效率的影響就會被放大而變的突出。考慮下面的代碼片:..
Vector vect = new Vector(1000);
...
for( inti=0; i<vect.size(); i++){
...
}
for迴圈部分改寫成:
int size = vect.size();
for( int i=0; i>size; i++){
...
}
如果size=1000,就可以減少1000次size()的系統調用開銷,避免了迴圈體重複調用。
再看如下的代碼片:..
for (int i = 0;i <100000;i++)
if (i%10 == 9) {
... // 每十次執行一次
}
改寫成也可以提高效率:..
for(inti =0,j =10; i<100000; i++,j--){
if(j == 0){
... // 每十次執行一次
j = 10;
}
}
所以,當有較大的迴圈時,應該檢查迴圈內是否有效率不高的地方,尋找更優的方案加以改進。
·對象的建立
盡量少用new來初始化一個類的執行個體,當一個對象是用new進行初始化時,其建構函式鏈的所有建構函式都被調用到,所以new操作符是很消耗系統資源的,new一個對象耗時往往是局部變數賦值耗時的上千倍。同時,當產生對象後,系統還要花時間進行記憶體回收和處理。
當new建立對象不可避免時,注意避免多次的使用new初始化一個對象。
盡量在使用時再建立該對象。如:
NewObject object = new NewObject();
int value;
if(i>0 )
{
value =object.getValue();
}
可以修改為:
int value;
if(i>0 )
{
NewObject object = new NewObject();
Value =object.getValue();
}
另外,應該盡量重複使用一個對象,而不是聲明新的同類對象。一個重用對象的方法是改變對象的值,如可以通過setValue之類的方法改變對象的變數達到重用的目的。
·變數的注意事項
盡量使用局部變數,調用方法時傳遞的參數以及在調用中建立的臨時變數都儲存在棧(Stack) 中,速度較快。其他變數,如靜態變數、執行個體變數等,都在堆(Heap)中建立,速度較慢。
盡量使用靜態變數,即加修飾符static,如果類中的變數不會隨他的執行個體而變化,就可以定義為靜態變數,從而使他所有的執行個體都共用這個變數。
·方法(Method)調用
在Java中,一切都是對象,如果有方法(Method)調用,處理器先要檢查該方法是屬於哪個對象,該對象是否有效,對象屬於什麼類型,然後選擇合適的方法並調用。
可以減少方法的調用,同樣一個方法:
public void CallMethod(int i ){
if( i ==0 ){
return;
}
... // 其他處理
}
如果直接調用,
int i = 0;
...
CallMethod(i);
就不如寫成:
int i = 0;
...
if( i ==0 ){
CallMethod(i);
}
不影響可讀性等情況下,可以把幾個小的方法合成一個大的方法。
另外,在方法前加上final,private關鍵字有利於編譯器的最佳化。
·慎用異常處理
異常是Java的一種錯誤處理機制,對程式來說是非常有用的,但是異常對效能不利。拋出異常首先要建立一個新的對象,並進行相關的處理,造成系統的開銷,所以異常應該用在錯誤處理的情況,不應該用來控製程序流程,流程盡量用while,if等處理。
在不是很影響代碼健壯性的前提下,可以把幾個try/catch塊合成一個。
·同步
同步主要出現在多線程的情況,為多線程同時運行時提供對象資料安全的機制,多線程是比較複雜話題,應用多線程也是為了獲得效能的提升,應該儘可能減少同步。
另外,如果需要同步的地方,可以減少同步的程式碼片段,如只同步某個方法或函數,而不是整個代碼。
·使用Java系統API
Java的API一般都做了效能的考慮,如果完成相同的功能,優先使用API而不是自己寫的代碼,如數組複製通常的代碼如下:
int size = 1000;
String[] strArray1 = new String[size];
String[] strArray2 = new String[size];
for(inti=0;i<size;i++){ // 賦值
strArray1[i] = (new String("Array: " + i));
}
for(inti=0;i<size;i++){ // 複製
strArray2[i]=(new String((String)a[i]));
}
如果使用Java提供的API,就可以提高效能:
int size = 1000;
String[] strArray1 = new String[size];
String[] strArray2 = new String[size];
for(inti=0;i<size;i++){ // 賦值
strArray1[i] = (new String("Array: " + i));
}
System.arraycopy(strArray1,0,strArray2,0,size); // 複製
同樣的一個規則是,當有大量資料的複製時,應該使用System.arraycopy()。
三、I/O 效能
輸入/輸出(I/O)包括很多方面,我們知道,進行I/O操作是很費系統資源的。程式中應該盡量少用I/O操作。使用時可以注意: . 合理控制輸出函數System.out.println()對於大多時候是有用的,特別是系統調試的時候,但也會產生大量的資訊出現在控制台和日誌上,同時輸出時,有序列化和同步的過程,造成了開銷。
特別是在發行版中,要合理的控制輸出,可以在項目開發時,設計好一個Debug的工具類,在該類中可以實現輸出開關,輸出的層級,根據不同的情況進行不同的輸出的控制。
·使用緩衝
讀寫記憶體要比讀寫檔案要快很多,應儘可能使用緩衝。
儘可能使用帶有Buffer的類代替沒有Buffer的類,如可以用BufferedReader 代替Reader,用BufferedWriter代替Writer來進行處理I/O操作。
同樣可以用BufferedInputStream代替InputStream都可以獲得效能的提高。
四、Servlet
Servlet採用請求——響應模式提供Web服務,通過ServletResponse以及ServletRequest這兩個對象來輸出和接收使用者傳遞的參數,在伺服器端處理使用者的請求,根據請求訪問資料庫、訪問別的Servlet方法、調用EJB等等,然後將處理結果返回給用戶端。
·盡量不使用同步
Servlet是多線程的,以處理不同的請求,基於前面同步的分析,如果有太多的同步就失去了多線程的優勢了。
·不用儲存太多的資訊在HttpSession中
很多時候,儲存一些對象在HttpSession中是有必要的,可以加快系統的開發,如網上商店系統會把購物車資訊儲存在該使用者的Session中,但當儲存大量的資訊或是大的對象在會話中是有害的,特別是當系統中使用者的訪問量很大,對記憶體的需求就會很高。
具體開發時,在這兩者之間應作好權衡。
·清除Session
通常情況,當達到設定的逾時時間時,同時有些Session沒有了活動,伺服器會釋放這些沒有活動的Session,.. 不過這種情況下,特別是多使用者並訪時,系統記憶體要維護多個的無效Session。
當使用者退出時,應該手動釋放,回收資源,實現如下:..
HttpSession theSession = request.getSession();
// 擷取當前Session
if(theSession != null){
theSession.invalidate(); // 使該Session失效
}
五、EJB 問題
EJB是Java伺服器端服務架構的規範,軟體廠商根據它來實現EJB伺服器。應用程式開發人員可以專註於支援應用所需的商業邏輯,而不用擔心周圍架構的實現問題。EJB規範詳細地解釋了一些最小但是必須的服務,如事務,安全和名字等。
·緩衝Home介面
EJB庫使用Enterprise Bean 的用戶端通過它的Home介面建立它的執行個體。用戶端能通過JNDI訪問它。伺服器通過Lookup方法來擷取。
JNDI是個遠程對象,通過RMI方式調用,對它的訪問往往是比較費時的。所以,在設計時可以設計一個類專門用來緩衝Home介面,在系統初始化時就獲得需要的Home介面並緩衝,以後的引用只要引用緩衝即可。
·封裝Entity Bean
直接存取Entity Bean是個不好的習慣,用會話Bean封裝對實體Bean的訪問能夠改進交易管理,因為每一個對get方法的直接調用將產生一個事務,容器將在每一個實體Bean的事務之後執行一個“Load-Store”.. 操作。
最好在Session Bean中完成Entity Bean的封裝,減少容器的交易處理,並在Session Bean中實現一些具體的業務方法。
·釋放有狀態的Session Bean
相當於HttpSession,當把一個Session Bean設為Stateful,即有狀態的Session Bean 後,應用程式容器(Container)就可能有“鈍化”(Passivate)和活化(Activate)過程,即在主存和二級緩衝之間對SessionBean進行儲存位置的轉移,在這個過程中,存在序列化過程。
通常有狀態Session Bean的釋放是在逾時時發生,容器自動的清除該對象,但是如果交給容器管理,一方面可能產生對象鈍化,另一方面未逾時期間,系統還要 維護一份該對象,所以如果我們確認使用完該StatefulSession Bean後不再需要時,可以顯式的將其釋放掉,方法是調用:
theSesionBean.remove();
六、資料庫訪問
在J2EE開發的應用系統中,資料庫訪問一般是個必備的環節。資料庫用來儲存業務資料,供應用程式訪問。
在Java技術的應用體系中,應用程式是通過JDBC(Java Database Connectivity)實現的介面來訪問資料庫的,JDBC支援“建立串連、SQL語句查詢、處理結果”等準系統。在應用JDBC介面訪問資料庫的過程中,只要根據規範來實現,就可以達到要求的功能。
但是,有些時候進行資料查詢的效率著實讓開發人員不如所願,明明根據規範編寫的程式,運行效果卻很差,造成整個系統的執行效率不高。
·使用速度快的JDBC驅動
JDBC API包括兩種實現介面形式,一種是純Java實現的驅動,一種利用ODBC驅動和資料庫用戶端實現,具體有四種驅動模式並各有不同的應用範圍,針對不同的應用開發要選擇合適的JDBC驅動,在同一個應用系統中,如果選擇不同的JDBC驅動,在效率上會有差別。
例如,有一個公司專屬應用程式系統,不要求支援不同廠商的資料庫,這時就可以選擇模式4的JDBC驅動,該驅動一般由資料庫廠商實現的基於本地協議的驅動,直接調用資料庫管理系統使用的協議,減少了模式3中的中介層。
·使用JDBC串連池
為了提高訪問資料庫的效能,我們還可以使用JDBC 2.0的一些規範和特性,JDBC是佔用資源的,在使用資料庫連接時可以使用串連池Connection Pooling,避免頻繁開啟、關閉Connection。而我們知道,擷取Connection是比較消耗系統資源的。
Connection緩衝池是這樣工作的:當一個應用程式關閉一個資料庫連接時,這個串連並不真正釋放而是被迴圈利用,建立串連是消耗較大的操作,迴圈利用串連可以顯著的提高效能,因為可以減少新串連的建立。
一個通過DataSource擷取緩衝池獲得串連,並串連到一個CustomerDB資料來源的代碼示範如下:
Context ctx = new InitialContext();
DataSource dataSource = (DataSource) ctx.lookup("jdbc/CustomerDB");
Connection conn = dataSource.getConnection("password","username");
·緩衝DataSource
一個DataSource對象代表一個實際的資料來源。這個資料來源可以是從關聯式資料庫到表格形式的檔案,完全依賴於它是怎樣實現的,一個資料來源對象註冊到JNDI名字服務後,應用程式就可以從JNDI伺服器上取得該對象,並使用之和資料來源建立串連。
通過上面的例子,我們知道DataSource是從串連池獲得串連的一種方式,通過JNDI方式獲得,是佔用資源的。
為了避免再次的JNDI調用,可以系統中緩衝要使用的DataSource。
·關閉所有使用的資源
系統一般是並發的系統,在每次申請和使用完資源後,應該釋放供別人使用,資料庫資源每個模式的含義可以參考SUN JDBC的文檔,不同是比較寶貴的,使用完成後應該保證徹底的釋放。
請看下面的程式碼片段:
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
DataSource dataSource = getDataSource();
// 取的DataSource的方法,實現略。
conn = datasource.getConnection();
stmt = conn.createStatement();
rs = stmt.executeQuery("SELECT * FROM ...");
... // 其他處理
rs.close();
stmt.close();
conn.close();
}catch (SQLException ex) {
... // 錯誤處理
}
粗看似乎沒有什麼問題,也有關閉相關如Connection等系統資源的代碼,但當出現異常後,關閉資源的代碼可能並不被執行,為保證資源的確實已被關閉,應該把資源關閉的代碼放到finally塊:
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
DataSource dataSource = getDataSource();
// 取的DataSource的方法,實現略。
conn = datasource.getConnection();
stmt = conn.createStatement();
rs = stmt.executeQuery("SELECT * FROM ...");
... // 其他處理
}catch (SQLException ex) {
... // 錯誤處理
}finally{
if (rs!=null) {
try {
rs.close(); // 關閉ResultSet}
catch (SQLException ex) {
... // 錯誤處理
}
}
if (stmt!=null){
try {
stmt.close(); // 關閉Statement}
catch (SQLException ex) {
... // 錯誤處理
}
}
if (conn!=null){
try {
conn.close(); // 關閉Connection}
catch (SQLException ex) {
... // 錯誤處理
}
}
}
·大型資料量處理
當我們在讀取諸如資料列表、報表等大量資料時,可以發現使用EJB的方法是非常慢的,這時可以使用直接存取資料庫的方法,用SQL直接存取資料,從而消除EJB的經常開支(例如遠程方法調用、交易管理和資料序列化,對象的構造等)。
·緩衝經常使用的資料
對於構建的業務系統,如果有些資料要經常要從資料庫中讀取,同時,這些資料又不經常變化,這些資料就可以在系統中緩衝起來,使用時直接讀取緩衝,而不用頻繁的訪問資料庫讀取資料。
緩衝工作可以在系統初始化時一次性讀取資料,特別是一些唯讀資料,當資料更新時更新資料庫內容,同時更新緩衝的資料值。
一個例子是,在一套公司專屬應用程式系統中,企業的資訊資料(如企業的名稱)在多個業務應用模組中使用,這時就可以把這些資料緩衝起來,需要時直接讀取緩衝的公司資訊資料。
七、總結
一般意義上說,參與系統啟動並執行代碼都會對效能產生影響,實際應用中應該養成良好的編程規範、編寫高品質的代碼,當系統效能出現問題時,要找到主要影響效能的瓶頸所在,然後集中精力最佳化這些代碼,能達到事半功倍的效果。
J2EE效能的最佳化包括很多方面的,要達到一個效能優良的系統,除了關注代碼之外,還應該根據系統實際的運行情況,從伺服器軟硬體環境、叢集技術、系統構架設計、系統部署環境、資料結構、演算法設計等方面綜合考慮。