對象池技術是一種常見的對象緩存手段。’對象’意味着池中的內容是一種結構化實體,這也就是一般意義上面向對象
中的對象模型;’池’(或動詞池化
)意味着將有生命周期的對象緩存到’池子’中進行管理,即用即取。緩存的目的大多是為了提升性能,對象池技術的目的也即如此。所以,對象池技術的本質簡單來說就是:將具有生命周期的結構化對象緩存到帶有一定管理功能的容器中,以提高對象的訪問性能。
處理網絡連接是對象池使用最多的場景。比如一些RPC框架的NettyChannel
緩存(如motan
),以及數據庫連接池的Connection
緩存(如DBCP
)等。除此之外,我們還可以利用對象池管理一些大對象,這些對象有着相對復雜的構造過程。那么對象池技術與普通的本地緩存(比如guava cache)有什么區別呢?其一,本地cache可能會有一些失效策略,比如按照時間、訪問次數等,而對象池是可以沒有這些特性的;其二,也是最重要的一點,緩存中的對象是沒有一個完整生命周期的概念,而對象池中的對象是具有生命周期的,我們甚至可以對對象的生命周期施加影響。
本篇文章結合了Apache Commons Pool的源代碼,講解了一個通用對象池實現上可能需要考慮的方方面面。在閱讀本文之前,我們先思考以下幾個問題:
- 進入池子的對象是什么?需要包裝一下嗎?
- 池子中的對象可能有一些什么狀態和屬性呢?
- 池子中的對象可能有哪些組織形式(數據結構)?
- 池子中的對象是怎么創建出來的?我們如何控制池子中的對象(生命周期)?
- 什么時候我們需要校驗一個池對象的有效性?
- 什么是eviction?什么是abandon?,區別是什么?如何實現?
- 有哪些對象池接口方法可以提供給用戶使用呢?
- 對象被借到的順序是怎么樣的?fifo or lifo?
- 池子中需要定時任務么?定時任務能夠做什么?
一個入門的例子
我們先來看官方文檔中的一個簡單例子,通過這個入門例子讓我們對Apache Commons Pool
有一個感性的認識,至少我們要知道如何使用這個框架。
不使用對象池
public class ReaderUtil { public String readToString(Reader in) throws IOException { // 這里我們構建一個StringBuilder,我們假如他是個大對象,是需要很長時間的創建 StringBuilder buf = new StringBuilder(); Closer closer = Closer.create(); closer.register(in); try { for(int c = in.read(); c != -1; c = in.read()) { buf.append((char)c); } return buf.toString(); } finally { closer.close(); } } }
上述代碼的意思就是:由Reader到String的映射轉換。這個官方例子舉的不是很好,因為StringBuilder並沒有那么迫切的需要放到對象池中管理。但是為了說明問題,我們可以假想他是一個很大的對象,需要花費很長時間進行創建(new)。因此上述代碼的問題在於,每次使用這個工具的readToString()
方法都會產生比較大的對象創建開銷
,這有可能會影響ygc
。
使用對象池
如果此時使用對象池技術,我們就可以預置部分對象,每次使用的時候就直接從池子中取,避免對象的重復創建消耗。如下述代碼:
public class ReaderUtil { private ObjectPool<StringBuilder> pool; ReaderUtil(ObjectPool<StringBuilder> pool) { this.pool = pool; } public String readToString(Reader in) throws IOException { StringBuilder buf = null; Closer closer = Closer.create(); closer.register(in); try { // ① 從對象池中拿出來 buf = pool.borrowObject(); for (int c = in.read(); c != -1; c = in.read()) { buf.append((char) c); } return buf.toString(); } catch (IOException e) { throw e; } catch (Exception e) { throw new RuntimeException("Unable to borrow buffer from pool" + e.toString()); } finally { closer.close(); try { if (buf != null) { //② 用完要還回來 pool.returnObject(buf); } } catch (Exception e) {} } } // 針對池對象的生命周期管理 private static class StringBuilderFactory extends BasePooledObjectFactory<StringBuilder> { @Override public StringBuilder create() throws Exception { // 創建新對象 return new StringBuilder(); } @Override public PooledObject<StringBuilder> wrap(StringBuilder obj) { // 將對象包裝成池對象 return new DefaultPooledObject<>(obj); } // ③ 反初始化每次回收的時候都會執行這個方法 @Override public void passivateObject(PooledObject<StringBuilder> pooledObject) { pooledObject.getObject().setLength(0); } } // 使用這個工具 public static void main(String[] args) { // ④ GenericObjectPool這個是一個通用的范型對象池 ReaderUtil readerUtil = new ReaderUtil(new GenericObjectPool<>(new StringBuilderFactory())); } }
從上述代碼可以看到,使用對象池的主要方法pool.borrowObject()
和pool.returnObject(buf)
進行對象的申請和釋放。這兩個方法也是對象池的最核心方法。BasePooledObjectFactory
是池對象工廠,用於管理池對象的生命周期,我們只需要繼承他,並覆寫父類相關方法即可控制池對象的生成、初始化、反初始化、校驗等。這些內容后文會有詳細說明。GenericObjectPool
是Apache Commons Pool實現的一個通用泛型對象池,是一個對象池的完整實現,我們直接構建並使用即可。
我們在使用對象池的時候,一般是需要基於BasePooledObjectFactory
創建我們自己的對象工廠,並初始化一個對象池,將該工廠與對象池綁定(見上述代碼④),然后就可以使用這個對象池了。比如DBCP的PoolableConnectionFactory<PoolableConnection>
就是DBCP為了管理JDBC連接所實現的池對象工廠。
對象池實現原理
通過上面的例子,我們已經初步了解到了對象池的基本操作步驟,下面我們就將深入對象池內部,具體看看如何設計一個對象池。
對象池的基礎接口
想一想,如果讓我們去設計一個ObjectPool
接口,會給用戶提供哪些核心的方法呢?borrowObject()
,returnObject()
是上文已經說過的兩個核心方法,一個是’借’,一個是’還’。那么我們有可能需要對一個已經借到的對象置為失效(比如當我們的遠程連接關閉或產生異常,這個連接不可用需要失效掉),invalidateObject()
也是必不可少的。對象池剛剛創建的時候,我們可能需要預熱一部分對象,而不是采用懶加載模式以避免系統啟動時候的抖動,因此addObject()
提供給用戶,以進行對象池的預熱。有創建就有銷毀,clear()
和close()
就是用來清空對象池(覺得叫purge()可能更好一點)。除此之外,我們可能還需要一些簡單的統計,比如getNumIdle()
獲得空閑對象個數和getNumActive()
獲得活動對象(被借出對象)的個數。如下表:
方法名 | 作用 |
---|---|
borrowObject() | 從池中借對象 |
returnObject() | 還回池中 |
invalidateObject() | 失效一個對象 |
addObject() | 池中增加一個對象 |
clear() | 清空對象池 |
close() | 關閉對象池 |
getNumIdle() | 獲得空閑對象數量 |
getNumActive() | 獲得被借出對象數量 |
除了ObjectPool
接口,我們還應該抽象出池對象接口PooledObject
以包裝外部對象,以及池對象工廠PooledObjectFactory
以提供池對象生命周期管理,后文有述。
基礎接口的實現類
Commons Pool不但針對ObjectPool提供了相應的對象池實現,還實現了一套KeyedObjectPool
接口,他能夠將對象更細粒度的划分,詳見后文。我們來看下ObjectPool和KeyedObjectPool的類圖:
我們比較關注的是GenericObjectPool和GenericKeyedObjectPool,他們是整個Apache Commons Pool的核心實現。除此之外,他還為我們實現了軟引用對象池SoftReferenceObjectPool
,軟引用對象吃中的對象又被SoftReference
封裝了一層。剩下所有實現類都是包裝器,Commons Pool采用了裝飾者模式以提供對象池額外的擴展功能。比如ProxiedObjectPool,提供了池對象代理功能,防止客戶端將池對象還回后還能繼續使用。
對象池的空間划分
一個對象存儲到對象池中,其位置不是一成不變的。空間的划分可以分為兩種,一種是物理空間划分
,一種是邏輯空間划分
。不同的實現可能采用不同的技術手段,Commons Pool實際上采用了邏輯划分。如下圖所示:
從整體上來講,可以將空間分為池外空間
和池內空間
,池外空間是指被’出借’的對象所在的空間(邏輯空間)。池內空間進一步可以划分為idle空間
,abandon空間
和invalid空間
。idle空間就是空閑對象所在的空間,空閑對象之間是有一定的組織結構的(詳見后文)。abandon空間又被稱作放逐空間,用於放逐被出借的對象。invalid空間其實就是對象的垃圾場,這些對象將不會在被使用,而是等待被gc處理掉。
池對象
池對象就是對象池中所管理的基本單元。我們可以思考一下,如果直接將我們的原始對象放到對象池中是否可以?答案當然是可以,但是不好,因為如果那樣做,我們的對象池就退化成了容器Collection
了,之所以需要將原始對象wrapper成池對象,是因為我們需要提供額外的管理功能,比如生命周期管理。commons pool采用了PooledObject<T>
接口用於表達池對象,它主要抽象了池對象的狀態管理和一些諸如狀態變遷時所產生的統計指標,這些指標可以配合對象池做更精准的管理操作。
池對象的狀態
說到對池對象的管理,最重要的當屬對狀態的管理。對於狀態管理,我們熟知的模型就是狀態機模型了。池對象當然也有一套自己的狀態機,我們先來看看commons pool所定義的池對象都有哪些狀態:
狀態 | 解釋 |
---|---|
IDLE | 空閑狀態 |
ALLOCATED | 已出借狀態 |
EVICTION | 正在進行驅逐測試 |
EVICTION_RETURN_TO_HEAD | 驅逐測試通過對象放回到頭部 |
VALIDATION | 空閑校驗中 |
VALIDATION_PREALLOCATED | 出借前校驗中 |
VALIDATION_RETURN_TO_HEAD | 校驗通過后放回頭部 |
INVALID | 無效對象 |
ABANDONED | 放逐中 |
RETURNING | 換回對象池中 |
上述狀態我們可能對EVICTION
和ABANDONED
不是特別熟悉,后文會講到,這里只需知道:放逐指的是不在對象池中的對象超時流放,驅逐指的是空閑對象超時銷毀。VALIDATION
是有效性校驗,主要校驗空閑對象的有效性。注意與驅逐和放逐之間的區別。我們通過一張圖來看看狀態之間的變遷。
我們看到上圖的’圓圈’表示的就是池對象,其中中間的英文簡寫是其對應的狀態。虛線外框則表示瞬時狀態。比如RETURNING
和ABANDONED
。這里我們省略了VALIDATION_RETURN_TO_HEAD
,VALIDATION_PREALLOCATED
,EVICTION_RETURN_TO_HEAD
,因為這對於我們理解池對象狀態變遷並沒有太多幫助。針對上圖,我們重點關注四個方面:
IDLE->ALLOCATED
即上圖的borrow操作,除了需要將狀態置為已分配,我們還需要考慮如果對象池耗盡了怎么辦?是繼續阻塞還是直接異常退出?如果阻塞是阻塞多久?ALLOCATED->IDLE
即上圖的return操作,我們需要考慮的是,如果池對象還回到對象池,此時對象池空閑數已經達到上界或該對象已經無效,我們是否需要進行特殊處理?IDLE->EVICTION
與ALLOCATED->ABANDONED
請參考后文IDLE->VALIDATION
是testWhileIdle的有效性測試所需要經歷的狀態變遷,他是指每隔一段時間對池中所有的idle對象進行有效性檢查,以排除那些已經失效的對象。失效的對象將會棄置到invalid空間。
池對象的生命周期控制
只搞清楚了池對象的狀態和狀態轉移是不夠的,我們還應該能夠對池對象生命周期施加影響。Commons Pool通過PooledObjectFactory<T>
接口對對象生命周期進行控制。該接口有如下方法:
方法 | 解釋 |
---|---|
makeObject | 創建對象 |
destroyObject | 銷毀對象 |
validateObject | 校驗對象 |
activateObject | 重新初始化對象 |
passivateObject | 反初始化對象 |
我們需要注意,池對象必須經過創建(makeObject()
)和初始化過程(activateObject()
)后才能夠被我們使用。我們看一看這些方法能夠影響哪些狀態變遷。
池對象組織結構與borrow公平性
池中的對象,並不是雜亂無章的,他們得有一定的組織結構。不同的組織結構可能會從整體影響對象池的使用。Apache Commons提供了兩種組織結構,其一是有界阻塞雙端隊列(LinkedBlockingDeque
),其二是key桶。
有界阻塞隊列能夠提供阻塞特性,當池中對象exhausted
后,新申請對象的線程將會阻塞,這是典型的生產者/消費者模型,通過這種雙端的阻塞隊列,我們能夠實現池對象的lifo
或fifo
。如下代碼:
if (getLifo()) { idleObjects.addFirst(p); } else { idleObjects.addLast(p); }
因為是帶有阻塞性質的隊列,我們能夠通過fairness
參數控制線程獲得鎖的公平性,這里我們可以參考AQS
實現,不說了。下面我們再來看一看key桶的數據結構:
從上圖我們可以看出,每一個key對應一個的雙端阻塞隊列ObjectDeque
,ObjectDeque實際上就是包裝了LinkedBlockingDeque,采用這種結構我們能夠對池對象進行一定的划分,從而更加靈活的使用對象池。Commons Pool采用了KeyedObjectPool<K,V>
用以表示采用這種數據結構的對象池。當我們borrow和return的時候,都需要指定對應的key空間。
對象池的放逐與驅逐
上文我們多次提到了驅逐(eviction)
和放逐(abandon)
,這兩個概念是對象池設計的核心。先來看驅逐,我們知道對象池的一個重要的特性就是伸縮性,所謂伸縮性是指對象池能夠根據當前池中空閑對象的數量(maxIdle和minIdle配置)自動進行調整,進而避免內存的浪費。自動伸縮,這是驅逐所需要達到的目標,他是如何實現的呢?實際上在對象池內部,我們可以維護一個驅逐定時器(EvictionTimer
),由timeBetweenEvictionRunsMillis
參數對定時器的間隔加以控制,每次達到驅逐時間后,我們就選定一批對象(由numTestsPerEvictionRun
參數進行控制)進行驅逐測試,這個測試可以采用策略模式,比如Commons Pool的DefaultEvictionPolicy
,代碼如下:
@Override public boolean evict(EvictionConfig config, PooledObject<T> underTest, int idleCount) { if ((config.getIdleSoftEvictTime() < underTest.getIdleTimeMillis() && config.getMinIdle() < idleCount) || config.getIdleEvictTime() < underTest.getIdleTimeMillis()) { return true; } return false; }
對於符合驅逐條件的對象,將會被對象池無情的驅逐出空閑空間,並丟棄到invalid空間。之后對象池還需要保證內部空閑對象數量需要至少達到minIdle
的控制要求。我們在看來放逐,對象出借時間太長(由removeAbandonedTimeout
控制),我們就把他們稱作流浪對象
,這些對象很有可能是那些用完不還的壞蛋們的傑作,也有可能是對象使用者出現了什么突發狀況,比如網絡連接超時時間設置長於放逐時間。總之,被放逐的對象是不允許再次回歸到對象池中的,他們會被擱置到abandon空間,進而進入invalid空間再被gc掉以完成他們的使命。放逐由removeAbandoned()
方法實現,分為標記過程
和放逐過程
,代碼實現並不難,有興趣的可以直接翻翻源代碼。
驅逐是由內而外將對象驅逐出境,放逐則是由外而內,將對象流放。他們一內一外,正是整個對象池形成閉環的核心要素。
對象池的有效性探測
用過數據庫連接池的同學可能對類似testOnBorrow
的配置比較熟悉。除了testOnBorrow,對象池還提供了testOnCreate
, testOnReturn
, testWhileIdle
,其中testWhileIdle是當對象處於空閑狀態的時候所進行的測試,當測試通過則繼續留在對象池中,如果失效,則棄置到invalid空間。所謂testOnBorrow其實就是當對象出借前進行測試,測試什么?當然是有效性測試,在測試之前我們需要調用factory.activateObject()
以激活對象,在調用factory.validateObject(p)
對准備出借的對象做有有效性檢查,如果這個對象無效則可能有拋出異常的行為,或者返回空對象,這全看具體實現了。testOnCreate表示當對象創建之后,再進行有效性測試,這並不適用於頻繁創建和銷毀對象的對象池,他與testOnBorrow的行為類似。testOnReturn是在對象還回到池子之前鎖進行的測試,與出借的測試不同,testOnReturn無論是測試成功還是失敗,我們都需要保證池子中的對象數量是符合配置要求的()ensureIdle()
方法就是做這個事情),並且如果測試失敗了,我們可以直接swallow這個異常,因為用戶根本不需要關心池子的狀態。
對象池的常見配置一覽
當我們了解到了對象池的基本實現原理之后,我們再從配置的角度預覽一下對象池可能提供的功能配置。
配置參數 | 意義 | 默認值 |
---|---|---|
maxTotal | 對象總數 | 8 |
maxIdle | 最大空閑對象數 | 8 |
minIdle | 最小空閑對象書 | 0 |
lifo | 對象池借還是否采用lifo | true |
fairness | 對於借對象的線程阻塞恢復公平性 | false |
maxWaitMillis | 借對象阻塞最大等待時間 | -1 |
minEvictableIdleTimeMillis | 最小驅逐空閑時間 | 30分鍾 |
numTestsPerEvictionRun | 每次驅逐數量 | 3 |
testOnCreate | 創建后有效性測試 | false |
testOnBorrow | 出借前有效性測試 | false |
testOnReturn | 還回前有效性測試 | false |
testWhileIdle | 空閑有效性測試 | false |
timeBetweenEvictionRunsMillis | 驅逐定時器周期 | false |
blockWhenExhausted | 對象池耗盡是否block | true |