Netty的对象池

insaneXs 2020-11-08 16:58:48
Netty 博客园 技术开发 对象


简介

这一篇文章来讲讲Netty的对象池技术。文章中的代码均是依据4.1的版本来分析。
和内存管理的侧重点不同的是,对象池技术主要负责的是针对对象的回收。
换句话说,对象池技术针对的是对象的回收,管理的主体是对象,只不过对象也需要内存空间才能创建,因此在这个过程中,内存只是对象的载体。
而内存管理技术针对的是独立的内存块,管理的主体是对象,但是我们又需要一个对象来表示这个内存块的引用,以便于我们访问,因此在这个过程中,对象其实是内存的载体。
因为这两种技术经常会一起使用,所以在开始后续流程的学习前,还是务必先理清二者的区别。

对象池——Recycler

Recycler 类就是对象池,对象管理的关键逻辑都在这个类上。
Recycler 是一个抽象的泛型类。泛型参数表示实际使用场景下,需要负责管理的对象类型。
虽然这个类被声明为抽象的,但是对象管理的主体逻辑都已经在固定了——Recycler 大部分方法都被声明为final,说明它并不希望子类去修改这些逻辑。而留给子类拓展的仅仅是newObject()方法,当池中没有缓存的对象时,用来创建新的对象(因为创建对象的逻辑可能需要用户自己定义)。

从属性开始分析

属性分成两个部分:
一、是全局的配置,通常用作在没有设置初始值的情况下提供默认的处置值。这类属性都是类的静态属性。

  • DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD默认值是4 * 1024
  • DEFAULT_MAX_CAPACITY_PER_THREAD默认会使用DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD做默认值,即 4 * 1024
  • INITIAL_CAPACITY默认值为DEFAULT_MAX_CAPACITY_PER_THREAD或是256
  • MAX_SHARED_CAPACITY_FACTOR默认值为2
  • MAX_DELAYED_QUEUES_PER_THREAD 默认值为CPU个数的两倍
  • LINK_CAPACITY默认值为16 LINK的大小(LINK是队列中的一个节点,LINK之间互相连接,形成队列,同时LINK内部又是一个数组,可以存放多个对象,数组的大小就由LINK_CAPACITY控制)
  • RATIO 默认值为8
    以上这些参数默认值均可以通过特定的系统参数进行调整。

二、是对象的配置,如果在创建Recycler时,构造函数中带了相关的配置,那么这些配置会覆盖默认参数。

  • maxCapacityPerThread,对应的默认值就是上文的DEFAULT_MAX_CAPACITY_PER_THREAD,表示每个线程的最大容量,即表示Stack的最大栈深度(Stack及其作用将在下文介绍)
  • maxSharedCapacityFactor 对应的默认值就是上文的MAX_SHARED_CAPACITY_FACTOR,,表示每个线程maxCapacityPerThreadsharedCapacity的比例关系,即 sharedCapacity = maxCapacity / maxSharedCapacityFactor
  • ratioMask 和上文的RATIO相关,表示回收比例,用来控制回收的频率,避免回收的过快
  • maxDelayedQueuesPerThread, 和上文MAX_DELAYED_QUEUES_PER_THREAD相关,表示每个线程允许拥有的最大Queue的数量(Queue及其作用也会在下文详细介绍)

真正影响对象池的配置是这四个相关属性,上文的静态属性只是给这个配置项提供了默认值。

此外,类当中还有一个成员变量FastThreadLocal<Stack<T>> threadLocal。了解jdk的读者应该知道ThreadLocal是用来存放线程本地变量的,而FastThreadLocalThreadLocal作用相同,但是对性能进行了优化。从泛型参数中我们可以看到此时存放的是Stack类的对象。
已经一个静态变量DELAYED_RECYCLED,同样是FastThreadLocal,只不过保存的类型的是Map,其中Map的Key是Stack,而Value是WeakOrderQueue。后面我们会了解到这个变量保存了某个线程为其他Stack创建的WeakOrderQueue。

几个内部类及其之间的关联

Recycler的属性还是比较少的,但是内部类却有好几个,分别是:

  • Stack——用来存放回收对象
  • WeakOrderQueue——存放其他线程回收的对象
  • DefaultHandle——对象句柄

Stack——用来存放回收对象的栈

Stack是存储回收对象的核心类。当回收对象时,会通过入栈的方式将对象押入Stack中保存(push()过程)。而申请对象时,会通过出栈的方式将保存的对象弹出给申请者(pop()过程)。
同时,每个线程都有自己的Stack实例(可以从上文的FastThreadLocal<Statk<T>>中确定),说明每个线程最终都是各自回收自己创建的对象并保存(注意是最终,其他线程可能参与帮助回收的工作,并暂存到WeakOrderQueue中过渡)。

