1. 程式人生 > 程式設計 >物件池化的藝術

物件池化的藝術

概述

物件池化的技術的出現都是可以說是不得以而為之,如果我們有足夠快的CPU,足夠大的記憶體,那麼物件池化的技術是完全沒必要,各種垃圾回收也是沒必要的;但凡事總有個但是,資源總是有限的,如何在有限資源下發揮出最優效果,也是自人類誕生以來一直在探索的問題。

Tomcat-高效的環形佇列

Tomcat是在Java技術體系中常用的Web容器,其採用的NIO(非阻塞I/O)模型相較於傳統的BIO(阻塞I/O)來說獲得了更高的效能。其NIO模型如下圖所示。

Acceptor用於阻塞的接收連線,在接收到連線之後選擇一個Poller來執行後續I/O任務處理。(Poller數量是固定的)

如果是你,你會如何實現這個選擇Poller的過程呢?不妨先考慮一下

在Tomcat中會將Poller儲存在一個環形佇列中,並通過一個原子變數來迴圈獲取佇列中的下一個元素,如下圖所示。

圖片來自於點選訪問,自己畫的實在太醜了

環形佇列在物理意義上是以線性陣列(連結串列亦可)的方式進行儲存的,並非是真的是圓形的方式存在在記憶體中。

我們可以使用javascript來快速體驗一下環形陣列.

let pollers = [1,2,3,4,5,6]
let index = 0
let getNext = function(){
    return pollers[Math.abs(index++) % pollers.length]
}
for(let i = 0
; i < 10086 ; i++){ console.log(getNext()) } 複製程式碼

可以看出環形佇列的實現就在於取餘操作可以將我們的索引index給限制pollers.length範圍內,使得我們永遠可以取到佇列中下一個元素,如果佇列被取完了,則會回到佇列的頭部重新開始遍歷。

僅僅如此嗎?在JS中你這樣子玩完全沒問題,因為瀏覽器的JavaScript是單執行緒的執行不會遇到併發問題,但作為一名後端程式猿,併發以及執行緒安全是你必須考慮到。

分析程式碼可以發現,我們需要維護一個索引index來標誌當前所在的位置,因此如果我們將index用原子類儲存,這樣就不會遇到執行緒安全問題也不用加鎖。

因此Poller物件池的實現其實挺簡單的,如下程式碼所示,pollerRotater是一個原子類,可以保證我們無鎖,且執行緒安全的獲取下一個索引(原子類的相關介紹,可以看這位老哥的文章)

    public Poller getPoller0() {
        int idx = Math.abs(pollerRotater.incrementAndGet()) % pollers.length;
        return pollers[idx];
    }
複製程式碼

Jetty-精打細算的房產投資者

如果你是手頭比較緊的房產投資人,考慮一下如何投資房產才能使效益最大化?通常來說有一下幾種選擇

  • 高位接盤,借遍了親戚朋友順便掏空了六個錢包,結果血本無歸
  • 買二手房,你接手了各種型別房子並改造成了各種型別的出租房(單身公寓、兩居、三居室等等)租給客戶,於是你每年都有了固定的收入(和城中村的二房東聊過,一年幾十萬是有的)

同理,對於計算機來說記憶體和CPU都是珍貴的資源,如果你一開始建立了大量的物件,那麼將佔據大量的資源,並且很有可能這些物件一個都不回被複用並且還會使你的記憶體溢位,服務崩潰。(當然,如果你伺服器記憶體足夠大,當我沒說)

因此,我們並可以回收那些不再需要用到物件,並儲存到我們物件池中,因此要被回收的物件需要有恢復到最初使的狀態。(租客不再續租房子了,我們需要對房子進行清理一遍租給其他客戶)

此外我們還可以對物件進一步細化進行分類,以滿足不同型別的需求(如單身客戶一般都租單人間,有老婆孩子都會租大一點的)

那麼物件池化技術在Jetty中都使怎麼應用的呢?

我們知道,不論使用NIO或者BIO都需要提供一個緩衝區以供讀寫資料,並且這些緩衝區會被頻繁的使用到,因此Jetty為緩衝區設計了一個物件池ByteBufferPool

ArrayByteBufferPool

ArrayByteBufferPool是ByteBufferPool的一個實現

預設情況下ArrayByteBufferPool的結構如下圖所示

如上圖所示Bucket使用線性陣列來儲存,每個Bucket裝的都是不同大小的ByteBuffer緩衝區,以適應不同緩衝區大小需求。預設的有64個BucketByteBuffer的基礎大小稱為Factor在此圖中factor的大小為1024。

為什麼要對緩衝區大小進行分類?原因很簡單,充分利用資源(你讓一個單身漢去租三居室,這不害人嗎,有錢的話,當我沒說)

並且我使用Fiddler簡單統計了一下訪問掘金首頁過程中常見的資料包大小。

  • 請求的資料包大都在 100b400b 之間(說明如果我們使用jetty的話還是有優化空間的,如將factor調整為512以節省空間)
  • 響應的資料報在100b1000kb之間,最大的主要是靜態資源(js、css)等完全可以放在CDN上來減輕Web容器的壓力

如你所看到,對ByteBuffer按大小進行分類可以讓我們充分利用資源,並且通過調整factor引數來減少記憶體的佔用來實現進一步的優化。

注意 實際儲存ByteBuffer的是ConcurrentLinkedDeque,因為名字太長所以用其介面來表示

那麼,如何根據緩衝區呢大小獲取相應的Bucket,使用以下公式即可
Bucket索引=(目標緩衝區大小 - 1 ) / factor

在本例中,factor是1024,如果要想要一個10086大小的緩衝區應有
(10086 - 1)/1024 = 10 即Bucekt得陣列索引為10,是Bucekt陣列中的第十一個元素其ByteBuffer的大小為1024*11

至於為什麼要將緩衝區大小減一,相信你稍微思考一下便知曉

值得注意是,ArrayByteBufferPool並不會在一開始就立即為所有Bucket分配ByteBufferPool。而是在需要使用的時候先判斷有沒有目標大小的ByteBuffer,如果有則從相應的Bucekt中取一個返回給呼叫方,如果沒有則新建一個 。在不需要使用的時候由呼叫方主動歸還給ArrayByteBufferPool

除此之外,還可以為ArrayByteBufferPool指定最大記憶體(避免耗盡記憶體造成記憶體溢位),當快取的ByteBuffer的大小總和超過這個值的時候會執行清理工作,將舊的Bucekt清除掉。

有興趣的可以閱讀相應類的原始碼

org.eclipse.jetty.io.ArrayByteBufferPool
複製程式碼

總結

分而治之可以說是人類解決問題基本方法論。如果你瞭解ConcurrentHashMap的分段鎖,那麼你就應該會對Jetty的ByteBufferPool的設計思想倍感親切,都是分而治之的思想的最好實踐。