歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> 一個廣為人知但鮮有人用的技巧:對象池

一個廣為人知但鮮有人用的技巧:對象池

日期:2017/3/1 9:26:47   编辑:Linux編程

對象池是一種設計模式,它會預先初始化一組可重用的實體,而不是按需銷毀然後重建。在使用套接字描述符時,人們通常會將其池化。實際上,套接字描述符的數量通常比較少(最多上千個),之所以要采用池的方式,是因為它們的初始化成本非常高。而在最近發表的一篇 博文 中, ClojureWerkz 核心成員 Alex Petrov 探討了另一種對象池應用場景,即將大量的存活期短且初始化成本低的對象池化,以降低內存分配和再分配成本,避免內存碎片。

Alex將對象池看作是減少GC壓力的首選方法,同時也是最簡單的方法。在下面兩種分配模式下,可以選擇使用對象池:

  • 對象以固定的速度不斷地分配,垃圾收集時間逐步增加,內存使用率隨之增大;
  • 對象分配存在爆發期,而每次爆發都會導致系統遲滯,並伴有明顯的GC中斷。

在絕大多數情況下,這些對象要麼是數據容器,要麼是數據封裝器,其作用是在應用程序和內部消息總線、通信層或某些API之間充當一個信封。這很常見。例如,數據庫驅動會針對每個請求和響應創建Request和Response對象,消息系統會使用Message和Event封裝器,等等。對象池可以幫助保存和重用這些構造好的對象實例。

Alex介紹了兩種基本的對象池回收模式:“借用(borrowing)”和引用計數。前者更清晰,而後者則意味著要實現自動回收。

借用非常像垃圾收集運行時之上的 malloc/free 。自然地,在使用這種方式時,開發人員需要面對早先使用非垃圾收集語言時面對的問題。如果某個對象已經釋放並返回到池中,那麼任何對它的修改或讀取都會產生不可預見的結果。例如,在C語言中,對已釋放的指針進行任何操作都會產生塊錯誤。借用適用於有明確的開始/結束點的操作。絕大多數時候,都不要將它用於對象可以被多個線程同步訪問的情況。借用最大的優點是,它不知道對象池的存在。被借用的對象本身要有某種 reset 機制,借用和返回操作都由對象消費者完成。

引用計數在實現方面稍微復雜些,但它對數據結構提供了更細粒度的控制。將對象池封裝到一個函數式接口中,消費者就可以不必了解它,就像下面這個樣子:

(pooledObject, pooledObjectConsumer) -> {
  pooledObject.retain();
  pooledObjectConsumer.accept(pooledObject);
  pooledObject.release();
  }; 

每當對象進入上述代碼塊,調用者就會 retain 該對象,並在執行塊執行完畢後將其 release 。每個對象都持有一個內部計數器和一個指向池的引用。當計數器為0時,對象就會返回池中。

通常,引用計數用於同時有多個消費者訪問已分配對象的情況,只有當所有的消費者都釋放了對象引用時,對象才可以被回收。這種方式也適用於管道或嵌套處理。在這種情況下,開發者可以避免顯式的開始/結束操作。

分配觸發負責在池中對象不足時分配新資源。Alex介紹了如下三種分配觸發方式:

  • 空池觸發 :任何時候,只要池空了,就分配對象。這是一種最簡單的方式。
  • 水位線 :空池觸發的缺點是,某次對象請求會因為執行對象分配而中斷。為了避免這種情況,可以使用水位線觸發。當從池中請求新對象時,檢查池中可用對象的數量。如果可用對象小於某個阈值,就觸發分配過程。
  • Lease/Return速度 :大多數時候,水位線觸發已經足夠,但有時候可能會需要更高的精度。在這種情況下,可以使用 leasereturn 速度。例如,如果池中有100個對象,每秒有20個對象被取走,但只有10個對象返回,那麼9秒後池就空了。開發者可以使用這種信息,提前做好對象分配計劃。

增長策略用於指定分配過程被觸發後需要分配的對象的數量。Alex也介紹了三種方式:

  • 固定大小 :這是最簡單的對象池實現方式。對象一次性預分配,對象池後續不再增長。這種實現適用於對象數量相對確定的情況,但池大小固定可能會導致資源饑餓。
  • 小步增長 :為了避免出現資源饑餓,可以允許對象池小步增長,比如一次額外分配一個對象。
  • 塊增長 :如果無法接受分配導致的中斷,就需要保證池中任何時候都有可用的對象。這時,就必須使用塊增長。例如,每當水位線到達25%時,就將對象池增大25%。不過,這種方式容易導致內存溢出。搭配Lease/Return速度分配觸發策略,可以得出更准確的池大小。

當然,使用對象池就意味著開發者開始自己管理內存,所以需要注意以下問題:

  • 引用洩露 :對象在系統中某個地方注冊了,但沒有返回到池中。
  • 過早回收 :消費者已經決定將對象返還給對象池,但仍然持有它的引用,並試圖執行寫或讀操作,這時會出現這種情況。
  • 隱式回收 :當使用引用計數時可能會出現這種情況。
  • 大小錯誤 :這種情況在使用字節緩沖區和數組時非常常見:對象應該有不同的大小,而且是以定制的方式構造,但返回對象池後卻作為通用對象重用。
  • 重復下單 :這是引用洩露的一個變種,存在多路復用時特別容易發生:一個對象被分配到多個地方,但其中一個地方釋放了該對象。
  • 就地修改 :對象不可變是最好的,但如果不具備那樣做的條件,就可能在讀取對象內容時遇到內容被修改的問題。
  • 縮小對象池 :當池中有大量的未使用對象時,要縮小對象池。
  • 對象重新初始化 :確保每次從池中取得的對象不含有上次使用時留下的髒字段。

最後,Alex指出:

對象池並不適合所有人。在應用程序開發的早期階段就開始使用對象池是沒有意義的,因為你那時候還不能確切地知道什麼需要池化,也不確定如何池化。

Copyright © Linux教程網 All Rights Reserved