☆
為什么使用對象池
恰當地使用對象池化技術,可以有效地減少對象生成和初始化時的消耗,提高系統的運行效率。Jakarta Commons Pool組件提供了一整套用于實現對象池化的框架,以及若干種各具特色的對象池實現
☆
對象池思路
對象池化的基本思路是:將用過的對象保存起來,等下一次需要這種對象的時候,再拿出來重復使用,從而在一定程度上減少頻繁創建對象所造成的開銷。用于充當保存對象的“容器”的對象,被稱為“對象池”(Object Pool,或簡稱Pool)。
對于沒有狀態的對象(例如String),在重復使用之前,無需進行任何處理;對于有狀態的對象(例如StringBuffer),在重復使用之前,就需要把它們恢復到等同于剛剛生成時的狀態。由于條件的限制,恢復某個對象的狀態的操作不可能實現了的話,就得把這個對象拋棄,改用新創建的實例了。
并非所有對象都適合拿來池化――因為維護對象池也要造成一定開銷。對生成時開銷不大的對象進行池化,反而可能會出現“維護對象池的開銷”大于“生成新對象的開銷”,從而使性能降低的情況。但是對于生成時開銷可觀的對象,池化技術就是提高性能的有效策略了。
☆
運用對象池
在Pool組件中,對象池化的工作被劃分給了三類對象:
PoolableObjectFactory用于管理被池化的對象的產生、激活、掛起、校驗和銷毀;
ObjectPool用于管理要被池化的對象的借出和歸還,并通知PoolableObjectFactory完成相應的工作;
ObjectPoolFactory則用于大量生成相同類型和設置的ObjectPool。
相應地,使用Pool組件的過程,也大體可以劃分成“創立PoolableObjectFactory”、“使用ObjectPool”和可選的“利用ObjectPoolFactory”三種動作。
☆
PoolableObjectFactory
☆
ObjectPool
☆
最終結構
☆
簡化PoolableObjectFactory
PoolableObjectFactory定義了許多方法,可以適應多種不同的情況。但是,在并沒有什么特殊需要的時候,直接實現PoolableObjectFactory接口,就要編寫若干的不進行任何操作,或是始終返回true的方法來讓編譯通過,比較繁瑣。這種時候就可以借助BasePoolableObjectFactory的威力,來簡化編碼的工作。
BasePoolableObjectFactory是org.apache.commons.pool包中的一個抽象類。它實現了PoolableObjectFactory接口,并且為除了makeObject之外的方法提供了一個基本的實現――activateObject、passivateObject和destroyObject不進行任何操作,而validateObject始終返回true。通過繼承這個類,而不是直接實現PoolableObjectFactory接口,就可以免去編寫一些只起到讓編譯通過的作用的代碼的麻煩了。
☆
ObjectPool實現類
☆
StackObjectPool
StackObjectPool利用一個java.util.Stack對象來保存對象池里的對象。這種對象池的特色是:
可以為對象池指定一個初始的參考大小(當空間不夠時會自動增長)。
在對象池已空的時候,調用它的borrowObject方法,會自動返回新創建的實例。
可以為對象池指定一個可保存的對象數目的上限。達到這個上限之后,再向池里送回的對象會被自動送去回收。
StackObjectPool的構造方法共有六個,其中:
最簡單的一個是StackObjectPool(),一切采用默認的設置,也不指明要用的PoolableObjectFactory實例。
最復雜的一個則是StackObjectPool(PoolableObjectFactory factory, int max, int init)。其中:
參數factory指明要與之配合使用的PoolableObjectFactory實例;
參數max設定可保存對象數目的上限;
參數init則指明初始的參考大小。
剩余的四個構造方法則是最復雜的構造方法在某方面的簡化版本,可以根據需要選用。它們是:
StackObjectPool(int max)
StackObjectPool(int max, int init)
StackObjectPool(PoolableObjectFactory factory)
StackObjectPool(PoolableObjectFactory factory, int max)
用不帶factory參數的構造方法構造的StackObjectPool實例,必須要在用它的setFactory(PoolableObjectFactory factory)方法與某一 PoolableObjectFactory實例關聯起來后才能正常使用。
這種對象池可以在沒有Jakarta Commmons Collections組件支持的情況下正常運行。
☆
SoftReferenceObjectPool
SoftReferenceObjectPool利用一個java.util.ArrayList對象來保存對象池里的對象。不過它并不在對象池里直接保存對象本身,而是保存它們的“軟引用”(Soft Reference)。這種對象池的特色是:
可以保存任意多個對象,不會有容量已滿的情況發生。
在對象池已空的時候,調用它的borrowObject方法,會自動返回新創建的實例。
可以在初始化同時,在池內預先創建一定量的對象。
當內存不足的時候,池中的對象可以被Java虛擬機回收。
SoftReferenceObjectPool的構造方法共有三個,其中:
最簡單的是SoftReferenceObjectPool(),不預先在池內創建對象,也不指明要用的PoolableObjectFactory實例。
最復雜的一個則是SoftReferenceObjectPool(PoolableObjectFactory factory, int initSize)。其中:
參數factory指明要與之配合使用的PoolableObjectFactory實例
參數initSize則指明初始化時在池中創建多少個對象。
剩下的一個構造方法,則是最復雜的構造方法在某方面的簡化版本,適合在大多數情況下使用。它是:
SoftReferenceObjectPool(PoolableObjectFactory factory)
用不帶factory參數的構造方法構造的SoftReferenceObjectPool實例,也要在用它的setFactory(PoolableObjectFactory factory)方法與某一PoolableObjectFactory實例關聯起來后才能正常使用。
這種對象池也可以在沒有Jakarta Commmons Collections組件支持的情況下正常運行。
☆
GenericObjectPool
GenericObjectPool利用一個org.apache.commons.collections.CursorableLinkedList對象來保存對象池里的對象。這種對象池的特色是:
可以設定最多能從池中借出多少個對象。
可以設定池中最多能保存多少個對象。
可以設定在池中已無對象可借的情況下,調用它的borrowObject方法時的行為,是等待、創建新的實例還是拋出異常。
可以分別設定對象借出和還回時,是否進行有效性檢查。
可以設定是否使用一個單獨的線程,對池內對象進行后臺清理。
GenericObjectPool的構造方法共有七個,其中:
最簡單的一個是GenericObjectPool(PoolableObjectFactory factory)。僅僅指明要用的PoolableObjectFactory實例,其它參數則采用默認值。
最復雜的一個是GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle, boolean testOnBorrow, boolean testOnReturn, long timeBetweenEvictionRunsMillis, int numTestsPerEvictionRun, long minEvictableIdleTimeMillis, boolean testWhileIdle)。其中:
參數factory指明要與之配合使用的PoolableObjectFactory實例。
參數maxActive指明能從池中借出的對象的最大數目。如果這個值不是正數,表示沒有限制。
參數whenExhaustedAction指定在池中借出對象的數目已達極限的情況下,調用它的borrowObject方法時的行為。可以選用的值有:
GenericObjectPool.WHEN_EXHAUSTED_BLOCK,表示等待;
GenericObjectPool.WHEN_EXHAUSTED_GROW,表示創建新的實例(不過這就使maxActive參數失去了意義);
GenericObjectPool.WHEN_EXHAUSTED_FAIL,表示拋出一個java.util.NoSuchElementException異常。
參數maxWait指明若在對象池空時調用borrowObject方法的行為被設定成等待,最多等待多少毫秒。如果等待時間超過了這個數值,則會拋出一個java.util.NoSuchElementException異常。如果這個值不是正數,表示無限期等待。
參數testOnBorrow設定在借出對象時是否進行有效性檢查。
參數testOnBorrow設定在還回對象時是否進行有效性檢查。
參數timeBetweenEvictionRunsMillis,設定間隔每過多少毫秒進行一次后臺對象清理的行動。如果這個值不是正數,則實際上不會進行后臺對象清理。
參數numTestsPerEvictionRun,設定在進行后臺對象清理時,每次檢查幾個對象。如果這個值不是正數,則每次檢查的對象數是檢查時池內對象的總數乘以這個值的負倒數再向上取整的結果――也就是說,如果這個值是-2(-3、-4、-5……)的話,那么每次大約檢查當時池內對象總數的1/2(1/3、1/4、1/5……)左右。
參數minEvictableIdleTimeMillis,設定在進行后臺對象清理時,視休眠時間超過了多少毫秒的對象為過期。過期的對象將被回收。如果這個值不是正數,那么對休眠時間沒有特別的約束。
參數testWhileIdle,則設定在進行后臺對象清理時,是否還對沒有過期的池內對象進行有效性檢查。不能通過有效性檢查的對象也將被回收。
另一個比較特別的構造方法是GenericObjectPool(PoolableObjectFactory factory, GenericObjectPool.Config config) 。其中:
參數factory指明要與之配合使用的PoolableObjectFactory實例;
參數config則指明一個包括了各個參數的預設值的對象(詳見《GenericObjectPool.Config》一節)。
剩下的五個構造函數則是最復雜的構造方法在某方面的簡化版本,可以根據情況選用。它們是:
GenericObjectPool(PoolableObjectFactory factory, int maxActive)
GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait)
GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, boolean testOnBorrow, boolean testOnReturn)
GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle)
GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle, boolean testOnBorrow, boolean testOnReturn)
這種對象池不可以在沒有Jakarta Commmons Collections組件支持的情況下運行。
【注意】如果沒有設置參數值,GenericObjectPool會使用默認的參數值,比如池的最大數為8,這樣可能造成影響。
☆
GenericObjectPool.Config
調用一個有很多的參數的方法的時候,很可能將參數的位置和個數搞錯,導致編譯或運行時的錯誤;閱讀包含了有很多參數的方法調用的代碼的時候,也很可能因為沒有搞對參數的位置和個數,產生錯誤的理解。因此,人們往往避免給一個方法安排太多的參數的做法(所謂的“Long Parameter List”)。不過,有些方法又確實需要許多參數才能完成工作。于是,就有人想到了一種將大批的參數封裝到一個對象(稱為參數對象,Parameter Object)里,然后將這個對象作為單一的參數傳遞的兩全其美的對策。
因為生成GenericKeyedObjectPool時可供設置的特性非常之多,所以它的構造方法里也就難免會需要不少的參數。GenericKeyedObjectPool除了提供了幾個超長的構造方法之外,同時也定義了一個使用參數對象的構造方法。所用參數對象的類型是GenericKeyedObjectPool.Config。
GenericKeyedObjectPool.Config定義了許多的public字段,每個對應一種可以為GenericKeyedObjectPool設置的特性,包括:
int maxActive
int maxIdle
long maxWait
long minEvictableIdleTimeMillis
int numTestsPerEvictionRun
boolean testOnBorrow
boolean testOnReturn
boolean testWhileIdle
long timeBetweenEvictionRunsMillis
byte whenExhaustedAction
這些字段的作用,與在GenericKeyedObjectPool最復雜的構造方法中與它們同名的參數完全相同。
使用的時候,先生成一個GenericKeyedObjectPool.Config對象,然后將個字段設置為想要的值,最后用這個對象作為唯一的參數調用GenericKeyedObjectPool的構造方法即可。
注意:使用有許多public字段、卻沒有任何方法的類,也是一個人們往往加以避免的行為(所謂的“Data Class”)。不過這次GenericKeyedObjectPool特立獨行了一回。
☆
帶鍵值的對象池
有時候,單用對池內所有對象一視同仁的對象池,并不能解決的問題。例如,對于一組某些參數設置不同的同類對象――比如一堆指向不同地址的java.net.URL對象或者一批代表不同語句的java.sql.PreparedStatement對象,用這樣的方法池化,就有可能取出不合用的對象的麻煩。
可以通過為每一組參數相同的同類對象建立一個單獨的對象池來解決這個問題。但是,如果使用普通的ObjectPool來實施這個計策的話,因為普通的PoolableObjectFactory只能生產出大批設置完全一致的對象,就需要為每一組參數相同的對象編寫一個單獨的PoolableObjectFactory,工作量相當可觀。這種時候就適合調遣Pool組件中提供的一種“帶鍵值的對象池”來展開工作了。
Pool組件采用實現了KeyedObjectPool接口的類,來充當帶鍵值的對象池。相應的,這種對象池需要配合實現了KeyedPoolableObjectFactory接口的類和實現了KeyedObjectPoolFactory接口的類來使用(這三個接口都在org.apache.commons.pool包中定義):
KeyedPoolableObjectFactory和PoolableObjectFactory形式如出一轍,只是每個方法都增加了一個Object key參數而已:
makeObject的參數變為(Object key)
activateObject的參數變為(Object key, Object obj)
passivateObject的參數變為(Object key, Object obj)
validateObject的參數變為Object key, Object obj)
destroyObject的參數變為(Object key, Object obj)
另外Pool組件也提供了BaseKeyedPoolableObjectFactory,用于充當和BasePoolableObjectFactory差不多的角色。
KeyedObjectPool和ObjectPool的形式大同小異,只是某些方法的參數類型發生了變化,某些方法分成了兩種略有不同的版本:
用Object borrowObject(Object key)和void returnObject(Object key, Object obj)來負責對象出借和歸還的動作。
用void close()來關閉不再需要的對象池。
用void clear(Object key)和void clear()來清空池中的對象,前者針對與特定鍵值相關聯的實例,后者針對整個對象池。
用int getNumActive(Object key)和int getNumActive()來查詢已借出的對象數,前者針對與特定鍵值相關聯的實例,后者針對整個對象池。
用int getNumIdle(Object key)和int getNumIdle()來查詢正在休眠的對象數,前者針對與特定鍵值相關聯的實例,后者針對整個對象池。
用void setFactory(KeyedPoolableObjectFactory factory)來設置要用的KeyedPoolableObjectFactory實例。
void clear、int getNumActive、int getNumIdle和void setFactory的各種版本都仍然是可以由具體實現自行決定是否要支持的方法。如果所 用的KeyedObjectPool實現不支持這些操作,那么調用這些方法的時候,會拋出一個UnsupportedOperationException異常。
KeyedObjectPoolFactory和ObjectPoolFactory的形式完全相同,只是所代表的對象不同而已。
☆
當出借少于歸還
Java并未提供一種機制來保證兩個方法被調用的次數之間呈現一種特定的關系(相等,相差一個常數,或是其它任何關系)。因此,完全可以做到建立一個ObjectPool對象,然后調用一次borrowObject方法,借出一個對象,之后重復兩次returnObject方法調用,進行兩次歸還。而調用一個從不曾借出對象的ObjectPool的returnObject方法也并不是一個不可完成的任務。
盡管這些使用方法并不合乎returnObject的字面意思,但是Pool組件中的各個ObjectPool/KeyedObjectPool實現都不在乎這一點。它們的returnObject方法都只是單純地召喚與當前對象池關聯的PoolableObjectFactory實例,看這對象能否經受得起validateObject的考驗而已。考驗的結果決定了這個對象是應該拿去作passivateObject處理,而后留待重用;還是應該拿去作destroyObject處理,以免占用資源。也就是說,當出借少于歸還的時候,并不會額外發生什么特別的事情(當然,有可能因為該對象池處于不接受歸還對象的請求的狀態而拋出異常,不過這是常規現象)。
在實際使用中,可以利用這一特性來向對象池內加入通過其它方法生成的對象。
☆
什么時候使用對象池
采用對象池化的本意,是要通過減少對象生成的次數,減少花在對象初始化上面的開銷,從而提高整體的性能。然而池化處理本身也要付出代價,因此,并非任何情況下都適合采用對象池化。
Dr. Cliff Click在JavaOne 2003上發表的《Performance Myths Exposed》中,給出了一組其它條件都相同時,使用與不使用對象池化技術的實際性能的比較結果。他的實測結果表明:
對于類似Point這樣的輕量級對象,進行池化處理后,性能反而下降,因此不宜池化;
對于類似Hashtable這樣的中量級對象,進行池化處理后,性能基本不變,一般不必池化(池化會使代碼變復雜,增大維護的難度);
對于類似JPanel這樣的重量級對象,進行池化處理后,性能有所上升,可以考慮池化。
根據使用方法的不同,實際的情況可能與這一測量結果略有出入。在配置較高的機器和技術較強的虛擬機上,不宜池化的對象的范圍可能會更大。不過,對于像網絡和數據庫連接這類重量級的對象來說,目前還是有池化的必要。
基本上,只在重復生成某種對象的操作成為影響性能的關鍵因素的時候,才適合進行對象池化。如果進行池化所能帶來的性能提高并不重要的話,還是不采用對象池化技術,以保持代碼的簡明,而使用更好的硬件和更棒的虛擬機來提高性能為佳。