top
Loading...
“快”在細節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大小超過原來的大小時,一些代碼的目的就是為了做容量的擴充,在預先知道該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性能的優化包括很多方面的,要達到一個性能優良的系統,除了關注代碼之外,還應該根據系統實際的運行情況,從服務器軟硬件環境、集群技術、系統構架設計、系統部署環境、數據結構、算法設計等方面綜合考慮。
作者:http://www.zhujiangroad.com
來源:http://www.zhujiangroad.com
北斗有巢氏 有巢氏北斗