對象池化的藝術

概述

對象池化的技術的出現都是能夠說是不得以而爲之,若是咱們有足夠快的CPU,足夠大的內存,那麼對象池化的技術是徹底不必,各類垃圾回收也是不必的;但凡事總有個可是,資源老是有限的,如何在有限資源下發揮出最優效果,也是自人類誕生以來一直在探索的問題。javascript

Tomcat-高效的環形隊列

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

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

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

在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是單線程的執行不會遇到併發問題,但做爲一名後端程序猿,併發以及線程安全是你必須考慮到。bash

分析代碼能夠發現,咱們須要維護一個索引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的設計思想倍感親切,都是分而治之的思想的最好實踐。

相關文章
相關標籤/搜索