前言

我们知道,Java 创建一个实例的消耗是不小的,如果没有使用栈上分配和 TLAB,那么就需要使用 CAS 在堆中创建对象。所以现在很多框架都使用对象池。Netty 也不例外,通过重用对象,能够避免频繁创建对象和销毁对象带来的损耗。

来看看具体实现。

1. Recycler 抽象类简介

该类 doc:

Light-weight object pool based on a thread-local stack.

基于线程局部堆栈的轻量级对象池。

该类是个容器,内部主要是一个 Stack 结构。当需要使用一个实例时,就弹出,当使用完毕时,就清空后入栈。

  • 该类有 2 个主要方法:
1. public final T get() // 从 threadLocal 中取出 Stack 中首个 T 实例。
2. protected abstract T newObject(Handle<T> handle) // 当 Stack 中没有实例的时候,创建一个实例返回。
  • 该类有 4 个内部接口 / 内部类:
// 定义 handler 回收实例
public interface Handle<T> {
void recycle(T object);
} // Handle 的默认实现,可以将实例回收,放入 stack。
static final class DefaultHandle<T> implements Handle<T> // 存储对象的数据结构。对象池的真正的 “池”
static final class Stack<T> // 多线程共享的队列
private static final class WeakOrderQueue // 队列中的链表结构,用于存储多线程回收的实例
private static final class Link extends AtomicInteger
  • 实现线程局部缓存的 FastThreadLocal:
private final FastThreadLocal<Stack<T>> threadLocal = new FastThreadLocal<Stack<T>>() {
@Override
protected Stack<T> initialValue() {
return new Stack<T>(Recycler.this, Thread.currentThread(), maxCapacityPerThread, maxSharedCapacityFactor,
ratioMask, maxDelayedQueuesPerThread);
} @Override
protected void onRemoval(Stack<T> value) {
if (value.threadRef.get() == Thread.currentThread()) {
if (DELAYED_RECYCLED.isSet()) {
DELAYED_RECYCLED.get().remove(value);
}
}
}
};
  • 核心方法 get 操作
public final T get() {
if (maxCapacityPerThread == 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;
}
  • 核心方法 DefaultHandle 的 recycle 操作
public void recycle(Object object) {
if (object != value) {
throw new IllegalArgumentException("object does not belong to handle");
}
stack.push(this);
}

2. Netty 中的使用范例

io.netty.channel.ChannelOutboundBuffer.Entry 类

  • 示例代码如下:
// 实现了 Recycler 抽象类
private static final Recycler<Entry> RECYCLER = new Recycler<Entry>() {
protected Entry newObject(Handle<Entry> handle) {
return new Entry(handle);
}
}; // 创建实例
Entry entry = RECYCLER.get();
// doSomeing......
// 归还实例
entry.recycle();

从上面的 get 方法,我们知道,最终会从 threadLocal 取出 Stack,从 Stack 中弹出 DefaultHandle 对象(如果没有就创建一个),然后调用我们重写的 newObject 方法,将创建的对象和 handle 绑定。最后返回这个对象。

当调用 entry.recycle() 方法的时候,实际会调用 DefaultHandle 的 recycle 方法。我们看看该方法实现:

public void recycle(Object object) {
if (object != value) {
throw new IllegalArgumentException("object does not belong to handle");
}
stack.push(this);
}

这里的 value 就是 get 方法中赋值的。如果不相等,就抛出异常。反之,将 handle 入栈 stack。注意:这里并没有对 value 做任何处理,只是在 Entry 内部做了清空处理。所以,这个 handle 和 handle 绑定的对象就保存在了 stack 中。

下次再次调用 get 时,就可以直接从该 threadLocal 中取出 handle 和 handle 绑定的 value了。完成了一次完美的对象池的实践。也就是说,一个 handle 绑定一个实例。而这个 handle 还是比较轻量的。

从这里可以看出,Stack 就是真正的 “池子”。我们就看看这个池子的内部实现。

而这个 stack 对外常用的方法的 pop 和 push。我们就来看看这两个方法。

3. pop 方法

代码如下:

DefaultHandle<T> pop() {
int size = this.size;
if (size == 0) {
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;
}

逻辑如下:

  1. 拿到这个 Stack 的长度,实际上,这个 Stack 就是一个 DefaultHandle 数组。
  2. 如果这个长度是 0,没有元素了,就调用 scavenge 方法尝试从 queue 中转移一些数据到 stack 中。scavenge 方法待会详细再讲。
  3. 重置 size 属性和其余两个属性。返回实例。

这个方法除了 scavenge 之外,还是比较简单的。

4. push 方法

代码如下:

 void push(DefaultHandle<?> item) {
Thread currentThread = Thread.currentThread();
if (threadRef.get() == currentThread) {
pushNow(item);
} else {
pushLater(item, currentThread);
}
}

当一个对象使用 pop 方法取出来之后,可能会被别的线程使用,这时候,如果是你,你怎么处理呢?

先看看当前线程的处理:

看看 pushNow 方法:

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)) {
return;
}
if (size == elements.length) {
elements = Arrays.copyOf(elements, min(size << 1, maxCapacity));
} elements[size] = item;
this.size = size + 1;
}

