Netty中需要大量创建PooledDirectByteBuf对象,为了避免高并发场景频发创建对象的开销,从而引入了对象池来统一管理PooledDirectByteBuf对象。
而使用对象池的好处就是可以直接从池子里取预先创建好的对象,再调用recycle方法进行回收,这样就减少了对象的创建开销,而且也能提高gc效率。
对象池无锁化设计
如果把对象池做出全局的,会有同步问题。而Netty为了避免,内部采用了FastThreadLocal去给每一个线程创建对象池(也就是每一个线程都有对应的Stack对象,弹栈创建对象,入栈回收对象),也就避免了加锁问题,也就是空间换时间来达到无锁化。
但这也会导致另一个对象回收的问题:
在单线程情况下,是没有任何问题的。
但在多线程场景下,一个线程A(创建线程)创建对象,但是对象的回收可能不是再由线程A进行回收,可能由线程B(回收线程)回收。如果这时多个线程对Stack并发释放回收对象,这就出现了同步竞争的情况。
因此Netty为每个线程分配了一个WeakOrderQueue节点,每次出现对象回收的时候,就先判断是否为创建线程,是的话直接入栈Stack即可,不是的话就先暂时将回收对象存放到WeakOrderQueue的Link节点中。后续创建线程要获取对象时,先从Stack中获取,数量为0时再从其他回收线程中transfer转移一些对象到Stack中,从而实现了无锁化。
给出大致结构,图片来源网络