这里的Stack并没有直接使用JDK中提供的java.util.Stack,因为java.util.Stack不具备这里所需的一些额外特性。而是直接依赖数组重新实现。
来了解下Stack的内部结构:


//关联的对象池 Recycler对象
final Recycler<T> parent;
//栈拥有线程的引用(所属线程)
final WeakReference<Thread> threadRef;
/****由Recycler的相关属性设置******/
//可共享的容量
final AtomicInteger availableSharedCapacity;
//队列的数量
final int maxDelayedQueues;
//栈最大深度
private final int maxCapacity;
//控制回收比例
private final int ratioMask;
//栈底层依赖的数组 存放的是句柄——DefaultHandle,而非直接对象的引用
private DefaultHandle<?>[] elements;
//栈大小
private int size;
//回收计数,配合ratioMask 可以决定此次是否回收
private int handleRecycleCount = -1; // Start with -1 so the first one will be recycled.
/********** WeakOrderQueue形成的链表*****************/
//当前指针,前一个指针;用来决定从哪些WeakOrderQueue中转移对象到Stack中
private WeakOrderQueue cursor, prev; //cursor 记录当前WeakOrderQueue链表的位置 因为链表是头插 所以需要cursor标记
//链表的实际表头
private volatile WeakOrderQueue head; //真正的链表头节点 每次创建新的WeakQueue时 会作为头节点插入链表

从上面的代码中我们可以了解到一下几点信息:

  • Stack内部使用数组存放对象句柄(DefaultHandler),栈的最大深度即数组的容量,由Recycler的相关属性确定
  • 每个Stack都是线程私有的,Stack的拥有线程通过threadRef记录
  • Stack内部有一个WeakOrderQueue的链表,除了记录链表的表头(head)外,还且记录了链表的当前的游标(cursor),和有标的前继节点(prev)

Stack的代码暂时先分析到这,下文会在对象回收和申请的流程中再详细介绍。

DefaultHandle——默认的对象句柄

DefualtHandle是接口Handle的默认实现,该接口声明了一个方法——void recycle(Object object),即在对象发生回收时,由句柄开始发起回收流程。

在早起的Netty版本中,Recycler直接提供了回收的接口,但是这个接口已经被废弃了,取而代之的就是Handler.recycle的接口。这样可以隐藏Stack和Recycler的一些细节。

DefaultHandle是Handle的默认实现,内部结构相对简单。

 static final class DefaultHandle<T> implements Handle<T> {
//记录回收的id 和是否被回收的状态
private int lastRecycledId;
private int recycleId;
boolean hasBeenRecycled;
//句柄关联的stack
private Stack<?> stack;
//句柄引用的对象
private Object value;
//构造方法 与stack绑定
DefaultHandle(Stack<?> stack) {
this.stack = stack;
}
//回收动作,对象入栈
@Override
public void recycle(Object object) {
if (object != value) {
throw new IllegalArgumentException("object does not belong to handle");
}
//将对象推入栈中
stack.push(this);
}
}

DefaultHandle的代码相对简单,从上面的代码中也可以总结出几点:

  • 句柄通过value对象持有对象的引用
  • 句柄和Stack对象是相互关联的,Stack分配对象后,对象的句柄就和该Stack绑定了,这样从句柄就知道该对象是哪个Stack分配的,继而也能推断出是哪个线程负责创建的

WeakOrderQueue——线程帮助回收非本线程创建的对象的暂存地

从整体来看的WeakOrderQueue的作用是用来暂存回收的对象的。那么什么样的对象会被WeakOrderQueue先暂存,而不是直接保存在Stack中呢?
答案是如果执行回收的线程不是对象的创建线程(前文已经介绍了句柄知道关联的Stack及线程),那么此次回收将会被暂存到WeakOrderQueue中过度。
这样做的好处是可以减少线程间的竞争,提高吞吐量。

从内部来看,WeakOrderQueue是由Link组成的链表,可以将Link看作是链表中的一个节点。
Link相关代码:

 static final class Link extends AtomicInteger {
//DefaultHandle的数组,存放回收的句柄
private final DefaultHandle<?>[] elements = new DefaultHandle[LINK_CAPACITY];
//记录读索引,剩下的都是未读的部分
private int readIndex;
//指向下一节点,形成链表
Link next;
}

Link链表的表头是一个特殊的结构,主要的作用有两个,一个是在自身被回收时,通过finalize()实现释放操作,另一个是在添加节点时,需要先确认共享空间是否还有剩余,避免超出maxSharedCapacity的限制。

WeakOrderQueue除了上述介绍的两个特殊的属性外,其他属性相对简单。

 //哑元节点