该方法主要逻辑如下:

  1. 如果 Stack 大小已经大于等于最大容量或者这个 handle 在容器里了,就不做回收了。
  2. 如果数组满了,扩容一倍,最大 4096(默认)。
  3. size + 1。

看看 dropHandle 方法的实现:

boolean dropHandle(DefaultHandle<?> handle) {
// 没有被回收过
if (!handle.hasBeenRecycled) {
// 第一次是 -1,++ 之后变为0,取余7。其实如果正常情况下,结果应该都是0。
// 如果下面的判断不是0 的话,那么已经归还。这个对象就没有必要重复归还。
// 直接丢弃。
if ((++handleRecycleCount & ratioMask) != 0) {
// Drop the object.
return true;
}
// 改为被回收过,下次就不会进入了
handle.hasBeenRecycled = true;
}
// 删除失败
return false;
}

已经写了注释,就不再过多解释。

可以看到,pushNow 方法还是很简单的。由于在当前线程里,只需要还原到 Stack 的数组中就好了。

关键是:如果是其他的线程做回收操作,该怎么办?

5. pushLater 方法(多线程回收如何操作)

先说说 Netty 的解决办法和思路:

  • 每个线程都有一个 Stack 对象,每个线程也都有一个软引用 Map,键为 Stack,值是 queue。

  • 线程每次从 local 中获取 Stack 对象,再从 Stack 中取出实例。如果取不到,尝试从 queue 取,也就是从queue 中的 Link 中取出,并销毁 Link。

  • 但回收的时候,可能就不是原来的那个线程了,由于回收时使用的还是原来的 Stack,所以,需要考虑这个实例如何才能正确的回收。

  • 这个时候,就需要 Map 出场了。创建一个 queue 关联这个 Stack,将数据放到这个 queue 中。等到持有这个 Stack 的线程想拿数据了,就从 Stack 对应的 queue 中取出。

  • 看出来了吗?只有一个线程持有唯一的 Stack,其余的线程只持有这个 Stack 关联的 queue。因此,可以说,这个 queue 是两个线程共享的。除了 Stack 自己的线程外,其余的线程的归还都是放到 自己的queue 中。

  • 这个 queue 是无界的。内部的 Link 是有界的。每个线程对应一个 queue。

  • 这些线程的 queue 组成了链表。

具体如下图所示:

看完了设计,再看看代码实现:

pushLater 方法