对象池的实例Recycle
Filed
//用于区分回收线程id,标识池化对象是由哪个线程id回收的
private static final AtomicInteger ID_GENERATOR = new AtomicInteger(Integer.MIN_VALUE);
//标识创建池化对象的线程id
//static final 说明所有的创建线程都是同一个id
private static final int OWN_THREAD_ID = ID_GENERATOR.getAndIncrement();
//这里只需要能区分出创建线程及回收线程,不同的创建线程之间是各自独立的不需要区分线程id
private static final FastThreadLocal<Map<Stack<?>, WeakOrderQueue>> DELAYED_RECYCLED =
new FastThreadLocal<Map<Stack<?>, WeakOrderQueue>>() {
@Override
protected Map<Stack<?>, WeakOrderQueue> initialValue() {
return new WeakHashMap<Stack<?>, WeakOrderQueue>();
}
};
这里用weakHashmap的原因就是,极端下如果创建线程挂掉且被Gc回收后,Stack结构已经不会再使用到了,因此Gc会回收掉Stack。而对应的这个map的Entry也会被删除。
Handle接口
有默认的实现类 DefaultHandle,其实就是存放的需要池化的对象。
//标识最近被哪个线程id回收
private int lastRecycledId;
//标识最终被哪个线程id回收
private int recycleId;
//池化对象是否被回收指所属的Stack中
boolean hasBeenRecycled;
//这里是建立该对象与所属Stack的关系
private Stack<?> stack;
//真正的池化对象值
private Object value;
这里是为了避免重复回收问题,引入了这两个变量。而对象回收分为以下几种情况:
1.被回收前,latsRecycledId = recycleId = 0
2.由创建线程回收,latsRecycledId = recycleId = OWN_THREAD_ID
3.由回收线程回收,latsRecycledId=回收线程,recycleId = 0。当创建线程进行transfer时,会将latsRecycledId 再赋值给recycleId。这时才是真正回收了latsRecycledId = recycleId = 回收线程id
recycle 回收对象
//回收对象方法
public void recycle(Object object) {
if (object != value) {
throw new IllegalArgumentException("object does not belong to handle");
}
Stack<?> stack = this.stack;
if (lastRecycledId != recycleId || stack == null) {
throw new IllegalStateException("recycled already");
}
//入栈
stack.push(this);
}
Stack
重要属性
// we keep a queue of per-thread queues, which is appended to once only, each time a new thread other
// than the stack owner recycles: when we run out of items in our stack we iterate this collection
// to scavenge those that can be reused. this permits us to incur minimal thread synchronisation whilst
// still recycling all items.
//Stack结构所属的对象池实例 parent
final Recycler<T> parent;
// We store the Thread in a WeakReference as otherwise we may be the only ones that still hold a strong
// Reference to the Thread itself after it died because DefaultHandle will hold a reference to the Stack.
//
// The biggest issue is if we do not use a WeakReference the Thread may not be able to be collected at all if
// the user will store a reference to the DefaultHandle somewhere and never clear this reference (or not clear
// it in a timely manner).
//关联当前Stack创建线程
final WeakReference<Thread> threadRef;
//所有回收线程能帮助创建线程回收的对象个数
final AtomicInteger availableSharedCapacity;
//作为回收线程时能帮助回收对象的最大数量,由io.netty.recycler.maxDelayedQueuesPerThread控制,默认值CPU * 2
final int maxDelayedQueues;
//Stack结构中最大的对象数量
private final int maxCapacity;
private final int ratioMask;
//对象元素数组
private DefaultHandle<?>[] elements;
private int size;
private int handleRecycleCount = -1; // Start with -1 so the first one will be recycled.
//用于回收线程临时存储回收对象的 当前节点指针,前一节点指针
private WeakOrderQueue cursor, prev;
//头节点指针
private volatile WeakOrderQueue head;
至于为什么用弱引用WeakReference来关联创建线程。是因为在其他回收线程如果存在这么一个引用链池化对象->DefaultHandler->Stack->threadRef。若线程挂掉,可能会因为这条引用链存在而导致无法回收
push回收对象
void push(DefaultHandle<?> item) {
Thread currentThread = Thread.currentThread();
//查看Stack所属的创建线程是否与当前线程一致
if (threadRef.get() == currentThread) {
// The current Thread is the thread that belongs to the Stack, we can try to push the object now.
//当前线程属于创建线程,这里就直接往Stack中push即可
pushNow(item);
} else {
// The current Thread is not the one that belongs to the Stack
// (or the Thread that belonged to the Stack was collected already), we need to signal that the push
// happens later.
//属于回收线程,这里进行无锁化回收对象
pushLater(item, currentThread);
}
}
//逻辑比较简单,直接入栈即可
private void pushNow(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 || dropHandle(item)) {
// Hit the maximum capacity or should drop - drop the possibly youngest object.
return;
}
if (size == elements.length) {
elements = Arrays.copyOf(elements, min(size << 1, maxCapacity));
}
elements[size] = item;
this.size = size + 1;
}
private void pushLater(DefaultHandle<?> item, Thread thread) {
// 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(this);
//queue为空代表是第一次有回收线程回收对象
if (queue == null) {
if (delayedRecycled.size() >= maxDelayedQueues) {
// Add a dummy queue so we know we should drop the object
delayedRecycled.put(this, WeakOrderQueue.DUMMY);
return;
}
// Check if we already reached the maximum number of delayed queues and if we can allocate at all.
//这里就将Stack引用传到WeakOrderQueue构造函数中,但WeakOrderQueue不会直接持有Stack的引用,看后续分析
if ((queue = WeakOrderQueue.allocate(this, thread)) == null) {
// drop object
return;
}
delayedRecycled.put(this, queue);
} else if (queue == WeakOrderQueue.DUMMY) {
// drop object
return;
}
//上面代码都是为了取到回收线程对应的queue
queue.add(item);//往queue里加回收对象
}
这里分析下,将回收对象放入回收线程对应的queue中,这里就是用了线程隔离的思想(给每个线程都有对应的queue),实现了无锁化的对象回收。这也解释了上述为啥多线程回收时没有锁Stack实现线程同步。
setHead设置WeakOrderQueue头节点
这里也是全局加锁的地方,采用头插法
// Marked as synchronized to ensure this is serialized.
synchronized void setHead(WeakOrderQueue queue) {
queue.setNext(head);
head = queue;
}
pop 创建对象
DefaultHandle<T> pop() {
//size统计Stack现有对象数量,同时指向栈顶+1指针
int size = this.size;
if (size == 0) {
//stack中没有可用对象,就到回收线程中转移一批过来
if (!scavenge()) {
return null;
}
size = this.size;
}
size --;
//取栈顶
DefaultHandle ret = elements[size];
elements[size] = null;
if (ret.lastRecycledId != ret.recycleId) {
throw new IllegalStateException("recycled multiple times");
}
//这里对回收对象进行初始化
ret.recycleId = 0;
ret.lastRecycledId = 0;
this.size = size;
return ret;
}
scavenge
boolean scavenge() {
// continue an existing scavenge, if any
if (scavengeSome()) {
return true;
}
//走到这说明转移失败了
//会重置prev指针为null,当前指针指向头节点
// reset our scavenge cursor
prev = null;
cursor = head;
return false;
}
scavengeSome
boolean scavengeSome() {
WeakOrderQueue prev;
WeakOrderQueue cursor = this.cursor;
//cursor游标为空 说明是第一次从其他回收线程中取对象
if (cursor == null) {
prev = null;
cursor = head;
if (cursor == null) {
//没有任何其他回收线程有回收对象WeakOrderQueue
return false;
}
} else {
//指向前继
prev = this.prev;
}
boolean success = false;
do {
//这里到WeakOrderQueue 转移一批对象过来
if (cursor.transfer(this)) {
success = true;
//转移成功即跳出循环
break;
}
//走到这说明转移失败了
//继续到下一个WeakOrderQueue 查看有无回收对象
WeakOrderQueue next = cursor.next;
//这里说明回收线程已经消亡
//owner是个弱引用
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.
//如果消亡的回收线程中还有回收对象,则循环将全部的对象都转移到Stack中
if (cursor.hasFinalData()) {
for (;;) {
if (cursor.transfer(this)) {
success = true;
} else {
break;
}
}
}
//prev不等于null就说明不是头节点,就可以删除
if (prev != null) {
//这里消亡线程所对应的cursor:WeakOrderQueue就会从链中摘除
prev.setNext(next);
}
} else {
prev = cursor;
}
cursor = next;
} while (cursor != null && !success);
//重新赋值
this.prev = prev;
this.cursor = cursor;
return success;
}
1.if (prev != null)至于为什么要判prev是否为空,是因为移除WeakOrderQueue 节点是依靠prev指针,但当prev=null时,说明cursor节点为头节点。此时就不会进行移除节点。
因为操作WeakOrderQueue链表的头节这里用了同步,如果移除会带来同步开销,所以就算回收线程已经挂掉了也不会在本次遍历中将其移除(不着急删除)。
当后续又有新线程进来的时候,会头插加入WeakOrderQueue链表,后续再遍历到此消亡线程时,就已经不是头节点了,这时就会将其删掉。
2.之前的WeakHashMap,key为Stack,value为WeakOrderQueue。而这里用了weakhashmap已经保证了当创建线程挂掉时,创建线程(stack持有的是弱引用)和Stack都能被回收掉。但weakorderqueue的移除,便是在这里prev.setNext(next);删除了引用链(WeakOrderQueue就没有任何引用了),所以才能GC掉对应回收线程的WeakOrderQueue。
WeakOrderQueue
// chain of data items
private final Head head;
private Link tail;
// pointer to another queue of delayed items for the same stack
private WeakOrderQueue next;
//这里同样采用弱引用,也是为了防止回收线程如果挂了,能及时GC掉回收线程
//被Gc后,owner会变成null
//否则存在引用链会导致无法回收
private final WeakReference<Thread> owner;
//回收线程id
private final int id = ID_GENERATOR.getAndIncrement();
newQueue为回收线程创建一个对应的WeakOrderQueue
static WeakOrderQueue newQueue(Stack<?> stack, Thread thread) {
final WeakOrderQueue queue = new WeakOrderQueue(stack, thread);
// Done outside of the constructor to ensure WeakOrderQueue.this does not escape the constructor and so
// may be accessed while its still constructed.
stack.setHead(queue);
return queue;
}
构造方法
private WeakOrderQueue(Stack<?> stack, Thread thread) {
tail = new Link();
// Its important that we not store the Stack itself in the WeakOrderQueue as the Stack also is used in
// the WeakHashMap as key. So just store the enclosed AtomicInteger which should allow to have the
// Stack itself GCed.
// 传入共享变量给Head
head = new Head(stack.availableSharedCapacity);
head.link = tail;
owner = new WeakReference<Thread>(thread);
}
这里Netty也做了解释,为什么WeakOrderQueue不直接持有Stack的引用。是因为Stack是有可能会被GC回收的。
而Stack已经之前作为WeakHashMap的key,可以被回收,如果这里继续持有Stack引用的话,就会导致无法被回收而引发内存泄漏。
所以这里只用了stack的属性去初始化WeakOrderQueue对象。
Head结构
// This act as a place holder for the head Link but also will reclaim space once finalized.
// Its important this does not hold any reference to either Stack or WeakOrderQueue.
static final class Head {
//这里是被多线程共享的,用来统计多个回收线程总共帮助回收的线程数量,
需要考虑并发安全
private final AtomicInteger availableSharedCapacity;
Link link;
Head(AtomicInteger availableSharedCapacity) {
this.availableSharedCapacity = availableSharedCapacity;
}
/// TODO: In the future when we move to Java9+ we should use java.lang.ref.Cleaner.
@Override
protected void finalize() throws Throwable {
try {
super.finalize();
} finally {
Link head = link;
link = null;
while (head != null) {
reclaimSpace(LINK_CAPACITY);
Link next = head.next;
// Unlink to help GC and guard against GC nepotism.
head.next = null;
head = next;
}
}
}
void reclaimSpace(int space) {
assert space >= 0;
availableSharedCapacity.addAndGet(space);
}
boolean reserveSpace(int space) {
return reserveSpace(availableSharedCapacity, space);
}
static boolean reserveSpace(AtomicInteger availableSharedCapacity, int space) {
assert space >= 0;
for (;;) {
int available = availableSharedCapacity.get();
if (available < space) {
return false;
}
if (availableSharedCapacity.compareAndSet(available, available - space)) {
return true;
}
}
}
}
Link结构
// Let Link extend AtomicInteger for intrinsics. The Link itself will be used as writerIndex.
//Link直接继承了AtomicInter 这样就可以直接调用get()就可以做写指针来用
@SuppressWarnings("serial")
static final class Link extends AtomicInteger {
//这里放了一批的回收到的池化对象
private final DefaultHandle<?>[] elements = new DefaultHandle[LINK_CAPACITY];
//记录读指针
private int readIndex;
Link next;
}
Link直接继承了AtomicInteger。
1.这样就可以直接调用get()就可以做写指针来用。
2.而且回收线程在回收对象和创建线程在转移对象时,都会并发对写指针使用,因此也保证了线程安全
readIndex
这里是创建线程在转移对象的时候需要用到读指针,但只会被创建线程使用到,是个单线程,不需要考虑可见性和原子性,也就没有并发安全。
add
void add(DefaultHandle<?> handle) {
handle.lastRecycledId = id;
//这里是从尾部Link进行尾插回收!!!
Link tail = this.tail;
int writeIndex;
if ((writeIndex = tail.get()) == LINK_CAPACITY) {
if (!head.reserveSpace(LINK_CAPACITY)) {
// Drop it.
return;
}
// We allocate a Link so reserve the space
//创建新Link 并建立指针关系
this.tail = tail = tail.next = new Link();
writeIndex = tail.get();
}
//上述都是在赋值writeIndex写指针
//这里直接把回收对象放到写指针上
tail.elements[writeIndex] = handle;
//这里需要将stack置null
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
//用lazySet
tail.lazySet(writeIndex + 1);
}
1.这里又将回收对象handle.stack置为null的原因:
首先回收对象handle.stack我们都知道,只会与创建线程的stack绑定,而后续回收对象再次需要使用该对象时,又同样将回收对象handle.stack与创建线程绑定。这样看起来是多此一举。
handle.stack绑定创建线程的stack参见代码回收线程transfer方法(element.stack = dst)
但是并不是,因为在创建线程可能被GC的情况下,此时有两种情况:
- 1.池化对象仍在使用,仍持有stack的引用,自然stack不会被GC。(但池化对象后续也是会被归还回收,而创建线程被GC了,也不会再有池化对象的出现了,这个stack引用链也将不复存在。)
- 2.池化对象被回收了,已经进入了WeakOrderQueue中。但是这里的对象如果还继续持有stack引用,那么stack将永远不会被GC就导致内存泄漏。
2.这里使用lazySet延时更新writeIndex,这样并不会保证多线程之间对writeIndex的可见性。
而writeIndex在创建线程和回收线程会并发访问,如果创建线程在transfer对象,就不会看到新回收的对象。为了保证实时可见性,就需要引入内存屏障,这样的话肯定会导致回收对象的性能问题,所以Netty是刻意用了最终可见性。
最终可见性也能实现后续创建线程再继续transfer对象时,就能看到后面回收的对象了。
AtomicInteger.set 会刷新缓存行,而 AtomicInteger.lazySet 不会!!!这种方法可以减少内存屏障的开销,但仍然保证了可见性(最终)。
3.writeIndex = tail.get(); tail.elements[writeIndex] = handle;
这里很容易误以为会出现线程并发写入的问题,其实并不会。
- 因为上述介绍过回收对象放入回收线程对应的queue中,这里就是用了线程隔离的思想(给每个线程都有对应的queue)。也就是只有一个回收线程会对这个queue进行操作,所以自然不会存在并发写的问题。
- 而实际上出现并发问题是创建线程和回收线程这两个线程之间才会出现的。
hasFinalData
tail节点是回收对象插入的节点,所以在这里直接判断tail的读指针和写指针是否一致,一致说明没有回收对象存在了
boolean hasFinalData() {
return tail.readIndex != tail.get();
}
transfer
// transfer as many items as we can from this queue to the stack, returning true if any were transferred
@SuppressWarnings("rawtypes")
boolean transfer(Stack<?> dst) {
Link head = this.head.link;
if (head == null) {
return false;
}
if (head.readIndex == LINK_CAPACITY) {
if (head.next == null) {
//已经没有Link了,直接转移失败
return false;
}
//head已经读取完了,直接指向下一个Link
this.head.link = head = head.next;
}
//当前Link读指针
final int srcStart = head.readIndex;
//当前Link写指针
int srcEnd = head.get();
//当前Link可回收对象数量
final int srcSize = srcEnd - srcStart;
if (srcSize == 0) {
return false;
}
//Stack栈顶位置
final int dstSize = dst.size;
final int expectedCapacity = dstSize + srcSize;
//这里大于的话就需要扩容Stack了
if (expectedCapacity > dst.elements.length) {
final int actualCapacity = dst.increaseCapacity(expectedCapacity);
srcEnd = 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");
}
srcElems[i] = null;
//这里是为了防止Stack扩容太快,所以限制了频率
if (dst.dropHandle(element)) {
// Drop the object.
continue;
}
element.stack = dst;
dstElems[newDstSize ++] = element;
}
if (srcEnd == LINK_CAPACITY && head.next != null) {
// Add capacity back as the Link is GCed.
this.head.reclaimSpace(LINK_CAPACITY);
this.head.link = head.next;
}
head.readIndex = srcEnd;
if (dst.size == newDstSize) {
return false;
}
dst.size = newDstSize;
return true;
} else {
// The destination stack is full already.
return false;
}
}

被折叠的 条评论
为什么被折叠?