static final WeakOrderQueue DUMMY = new WeakOrderQueue();
//头节点
private final Head head;
//尾节点
private Link tail;
// pointer to another queue of delayed items for the same stack
//前文已经介绍过的Stack内部会有WeakOrderQueue形成的链表,就是通过这个next指针串联的
private WeakOrderQueue next;
//关联的线程 这里的thread不是stack的线程,而是weakOrderQueue中的线程
private final WeakReference<Thread> owner;
//ID号
private final int id = ID_GENERATOR.getAndIncrement();

总结几点:

  • WeakOrderQueue内部是Link构成的链表,每个Link有一个DefaultHandle的数组,用来保存对象
  • WeakOrderQueue之间互相形成链表,表示某个Stack下的所有WeakOrderQueue

相关流程

在了解了Recycler及内部主要类的结构之后,我们再通过Recycler回收及申请流程,加深各个变量和内部类的作用。

对象回收流程

前文提到过,对象的回收流程是从调用DefaultHandle.recycle()方法开始。我们便以此为入口,来看看对象回收的流程。

  1. 开始回收后,句柄会首先校验回收的对象即引用的对象,然后由内部关联的Stack通过入栈操作,回收对象,即stack.push(defaultHandle);
  2. 具体的入栈过程根据执行回收动作的线程是否是该stack的拥有者分为pushNow()pushLater()两个过程
  3. 如果回收的线程A就是该stack的拥有者,说明是线程A回收自己创建的对象,那么通过pushNow()直接将对象回收到Stack内部的数组中保存(当然,也需要考虑ratioMask和数组的容量,前者用来控制回收的频率,避免回收过快;而后者用来控制回收的最大数量,避免回收过多)
  4. 如果回收的线程A不是该stack的拥有者,说明不是对象的创建线程回收(我们将对象的创建线程先称为B),那么会进入pushLater()尝试将对象先暂存到特定的WearOrderQueue中。如果找特定的WeakOrderQueue呢?首先,通过前文介绍的类型为FastThreadLocal的变量DELAYED_RECYCLED,先获取回收线程A创建的所有的WeakOrderQueue,得到一个Map对象,在通过stack对象去查找线程A是否为该stack创建过WeakOrderQueue。如果没创建,则尝试创建一个WeakOrderQueue(但如果已经线程A创建的WeakOrderQueue已经到达最大数量或者该Stack的最大共享容量已经不够,那么将不会创建新的WeakOrderQueue,也就不会再去回收该对象。此外,对于前者的情况,还会在map中为该Stack关联一个特殊的哑元节点DUMMY,表示不会再尝试创建新WeakOrderQueue)。如果能新建WeakOrderQueue或是已经有WeakOrderQueue,那么会由WeakOrderQueue暂存对象。即将对象保存在WeakOrderQueue内部Link链表的尾节点的数组中。如果尾节点容量已经满了,会新建一个Link节点,并添加到链表的尾部,成为新的tail节点。同理,新的Link节点的创建也需要考虑是否超过最大共享容量avaliableSharedCapacity,如果超过了,则拒绝创建新的Link节点,也不回收该对象。

对象申请流程

对象的申请流程是从Recycler.get()开始的,即从对象池中获取对象。流程如下:

  1. 获取线程关联的Stack,前文已经介绍过了每个线程都有自己的Stack来保存对象。如果该线程还没有Stack,则通过initValue()创建一个Stack。Recycler的相关属性值会被用来创建Stack。
  2. 从Stack中尝试弹出对象(stack.pop()),如果此时能够弹出对象,说明该Stack之前回收过对象。如果没有回收到的对象,则会创建一个新对象。
  3. 创建新对象分为两步,第一创建由stack.newHandle()创建对象句柄,第二,由要通过newObject(handle)方法创建对象,这是一个Recycler的抽象方法,由具体的对象池子类根据管理对象的不用自行实现。得到的DefaultHandle会持有对象的引用。

新对象的创建过程还是比较简单的,主要还是理解从Stack中弹出对象的过程。我们已经了解到回收的对象可能存放在Stack内部的数组和WeakOrderQueue中Link的数组两个地方,其实弹出也正是从这里找对象,并返回。
首先,出栈过程会先从栈中获取元素,如果此时栈中没有元素,那么会从WeakOrderQueue中将暂存的元素移动到栈中。然后再从栈的尾部获取元素。

Recycler相关类之间的关系

简单将Recycler内部的类之间的关系画了一个图。帮助读者理解不同线程下给个类之间的关联。

思考

Netty其实已经提供了一个非常强大的对象池框架,利用这套框架我们也可以很容易的实现自己的对象池需求,譬如连接池等。
详细的源码注释可以见为的Github

版权声明
本文为[insaneXs]所创,转载请带上原文链接,感谢
https://www.cnblogs.com/insaneXs/p/13944871.html

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云