private void pushLater(DefaultHandle<?> item, Thread thread) {
// 每个 Stack 对应一串 queue,找到当前线程的 map
Map<Stack<?>, WeakOrderQueue> delayedRecycled = DELAYED_RECYCLED.get();
// 查看当前线程中是否含有这个 Stack 对应的队列
WeakOrderQueue queue = delayedRecycled.get(this);
if (queue == null) {// 如果没有
// 如果 map 长度已经大于最大延迟数了,则向 map 中添加一个假的队列
if (delayedRecycled.size() >= maxDelayedQueues) {// 8
delayedRecycled.put(this, WeakOrderQueue.DUMMY);
return;
}
// 如果长度不大于最大延迟数,则尝试创建一个queue,链接到这个 Stack 的 head 节点前(内部创建Link)
if ((queue = WeakOrderQueue.allocate(this, thread)) == null) {
// drop object
return;
}
delayedRecycled.put(this, queue);
} else if (queue == WeakOrderQueue.DUMMY) {
// drop object
return;
} queue.add(item);
}

该方法步骤如下:

  1. 从 threadLcoal 中取出当前线程的 Map,尝试从 Map 中取出 Stack 映射的 queue。
  2. 如果没有,就调用 WeakOrderQueue.allocate(this, thread) 方法创建一个。然后将这个 Stack 和 queue 绑定。
  3. 将实例添加到这个 queue 中。

我们主要关注如何 allocate 方法,关键方法 newQueue:

@1
static WeakOrderQueue newQueue(Stack<?> stack, Thread thread) {
WeakOrderQueue queue = new WeakOrderQueue(stack, thread);
stack.setHead(queue);
return queue;
} @2
private WeakOrderQueue(Stack<?> stack, Thread thread) {
head = tail = new Link();
owner = new WeakReference<Thread>(thread);
availableSharedCapacity = stack.availableSharedCapacity;
} @3
private static final class Link extends AtomicInteger {
private final DefaultHandle<?>[] elements = new DefaultHandle[LINK_CAPACITY];
private int readIndex;
private Link next;
} @4
synchronized void setHead(WeakOrderQueue queue) {
queue.setNext(head);
head = queue;
}

代码1,2,3,4。

  1. 调用 WeakOrderQueue 构造方法,传入 stack 和 thread。
  2. 创建一个 Link 对象,赋值给链表中的 head 和 tail。
  3. Lind 的构造函数,也是一个链表。其中包含了保存实例的 Handle 数组,默认 16.
  4. 将这个新的 queue 设置为该 stack 的 head 节点。

其中,有一个需要注意的地方就是 owner = new WeakReference(thread),使用了弱引用,当这个线程对象被 GC 后,这个 owner 也会变为 null,就可以像 threadLoca 一样对该引用进行判 null,来检查这个线程对象是否回收了。

再看看如何添加进 queue 中的:

void add(DefaultHandle<?> handle) {
handle.lastRecycledId = id;
Link tail = this.tail;
int writeIndex;
if ((writeIndex = tail.get()) == LINK_CAPACITY) {
if (!reserveSpace(availableSharedCapacity, LINK_CAPACITY)) {
// Drop it.
return;
}
this.tail = tail = tail.next = new Link();
writeIndex = tail.get();
}
tail.elements[writeIndex] = handle;
handle.stack = null;
tail.lazySet(writeIndex + 1);
}

首先,拿到这个 queue 的 tail 节点,如果这个 tiail 节点满了,查看是否还有共享空间,如果没了,就丢弃这个实例。

反之,则新建一个 Link,追加到 tail 节点的尾部。然后,将数据插入新 tail 的数组。然后,将这个 handle 的 stack 属性设置成 null,表示这个 handle 不属于任何 statck 了,其他 stack 都可以使用。

数据放进去了,怎么取出来呢?

6. scavenge 方法

我们刚刚留了这个方法,现在可以开始讲了。代码如下:

boolean scavenge() {
// continue an existing scavenge, if any
// 清理成功后,stack 的 size 会变化
if (scavengeSome()) {
return true;
} // reset our scavenge cursor
prev = null;
cursor = head;
return false;
}

主要调用的是 scavengeSome 方法,返回 true 表示将 queue 中的数据转移成功。看看该方法。

