Java程序性能測(cè)試
1 概述
在開(kāi)發(fā)中,性能測(cè)試是設(shè)計(jì)初期容易忽略的問(wèn)題,開(kāi)發(fā)人員會(huì)為了解決一個(gè)問(wèn)題而“不擇手段”,作者所參與的項(xiàng)目中也遇到了類(lèi)似問(wèn)題,字符串拼接、大量的網(wǎng)絡(luò)調(diào)用和數(shù)據(jù)庫(kù)訪問(wèn)等等都對(duì)系統(tǒng)的性能產(chǎn)生了影響,可是大家不會(huì)關(guān)心這些問(wèn)題,“CPU速度在變快”,“內(nèi)存在變大”,并且,“好像也沒(méi)有那么慢吧”。
有很多商業(yè)的性能測(cè)試軟件可供使用,如Jprofiler、JProbe Profiler等,但在開(kāi)發(fā)當(dāng)中顯得有些遙遠(yuǎn)而又昂貴。
2 目標(biāo)
本文將講述如何利用Java語(yǔ)言本身提供的方法在開(kāi)發(fā)中進(jìn)行性能測(cè)試,找到系統(tǒng)瓶頸,進(jìn)而改進(jìn)設(shè)計(jì);并且在盡量不修改測(cè)試對(duì)象的情況下進(jìn)行測(cè)試。
3 預(yù)備知識(shí)
面向?qū)ο缶幊掏ㄟ^(guò)抽象繼承采用模塊化的思想來(lái)求解問(wèn)題域,但是模塊化不能很好的解決所有問(wèn)題。有時(shí),這些問(wèn)題可能在多個(gè)模塊中都出現(xiàn),像日志功能,為了記錄每個(gè)方法進(jìn)入和離開(kāi)時(shí)的信息,你不得不在每個(gè)方法里添加log("in some method")等信息。如何解決這類(lèi)問(wèn)題呢?將這些解決問(wèn)題的功能點(diǎn)散落在多個(gè)模塊中會(huì)使冗余增大,并且當(dāng)很多個(gè)功能點(diǎn)出現(xiàn)在一個(gè)模塊中時(shí),代碼變的很難維護(hù)。因此,AOP(Aspect Oriented Programming)應(yīng)運(yùn)而生。如果說(shuō)OOP(Aobject Oriented Programming)關(guān)注的是一個(gè)類(lèi)的垂直結(jié)構(gòu),那么AOP是從水平角度來(lái)看待問(wèn)題。
動(dòng)態(tài)代理類(lèi)可以在運(yùn)行時(shí)實(shí)現(xiàn)若干接口,每一個(gè)動(dòng)態(tài)代理類(lèi)都有一個(gè)Invocation handler對(duì)象與之對(duì)應(yīng),這個(gè)對(duì)象實(shí)現(xiàn)了InvocationHandler接口,通過(guò)動(dòng)態(tài)代理的接口對(duì)動(dòng)態(tài)代理對(duì)象的方法調(diào)用會(huì)轉(zhuǎn)而會(huì)調(diào)用Invocation handler對(duì)象的invoke方法,通過(guò)動(dòng)態(tài)代理實(shí)例、方法對(duì)象和參數(shù)對(duì)象可以執(zhí)行調(diào)用并返回結(jié)果。
說(shuō)到AOP,大家首先會(huì)想到的是日志記錄、權(quán)限檢查和事務(wù)管理,是的,AOP是解決這些問(wèn)題的好辦法。本文根據(jù)AOP的思想,通過(guò)動(dòng)態(tài)代理來(lái)解決一類(lèi)新的問(wèn)題——性能測(cè)試(performance testing)。
性能測(cè)試主要包括以下幾個(gè)方面:
l 計(jì)算性能:可能是人們首先關(guān)心的,簡(jiǎn)單的說(shuō)就是執(zhí)行一段代碼所用的時(shí)間
l 內(nèi)存消耗:程序運(yùn)行所占用的內(nèi)存大小
l 啟動(dòng)時(shí)間:從你啟動(dòng)程序到程序正常運(yùn)行的時(shí)間
l 可伸縮性(scalability)
l 用戶察覺(jué)性能(perceived performance):不是程序?qū)嶋H運(yùn)行有多快,而是用戶感覺(jué)程序運(yùn)行有多快.
本文主要給出了計(jì)算性能測(cè)試和內(nèi)存消耗測(cè)試的可行辦法。
4 計(jì)算性能測(cè)試
4.1 目標(biāo):
通過(guò)該測(cè)試可以得到一個(gè)方法執(zhí)行需要的時(shí)間
4.2實(shí)現(xiàn):
Java為我們提供了System. currentTimeMillis()方法,可以得到毫秒級(jí)的當(dāng)前時(shí)間,我們?cè)谝郧暗某绦虍?dāng)中一定也寫(xiě)過(guò)類(lèi)似的代碼來(lái)計(jì)算執(zhí)行某一段代碼所消耗的時(shí)間。
long start=System.currentTimeMillis();
doSth();
long end=System.currentTimeMillis();
System.out.println("time lasts "+(end-start)+"ms");
但是,在每個(gè)方法里面都寫(xiě)上這么一段代碼是一件很枯燥的事情,我們通過(guò)Java的java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler利用動(dòng)態(tài)代理來(lái)很好的解決上面的問(wèn)題。
我們要測(cè)試的例子是java.util.LinkedList和java.util.ArrayList的get(int index)方法,顯然ArrayList要比LinkedList高效,因?yàn)榍罢呤请S機(jī)訪問(wèn),而后者需要順序訪問(wèn)。
首先我們創(chuàng)建一個(gè)接口
public interface Foo {
public void testArrayList();
public void testLinkedList();
}
然后我們創(chuàng)建測(cè)試對(duì)象實(shí)現(xiàn)這個(gè)接口
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);
}
}
接下來(lái)我們要做關(guān)鍵的一步,實(shí)現(xiàn)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;
}
}
最后,我們創(chuàng)建測(cè)試客戶端,
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();
}
}
}
運(yùn)行的結(jié)果如下:
begin method testArrayList( )
the method testArrayList lasts 0ms
end method testArrayList
begin method testLinkedList( )
the method testLinkedList lasts 219ms
end method testLinkedList
使用動(dòng)態(tài)代理的好處是你不必修改原有代碼FooImpl,但是一個(gè)缺點(diǎn)是你不得不寫(xiě)一個(gè)接口,如果你的類(lèi)原來(lái)沒(méi)有實(shí)現(xiàn)接口的話。
4.3擴(kuò)展
在上面的例子中演示了利用動(dòng)態(tài)代理比較兩個(gè)方法的執(zhí)行時(shí)間,有時(shí)候通過(guò)一次簡(jiǎn)單的測(cè)試進(jìn)行比較是片面的,因此可以進(jìn)行多次執(zhí)行測(cè)試對(duì)象,從而計(jì)算出最差、最好和平均性能。這樣,我們才能“加快經(jīng)常執(zhí)行的程序的速度,盡量少調(diào)用速度慢的程序”。
5 內(nèi)存消耗測(cè)試
5.1 目標(biāo)
當(dāng)一個(gè)java應(yīng)用程序運(yùn)行時(shí),有很多需要消耗內(nèi)存的因素存在,像對(duì)象、加載類(lèi)、線程等。在這里只考慮程序中的對(duì)象所消耗的虛擬機(jī)堆空間,這樣我們就可以利用Runtime 類(lèi)的freeMemory()和totalMemory()方法。
5.2 實(shí)現(xiàn)
為了方便期間,我們首先添加一個(gè)類(lèi)計(jì)算當(dāng)前內(nèi)存消耗。
class Memory
{
public static long used()
{
long total=Runtime.getRuntime().totalMemory();
long free=Runtime.getRuntime().freeMemory();
return (total-free);
}
}
然后修改Handler類(lèi)的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;
}
同時(shí)我們的測(cè)試用例也做了一下改動(dòng),測(cè)試同樣一個(gè)顯而易見(jiàn)的問(wèn)題,比較一個(gè)長(zhǎng)度為1000的ArrayList和HashMap所占空間的大小,接口、實(shí)現(xiàn)如下:
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);
}
}
測(cè)試客戶端代碼如下:
MemoConsumer arrayMemo=(MemoConsumer)Handler.newInstance(new MemoConsumerImpl ());
arrayMemo.creatArray();
arrayMemo.creatHashMap();
測(cè)試結(jié)果如下:
begin method creatArray( )
memory increased by 4400bytes
end method creatArray
begin method creatHashMap( )
memory increased by 4480bytes
end method creatHashMap
結(jié)果一幕了然,可以看到,我們只需要修改invoke()方法,然后簡(jiǎn)單執(zhí)行客戶端調(diào)用就可以了。
6 結(jié)束語(yǔ)
AOP通過(guò)分解關(guān)注點(diǎn)和OOP相得益彰,使程序更加簡(jiǎn)潔易懂,通過(guò)Java語(yǔ)言本身提供的動(dòng)態(tài)代理幫助我們很容易分解關(guān)注點(diǎn),取得了較好的效果。不過(guò)測(cè)試對(duì)象必須實(shí)現(xiàn)接口在一定程度上限制了動(dòng)態(tài)代理的使用,可以借鑒Spring中使用的CGlib來(lái)為沒(méi)有實(shí)現(xiàn)任何接口的類(lèi)創(chuàng)建動(dòng)態(tài)代理。
7 參考資料
本文中提到的一些性能測(cè)試概念主要來(lái)自
http://java.sun.com/docs/books/performance/一些AOP的概念來(lái)自Jboss的
http://www.jboss.org/index.html?module=html&op=userdisplay&id=developers/projects/jboss/aop動(dòng)態(tài)代理和AOP的某些知識(shí)來(lái)自
http://www.springframework.org/docs/reference/aop.html