什么是對象池技術(shù)?對象池應用在哪些地方识啦?
對象池其實就是緩存一些對象從而避免大量創(chuàng)建同一個類型的對象永脓,類似線程池的概念。對象池緩存了一些已經(jīng)創(chuàng)建好的對象贸宏,避免需要時才創(chuàng)建對象造寝,同時限制了實例的個數(shù)。池化技術(shù)最終要的就是重復的使用池內(nèi)已經(jīng)創(chuàng)建的對象吭练。從上面的內(nèi)容就可以看出對象池適用于以下幾個場景:
- 創(chuàng)建對象的開銷大
- 會創(chuàng)建大量的實例
- 限制一些資源的使用
如果創(chuàng)建一個對象的開銷特別大诫龙,那么提前創(chuàng)建一些可以使用的并且緩存起來(池化技術(shù)就是重復使用對象,提前創(chuàng)建并緩存起來重復使用就是池化)可以降低創(chuàng)建對象時的開銷鲫咽。
會大量創(chuàng)建實例的場景赐稽,重復的使用對象可減少創(chuàng)建的對象數(shù)量叫榕,降低GC的壓力(如果這些對象的生命周期都很短暫,那么可以降低YoungGC的頻率姊舵;如果生命周期很長晰绎,那么可以避免掉這些對象被FullGC——生命周期長凝果,且大量創(chuàng)建俐巴,這里就要結(jié)合系統(tǒng)的TPS等考慮池的大小了)袍嬉。
對于限制資源的使用更多的是一種保護策略敢课,比如數(shù)據(jù)庫鏈接池苇瓣。除去這些對象本身的開銷外负蠕,他們對外部系統(tǒng)也會造成壓力串绩,比如大量創(chuàng)建鏈接對DB也是有壓力的聚凹。那么池化除了優(yōu)化資源以外构资,本身限制了資源數(shù)抽诉,對外部系統(tǒng)也起到了一層保護作用。
如何實現(xiàn)對象池吐绵?
開源實現(xiàn):Apache Commons Pool
自己實現(xiàn):Netty輕量級對象池實現(xiàn)
Apache Commons Pool開源軟件庫提供了一個對象池API和一系列對象池的實現(xiàn)迹淌,支持各種配置,比如活躍對象數(shù)或者閑置對象個數(shù)等己单。DBCP數(shù)據(jù)庫連接池基于Apache Commons Pool實現(xiàn)唉窃。
Netty自己實現(xiàn)了一套輕量級的對象池。在Netty中纹笼,通常會有多個IO線程獨立工作纹份,基于NioEventLoop的實現(xiàn),每個IO線程輪詢單獨的Selector實例來檢索IO事件廷痘,并在IO來臨時開始處理蔓涧。最常見的IO操作就是讀寫,具體到NIO就是從內(nèi)核緩沖區(qū)拷貝數(shù)據(jù)到用戶緩沖區(qū)或者從用戶緩沖區(qū)拷貝數(shù)據(jù)到內(nèi)核緩沖區(qū)笋额。這里會涉及到大量的創(chuàng)建和回收Buffer元暴,Netty對Buffer進行了池化從而降低系統(tǒng)開銷。
<h3>Netty對象池實現(xiàn)分析</h3>
上面提到了IO操作中會涉及到大量的緩沖區(qū)操作鳞陨,NIO提供了兩種Buffer最為緩沖區(qū):DirectByteBuffer和HeapByteBuffer昨寞。Netty在兩種緩沖區(qū)的基礎上進行了池化進而提升性能。
DirectByteBuffer
DirectByteBuffer顧名思義是直接內(nèi)存(Direct Memory)上的Byte緩存區(qū)厦滤,直接內(nèi)存不是JVM Runtime數(shù)據(jù)區(qū)域的一部分援岩,也不是Java虛擬機規(guī)范中定義的內(nèi)存區(qū)域。簡單的說這部分就是機器內(nèi)存掏导,分配的大小等都和虛擬機限制無關(guān)享怀。JDK1.4中開始我們可以使用native方法在直接內(nèi)存上來分配內(nèi)存,并在JVM堆內(nèi)存上維持一個引用來進行訪問趟咆,當JVM堆內(nèi)存上的引用被回收后添瓷,這塊內(nèi)存被操作系統(tǒng)回收梅屉。
HeapByteBuffer
HeapByteBuffer是在JVM堆內(nèi)存上分配的Byte緩沖區(qū),可以簡單的理解為byte[]數(shù)組的一種封裝鳞贷∨魈溃基于HeapByteBuffer的寫流程通常要先在直接內(nèi)存上分配一個臨時的緩沖區(qū),將數(shù)據(jù)從Heap拷貝到直接內(nèi)存搀愧,然后再將直接內(nèi)存的數(shù)據(jù)發(fā)送到IO設備的緩沖區(qū)惰聂,之后回收直接內(nèi)存。讀流程也類似咱筛。使用DirectByteBuffer避免了不必要的拷貝工作搓幌,所以在性能上會有提升。
DirectByteBuffer的缺點在于分配和回收的的代價相對較大迅箩,因此DirectByteBuffer適用于緩沖區(qū)可以重復使用的場景溉愁。
Netty的池化實現(xiàn)
以Buffer為例,對應直接內(nèi)存和堆內(nèi)存饲趋,Netty的池化分別為PooledDirectByteBuffer和PolledHeapByteBuffer拐揭。
通過PooledDirectByteBuffer的API定義可以看到,它的構(gòu)造方法是私有的篙贸,而創(chuàng)建一個實例的入口是:
static PooledDirectByteBuf newInstance(int maxCapacity) {
PooledDirectByteBuf buf = RECYCLER.get();
buf.reuse(maxCapacity);
return buf;
}
可見RECYCLER是池化的核心投队,創(chuàng)建對象時都通過RECYCLER.get來獲得一個實例(Recycler就是Netty實輕量級池化技術(shù)的核心)枫疆。
Recycler實現(xiàn)分析(源碼分析)
/**
* Light-weight object pool based on a thread-local stack.
*
* @param <T> the type of the pooled object
*/
public abstract class Recycler<T>
從注釋可以看出Netty基于thread-local實現(xiàn)了輕量級的對象池爵川。
Recycler的API非常簡單:
- get():獲取一個實例
- recycle(T, Handle<T>):回收一個實例
- newObject(Handle<T>):創(chuàng)建一個實例
get流程
@SuppressWarnings("unchecked")
public final T get() {
if (maxCapacity == 0) {
return newObject((Handle<T>) NOOP_HANDLE);
}
Stack<T> stack = threadLocal.get();
DefaultHandle<T> handle = stack.pop();
if (handle == null) {
handle = stack.newHandle();
handle.value = newObject(handle);
}
return (T) handle.value;
}
get的簡化流程(這里先不深究細節(jié)):
- 拿到當前線程對應的stack
- 從stack中pop出一個元素
- 如果不為空則返回,否則創(chuàng)建一個新的實例
可以大概明白Stack是對象池化背后存儲實例的數(shù)據(jù)結(jié)構(gòu):如果能從stack中拿到可用的實例就不再創(chuàng)建新的實例息楔。
recycle流程
一個“池子”最核心的就是做兩件事情寝贡,第一個是上面的Get,即從池子中拿出一個可用的實例值依。另一個就是在用完后將數(shù)據(jù)放回到池子中(線程池圃泡、連接池都是這樣)。
public final boolean recycle(T o, Handle<T> handle) {
if (handle == NOOP_HANDLE) {
return false;
}
DefaultHandle<T> h = (DefaultHandle<T>) handle;
if (h.stack.parent != this) {
return false;
}
h.recycle(o);
return true;
}
public void recycle(Object object) {
if (object != value) {
throw new IllegalArgumentException("object does not belong to handle");
}
Thread thread = Thread.currentThread();
if (thread == stack.thread) {
stack.push(this);
return;
}
// we don't want to have a ref to the queue as the value in our weak map
// so we null it out; to ensure there are no races with restoring it later
// we impose a memory ordering here (no-op on x86)
Map<Stack<?>, WeakOrderQueue> delayedRecycled = DELAYED_RECYCLED.get();
WeakOrderQueue queue = delayedRecycled.get(stack);
if (queue == null) {
delayedRecycled.put(stack, queue = new WeakOrderQueue(stack, thread));
}
queue.add(this);
}
回收一個實例核心的步驟由以上兩個方法組成:Recycler的recycle方法和DefaultHandle的recycle方法愿险。
Recycler的recycle方法主要做了一些參數(shù)驗證颇蜡。
DefaultHandle的recycle方法流程如下:
- 如果當前線程是當前stack對象的線程,那么將實例放入stack中辆亏,否則:
- 獲取當前線程對應的Map<Stack, WeakOrderQueue>风秤,并將實例加入到Stack對應的Queue中。
從獲取實例和回收實例的代碼可以看出扮叨,整個對象池的核心實現(xiàn)由ThreadLocal和Stack及WrakOrderQueue構(gòu)成缤弦,接著來看Stack和WrakOrderQueue的具體實現(xiàn),最后概括整體實現(xiàn)彻磁。
Stack實體
Stack<T>
parent:Recycler // 關(guān)聯(lián)對應的Recycler
thread:Thread // 對應的Thread
elements:DefaultHandle<?>[] // 存儲DefaultHandle的數(shù)組
head:WeakOrderQueue // 指向WeakOrderQueue元素組成的鏈表的頭部“指針”
cursor,prev:WrakOrderQueue // 當前游標和前一元素的“指針”
pop實現(xiàn)
DefaultHandle<T> pop() {
int size = this.size;
if (size == 0) {
if (!scavenge()) {
return null;
}
size = this.size;
}
size --;
DefaultHandle ret = elements[size];
if (ret.lastRecycledId != ret.recycleId) {
throw new IllegalStateException("recycled multiple times");
}
ret.recycleId = 0;
ret.lastRecycledId = 0;
this.size = size;
return ret;
}
- 如果size為0(這里的size表示stack中可用的元素)碍沐,嘗試進行scavenge狸捅。
- 返回elements中的最后一個元素。
boolean scavenge() {
// continue an existing scavenge, if any
if (scavengeSome()) {
return true;
}
// reset our scavenge cursor
prev = null;
cursor = head;
return false;
}
boolean scavengeSome() {
WeakOrderQueue cursor = this.cursor;
if (cursor == null) {
cursor = head;
if (cursor == null) {
return false;
}
}
boolean success = false;
WeakOrderQueue prev = this.prev;
do {
if (cursor.transfer(this)) {
success = true;
break;
}
WeakOrderQueue next = cursor.next;
if (cursor.owner.get() == null) {
// If the thread associated with the queue is gone, unlink it, after
// performing a volatile read to confirm there is no data left to collect.
// We never unlink the first queue, as we don't want to synchronize on updating the head.
if (cursor.hasFinalData()) {
for (;;) {
if (cursor.transfer(this)) {
success = true;
} else {
break;
}
}
}
if (prev != null) {
prev.next = next;
}
} else {
prev = cursor;
}
cursor = next;
} while (cursor != null && !success);
this.prev = prev;
this.cursor = cursor;
return success;
}
簡要概括上面的流程就是Stack從“背后”的Queue中獲取可用的實例累提,如果Queue中沒有可用實例就遍歷到下一個Queue(Queue組成了一個鏈表)尘喝。
push實現(xiàn)
void push(DefaultHandle<?> item) {
if ((item.recycleId | item.lastRecycledId) != 0) {
throw new IllegalStateException("recycled already");
}
item.recycleId = item.lastRecycledId = OWN_THREAD_ID;
int size = this.size;
if (size >= maxCapacity) {
// Hit the maximum capacity - drop the possibly youngest object.
return;
}
if (size == elements.length) {
elements = Arrays.copyOf(elements, Math.min(size << 1, maxCapacity));
}
elements[size] = item;
this.size = size + 1;
}
push相對pop流程要更加簡單,直接將回收的元素放到隊尾(實際是一個數(shù)組)斋陪。
WeakOrderQueue實體
WeakOrderQueue
head,tail:Link // 內(nèi)部元素的指針(WeakOrderQueue內(nèi)部存儲的是一個Link的鏈表)
next:WeakOrderQueue // 指向下一個WeakOrderQueue的指針
owner:Thread // 對應的線程
WeakOrderQueue核心包含兩個方法瞧省,add方法將元素添加到自身的“隊列”中,transfer方法將自己擁有的元素“傳輸”到Stack中鳍贾。
Linke結(jié)構(gòu)如下
private static final class Link extends AtomicInteger {
private final DefaultHandle<?>[] elements = new DefaultHandle[LINK_CAPACITY];
private int readIndex;
private Link next;
}
Link內(nèi)部包含了一個數(shù)組用于存放實例鞍匾,同時標記了讀取位置的索引和下一個Link元素的指針。
結(jié)合Link的結(jié)構(gòu)骑科,Weak的結(jié)構(gòu)如下:
add方法
void add(DefaultHandle<?> handle) {
handle.lastRecycledId = id;
Link tail = this.tail;
int writeIndex;
if ((writeIndex = tail.get()) == LINK_CAPACITY) {
this.tail = tail = tail.next = new Link();
writeIndex = tail.get();
}
tail.elements[writeIndex] = handle;
handle.stack = null;
// we lazy set to ensure that setting stack to null appears before we unnull it in the owning thread;
// this also means we guarantee visibility of an element in the queue if we see the index updated
tail.lazySet(writeIndex + 1);
}
add操作將元素添加到tail指向的Link對象中橡淑,如果Link已滿則創(chuàng)建一個新的Link實例。
transfer方法
boolean transfer(Stack<?> dst) {
Link head = this.head;
if (head == null) {
return false;
}
if (head.readIndex == LINK_CAPACITY) {
if (head.next == null) {
return false;
}
this.head = head = head.next;
}
final int srcStart = head.readIndex;
int srcEnd = head.get();
final int srcSize = srcEnd - srcStart;
if (srcSize == 0) {
return false;
}
final int dstSize = dst.size;
final int expectedCapacity = dstSize + srcSize;
if (expectedCapacity > dst.elements.length) {
final int actualCapacity = dst.increaseCapacity(expectedCapacity);
srcEnd = Math.min(srcStart + actualCapacity - dstSize, srcEnd);
}
if (srcStart != srcEnd) {
final DefaultHandle[] srcElems = head.elements;
final DefaultHandle[] dstElems = dst.elements;
int newDstSize = dstSize;
for (int i = srcStart; i < srcEnd; i++) {
DefaultHandle element = srcElems[i];
if (element.recycleId == 0) {
element.recycleId = element.lastRecycledId;
} else if (element.recycleId != element.lastRecycledId) {
throw new IllegalStateException("recycled already");
}
element.stack = dst;
dstElems[newDstSize ++] = element;
srcElems[i] = null;
}
dst.size = newDstSize;
if (srcEnd == LINK_CAPACITY && head.next != null) {
this.head = head.next;
}
head.readIndex = srcEnd;
return true;
} else {
// The destination stack is full already.
return false;
}
}
transfer方法收件根據(jù)stack的容量和自身擁有的實例數(shù)咆爽,計算出最終需要轉(zhuǎn)移的實例數(shù)梁棠。之后就是數(shù)組的拷貝和指標的調(diào)整。
基本上所有的流程有個大致的了解斗埂,下面從整體的角度回顧一下Netty對象池的實現(xiàn)符糊。
整體實現(xiàn)
結(jié)構(gòu)
整個設計上核心的幾點:
1. Stack相當于是一級緩存,同一個線程內(nèi)的使用和回收都將使用一個Stack
2. 每個線程都會有一個自己對應的Stack呛凶,如果回收的線程不是Stack的線程男娄,將元素放入到Queue中
3. 所有的Queue組合成一個鏈表,Stack可以從這些鏈表中回收元素(實現(xiàn)了多線程之間共享回收的實例)