1 概述
在開發中,性能測試是設計初期容易忽略的問題,開發人員會爲了解決一個問題而“不擇手段”,作者所參與的項目中也遇到了類似問題,字符串拼接、大量的網絡 調用和數據庫訪問等等都對系統的性能產生了影響,可是大家不會關心這些問題,“CPU速度在變快”,“內存在變大”,並且,“好像也沒有那麼慢吧”。
有很多商業的性能測試軟件可供使用,如Jprofiler、JProbe Profiler等,但在開發當中顯得有些遙遠而又昂貴。
2 目標
本文將講述如何利用Java語言本身提供的方法在開發中進行性能測試,找到系統瓶頸,進而改進設計;並且在儘量不修改測試對象的情況下進行測試。
3 預備知識
面向對象編程通過抽象繼承採用模塊化的思想來求解問題域,但是模塊化不能很好的解決所有問題。有時,這些問題可能在多個模塊中都出現,像日誌功能,爲了記 錄每個方法進入和離開時的信息,你不得不在每個方法裏添加log("in some method")等信息。如何解決這類問題呢?將這些解決問題的功能點散落在多個模塊中會使冗餘增大,並且當很多個功能點出現在一個模塊中時,代碼變的很 難維護。因此,AOP(ASPect Oriented Programming)應運而生。如果說OOP(Aobject Oriented Programming)關注的是一個類的垂直結構,那麼AOP是從水平角度來看待問題。
動態代理類可以在運行時實現若干接口,每一個動態代理類都有一個Invocation handler對象與之對應,這個對象實現了InvocationHandler接口,通過動態代理的接口對動態代理對象的方法調用會轉而會調用 Invocation handler對象的invoke方法,通過動態代理實例、方法對象和參數對象可以執行調用並返回結果。
說到AOP,大家首先會想到的是日誌記錄、權限檢查和事務管理,是的,AOP是解決這些問題的好辦法。本文根據AOP的思想,通過動態代理來解決一類新的問題——性能測試(performance testing)。
性能測試主要包括以下幾個方面:
l 計算性能:可能是人們首先關心的,簡單的說就是執行一段代碼所用的時間
l 內存消耗:程序運行所佔用的內存大小
l 啓動時間:從你啓動程序到程序正常運行的時間
l 可伸縮性(scalability)
l 用戶察覺性能(perceived performance):不是程序實際運行有多快,而是用戶感覺程序運行有多快.
本文主要給出了計算性能測試和內存消耗測試的可行辦法。
4 計算性能測試
4.1 目標:
通過該測試可以得到一個方法執行需要的時間
4.2實現:
Java爲我們提供了System. currentTimeMillis()方法,可以得到毫秒級的當前時間,我們在以前的程序當中一定也寫過類似的代碼來計算執行某一段代碼所消耗的時間。
long start=System.currentTimeMillis();
doSth();
long end=System.currentTimeMillis();
System.out.println("time lasts "+(end-start)+"ms");
但是,在每個方法裏面都寫上這麼一段代碼是一件很枯燥的事情,我們通過Java的java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler利用動態代理來很好的解決上面的問題。
我們要測試的例子是java.util.LinkedList和java.util.ArrayList的get(int index)方法,顯然ArrayList要比LinkedList高效,因爲前者是隨機訪問,而後者需要順序訪問。
首先我們創建一個接口
public interface Foo {
public void testArrayList();
public void testLinkedList();
}
然後我們創建測試對象實現這個接口
public class FooImpl implements Foo {
private List link=new LinkedList();
private List array=new ArrayList();
public FooImpl()
{
for(int i=0;i<10000;i++)
{
array.add(new Integer(i));
link.add(new Integer(i));
}
}
public void testArrayList()
{
for(int i=0;i<10000;i++)
array.get(i);
}
public void testLinkedList()
{
for(int i=0;i<10000;i++)
link.get(i);
}
}
接下來我們要做關鍵的一步,實現InvocationHandler接口
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.*;
public class Handler implements InvocationHandler {
private Object obj;
public Handler(Object obj) {
this.obj = obj;
}
public static Object newInstance(Object obj) {
Object result = Proxy.newProxyInstance(obj.getClass().getClassLoader(),
obj.getClass().getInterfaces(), new Handler(obj));
return (result);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result;
try {
System.out.print("begin method " + method.getName() + "(");
for (int i = 0; args != null && i < args.length; i++) {
if (i > 0) System.out.print(",");
System.out.print(" " +
args[i].toString());
}
System.out.println(" )");
long start=System.currentTimeMillis();
result = method.invoke(obj, args);
long end=System.currentTimeMillis();
System.out.println("the method "+method.getName()+" lasts "+(end-start)+"ms");
} catch (InvocationTargetException e) {
throw e.getTargetException();
} catch (Exception e) {
throw new RuntimeException
("unexpected invocation exception: " +
e.getMessage());
} finally {
System.out.println("end method " + method.getName());
}
return result;
}
}
最後,我們創建測試客戶端,
public class TestProxy {
public static void main(String[] args) {
try {
Foo foo = (Foo) Handler.newInstance(new FooImpl());
foo.testArrayList();
foo.testLinkedList();
} catch (Exception e) {
e.printStackTrace();
}
}
}
運行的結果如下:
begin method testArrayList( )
the method testArrayList lasts 0ms
end method testArrayList
begin method testLinkedList( )
the method testLinkedList lasts 219ms
end method testLinkedList
使用動態代理的好處是你不必修改原有代碼FooImpl,但是一個缺點是你不得不寫一個接口,如果你的類原來沒有實現接口的話。
4.3擴展
在上面的例子中演示了利用動態代理比較兩個方法的執行時間,有時候通過一次簡單的測試進行比較是片面的,因此可以進行多次執行測試對象,從而計算出最差、最好和平均性能。這樣,我們才能“加快經常執行的程序的速度,儘量少調用速度慢的程序”。
5 內存消耗測試
5.1 目標
當一個java應用程序運行時,有很多需要消耗內存的因素存在,像對象、加載類、線程等。在這裏只考慮程序中的對象所消耗的虛擬機堆空間,這樣我們就可以利用Runtime 類的freeMemory()和totalMemory()方法。
5.2 實現
爲了方便期間,我們首先添加一個類計算當前內存消耗。
class Memory
{
public static long used()
{
long total=Runtime.getRuntime().totalMemory();
long free=Runtime.getRuntime().freeMemory();
return (total-free);
}
}
然後修改Handler類的invoke()方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result;
try {
System.out.print("begin method " + method.getName() + "(");
for (int i = 0; args != null && i < args.length; i++) {
if (i > 0) System.out.print(",");
System.out.print(" " +
args[i].toString());
}
System.out.println(" )");
long start=Memory.used();
result = method.invoke(obj, args);
long end=Memory.used();
System.out.println("memory increased by "+(end-start)+"bytes");
} catch (InvocationTargetException e) {
throw e.getTargetException();
} catch (Exception e) {
throw new RuntimeException
("unexpected invocation exception: " +
e.getMessage());
} finally {
System.out.println("end method " + method.getName());
}
return result;
}
同時我們的測試用例也做了一下改動,測試同樣一個顯而易見的問題,比較一個長度爲1000的ArrayList和HashMap所佔空間的大小,接口、實現如下:
public interface MemoConsumer {
public void creatArray();
public void creatHashMap();
}
public class MemoConsumerImpl implements MemoConsumer {
ArrayList arr=null;
HashMap hash=null;
public void creatArray() {
arr=new ArrayList(1000);
}
public void creatHashMap() {
hash=new HashMap(1000);
}
}
測試客戶端代碼如下:
MemoConsumer arrayMemo=(MemoConsumer)Handler.newInstance(new MemoConsumerImpl ());
arrayMemo.creatArray();
arrayMemo.creatHashMap();
測試結果如下:
begin method creatArray( )
memory increased by 4400bytes
end method creatArray
begin method creatHashMap( )
memory increased by 4480bytes
end method creatHashMap
結果一幕瞭然,可以看到,我們只需要修改invoke()方法,然後簡單執行客戶端調用就可以了。
6 結束語
AOP通過分解關注點和OOP相得益彰,使程序更加簡潔易懂,通過Java語言本身提供的動態代理幫助我們很容易分解關注點,取得了較好的效果。不過測試 對象必須實現接口在一定程度上限制了動態代理的使用,可以借鑑Spring中使用的CGlib來爲沒有實現任何接口的類創建動態代理。