boolean scavengeSome() {
WeakOrderQueue prev;
WeakOrderQueue cursor = this.cursor;
if (cursor == null) {
prev = null;
cursor = head;
if (cursor == null) {
return false;
}
} else {
prev = this.prev;
}
boolean success = false;
do {
// 将 head queue 的实例转移到 this stack 中
if (cursor.transfer(this)) {
success = true;
break;
}
// 如果上面失败,找下一个节点
WeakOrderQueue next = cursor.next;
// 如果当前线程被回收了,
if (cursor.owner.get() == null) {
// 只要最后一个节点还有数据,就一直转移
if (cursor.hasFinalData()) {
for (;;) {
if (cursor.transfer(this)) {
success = true;
} else {
break;
}
}
}
if (prev != null) {
prev.setNext(next);
}
} else {
prev = cursor;
}
cursor = next;
} while (cursor != null && !success);
// 转移成功之后,将 cursor 重置
this.prev = prev;
this.cursor = cursor;
return success;
}

方法还是挺长的。我们拆解一下:

  1. 拿到这个 stack 的 queue,调用这个 queue 的 transfer 方法,如果成功,结束循环。
  2. 如果 queue 所在的线程被回收了,就将这个线程对应的 queue 中的所有数据全部转移到 stack 中。

可以看到,最重要的还是 transfer 方法。然而该方法更长,就不贴代码了,说说主要逻辑,有兴趣可以自己看看,逻辑如下:

  1. 拿到这个 queue 的 head 节点,也就是 Link。如果 head 是 null,取出 next。
  2. 循环 Link 中的实例,将其赋值到 stack 数组中。并将刚刚 handle 置为 null 的 stack 属性赋值。
  3. 最后,将 statck 的 size 属性更新。

其中有一个疑问:为什么在其他线程插入 Link 时将 handle 的 stack 的属性置为 null?在取出时,又将 handle 的 stack 属性恢复。

答:因为如果 stack 被用户手动置为 null,而容器中的 handle 还持有他的引用的话,就无法回收了。同时 Map 也使用了软引用map,当 stack 没有了引用被 GC 回收时,对应的 queue 也就被回收了。避免了内存泄漏。实际上,在之前的 Recycler 版本中,确实存在内存泄漏的情况。

该方法的主要目的就是将 queue 所属的 Link 中的数据转移到 stack 中。从而完成多线程的最终回收。

总结

Netty 并没有使用第三方库实现对象池,而是自己实现了一个相对轻量的对象池。通过使用 threadLocal,避免了多线程下取数据时可能出现的线程安全问题,同时,为了实现多线程回收同一个实例,让每个线程对应一个队列,队列链接在 Stack 对象上形成链表,这样,就解决了多线程回收时的安全问题。同时,使用了软引用的map 和 软引用的 thradl 也避免了内存泄漏。

在本次的源码阅读中,确实收获很大。再回顾以下 Recycler 的设计图吧。设计的真的非常好。

Netty 高性能之道 - Recycler 对象池的复用的更多相关文章

  1. netty源码分析 - Recycler 对象池的设计

    目录 一.为什么需要对象池 二.使用姿势 2.1 同线程创建回收对象 2.2 异线程创建回收对象 三.数据结构 3.1 物理数据结构图 3.2 逻辑数据结构图(重要) 四.源码分析 4.2.同线程获取 ...

  2. 抓到 Netty 一个隐藏很深的内存泄露 Bug | 详解 Recycler 对象池的精妙设计与实现

    欢迎关注公众号:bin的技术小屋,如果大家在看文章的时候发现图片加载不了,可以到公众号查看原文 本系列Netty源码解析文章基于 4.1.56.Final版本 最近在 Review Netty 代码的 ...

  3. Netty 系列之 Netty 高性能之道

    1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友通过私信告诉我,通过使用 Netty4 + Thrift 压缩二进制编解码技术,他们实现了 10 W TPS(1 K 的复杂 POJO 对象)的跨 ...

  4. Netty系列之Netty高性能之道

    转载自http://www.infoq.com/cn/articles/netty-high-performance 1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友通过私信告诉我,通过使用Ne ...

  5. Netty高性能之道

    1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友告诉我,通过使用Netty4 + Thrift压缩二进制编解码技术,他们实现了10W TPS(1K的复杂POJO对象)的跨节点远程服务调用.相比于 ...

  6. 转:Netty系列之Netty高性能之道

    1. 背景 1.1. 惊人的性能数据 最近一个圈内朋友通过私信告诉我,通过使用Netty4 + Thrift压缩二进制编解码技术,他们实现了10W TPS(1K的复杂POJO对象)的跨节点远程服务调用 ...

  7. 【读后感】Netty 系列之 Netty 高性能之道 - 相比 Mina 怎样 ?

    [读后感]Netty 系列之 Netty 高性能之道 - 相比 Mina 怎样 ? 太阳火神的漂亮人生 (http://blog.csdn.net/opengl_es) 本文遵循"署名-非商 ...

  8. Netty 系列之 Netty 高性能之道 高性能的三个主题 Netty使得开发者能够轻松地接受大量打开的套接字 Java 序列化

    Netty系列之Netty高性能之道 https://www.infoq.cn/article/netty-high-performance 李林锋 2014 年 5 月 29 日 话题:性能调优语言 ...

  9. Netty(五)Netty 高性能之道

    4.背景介绍 4.1.1 Netty 惊人的性能数据 通过使用 Netty(NIO 框架)相比于传统基于 Java 序列化+BIO(同步阻塞 IO)的通信框架,性能提升了 8 倍多.事 实上,我对这个 ...

随机推荐

  1. PowerDesigner常用功能介绍

    PowerDesigner常用功能:1:把SQL脚步导入PowerDesigner打开powerdesigner,选择File --> Reverse Engineer --> Datab ...

  2. 分形之谢尔宾斯基(Sierpinski)三角形

    谢尔宾斯基三角形(英语:Sierpinski triangle)是一种分形,由波兰数学家谢尔宾斯基在1915年提出,它是一种典型的自相似集.也有的资料将其称之为谢尔宾斯基坟垛. 其生成过程为: 取一个 ...

  3. 如何将Excel 图表导出

    简单分三步: Step1: Alt+ F11 --> 调出 VBA: Step2: Ctrl+G (开关键)—>调出立即窗口: Step3:  在立即窗口输入  activesheet.C ...

  4. SignalR2简易数据看板演示

    软件环境: 1.vs2015.windows7..net4.5 演示说明: 当点击按钮的时候,柱状图数值加1并实时变化 1.首先打开vs2015创建一个mvc项目,并安装SignalR2,具体操作可参 ...

  5. .net core中Quartz的使用

    原来工作中有用到定时任务Quartz,不过是在MVC项目中,现在net core项目中也要用到,就开始改版.中间发现在网上的教程只有执行定时计划的过程,却很少有人写注册的过程,觉得有点略坑.所以写此文 ...

  6. VS动态修改App.config中遇到的坑(宿主进程问题)

    昨天遇到了很奇怪的一个bug,具体描述如下: 这个系统是c/s架构的针对多个工厂做的资材管理系统,由于有很多个工厂,每个工厂都有自己的服务器.所以需要动态的改变连接字符串去链接不同的服务器. 由于这个 ...

  7. Spring IOC 容器源码分析 - 填充属性到 bean 原始对象

    1. 简介 本篇文章,我们来一起了解一下 Spring 是如何将配置文件中的属性值填充到 bean 对象中的.我在前面几篇文章中介绍过 Spring 创建 bean 的流程,即 Spring 先通过反 ...

  8. 《Python黑帽子:黑客与渗透测试编程之道》 自动化攻击取证

    工具安装: 下载源码:https://code.google.com/archive/p/volatility/downloads 工具配置: 获取内存镜像:https://www.downloadc ...

  9. jquery中ajax的几种方式

    三种简写: $.get(URL,data,function(data,status,xhr),dataType) $(selector).post(URL,data,function(data,sta ...

  10. flask中邮件发送方法

    from flask import Flask from flask_mail import Mail, Message app = Flask(__name__) #配置邮件:服务器/端口/传输层安 ...