netty Recycler对象池
前言
池化思想在实际开发中有很多应用,指的是针对一些创建成本高,创建频繁的对象,用完不弃,将其缓存在对象池子里,下次使用时优先从池子里获取,如果获取到则可以直接使用,以此降低创建对象的开销。
我们最熟悉的数据库连接池就是一种池化思想的应用,数据库操作是非常频繁的,数据库连接的创建、销毁开销很大,每次都需要进行TCP三次握手和四次挥手,权限检查等,所以如果每次操作数据库都重新创建连接,用完就丢弃,对于应用程序来说是不可接受的。在java世界里,一切皆对象,所以需要有一个数据库对象连接池,用于保存连接池对象。例如使用hikari,可以配置spring.datasource.hikari.maximum-pool-size=20,表示最多可以池化20个数据库连接对象。
此外,频繁的创建销毁对象还会影响GC,当一个对象使用完,再没被GC root引用,就变成不可达,所引用的内存可以被垃圾回收,GC是需要STW的,频繁的GC也会影响程序的吞吐量。
本篇我们要介绍的是netty的对象池Recycler,Recycler是对象池核心类,netty为了减少依赖,以及追求高性能,并没有使用第三方的对象池,而是自己设计了一套。
netty在高并发处理IO读写,内存对象的使用是非常频繁的,如果每次都重新申请,无疑性能会大打折扣,特别是对于堆外内存,申请和销毁的成本更高,所以对内存对象使用池化是很有必要的。
例如:PooledHeapByteBuf,PooledDirectByteBuf,ChannelOutboundBuffer.Entry都使用了对象池,这些类内部都有一个Recycler静态变量和一个Handle实例变量。
static final class Entry {
private static final Recycler<Entry> RECYCLER = new Recycler<Entry>() {
@Override
protected Entry newObject(Handle<Entry> handle) {
return new Entry(handle);
}
};
private final Handle<Entry> handle;
}
原理
我们先通过一个例子感受一下Recycler的使用,然后再来分析它的原理。
public final class Connection {
private Recycler.Handle handle;
private Connection(Recycler.Handle handle) {
this.handle = handle;
}
private static final Recycler<Connection> RECYCLER = new Recycler<Connection>() {
@Override
protected Connection newObject(Handle<Connection> handle) {
return new Connection(handle);
}
};
public static Connection newInstance() {
return RECYCLER.get();
}
public void recycle() {
handle.recycle(this);
}
public static void main(String[] args) {
Connection c1 = Connection.newInstance();
int hc1 = c1.hashCode();
c1.recycle();
Connection c2 = Connection.newInstance();
int hc2 = c2.hashCode();
c2.recycle();
System.out.println(hc1 == hc2); //true
}
}
代码非常简单,我们用final修饰Connection,这样就无法通过继承创建对象。同时构造方法定义为私有,防止外部直接new创建对象,这样就只能通过newInstance静态方法创建对象。
Recycler是一个抽象类,newObject是它的抽象方法,这里使用匿名类继承Recycler并重写newObject,用于创建一个新的对象。
Handle是一个接口,Recycler会创建并通过newObject方法传进来,默认是DefaultHandle,它的作用是用来回收对象,放回对象池。
接着我们创建两个Connection实例,可以看到它们的hashcode是一样的,证明是同一个对象。
需要注意的是,使用对象池创建的对象,用完需要调用recycle回收。
原理分析
想象一下,如果由我们设计,怎么设计一个高性能的对象池呢?对象池的操作很简单,一取一放,但考虑到多线程,实际情况就变得复杂了。
如果只有一个全局的对象池,多线程操作需要保证线程安全,那就需要通过加锁或者CAS,这都会影响存取效率,由于线程竞争,锁等待,可能通过对象池获取对象的效率还不如直接new一个,这样就得不偿失了。
针对这种情况,已经有很多的经验供我们借鉴,核心思想都是一样的,降低锁竞争。例如ConcurrentHashMap,通过每个节点上锁,hash到不同节点的线程就不会相互竞争;例如ThreadLocal,通过在线程级别绑定一个ThreadLocalMap,每个线程操作的都是自己的私有变量,不会相互竞争;再比如jvm在分配内存的时候,内存区域是共享的,所以jvm为每个线程设计了一块私有的TLAB,可以高效进行内存分配,关于TLAB可以参考:这篇文章。
这种无锁化的设计在netty中非常常见,例如对象池,内存分配,netty还设计了FastThreadLocal来代替jdk的ThreadLocal,使得线程内的存取更加高效。
Recycler设计如下:

如上图,Recycler内部维护了两个重要的变量,Stack和WeakOrderQueue,实际对象就是包装成DefaultHandle,保存在这两个结构中。
默认情况一个线程最多存储4 * 1024个对象,可以根据实际情况,通过Recycler的构造函数指定。
private static final int DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD = 4 * 1024; // Use 4k instances as default.
Stack是一个栈结构,是线程私有的,Recycler内部通过FastThreadLocal进行定义,对Stack的操作不会有线程安全问题。
private final FastThreadLocal<Stack<T>> threadLocal = new FastThreadLocal<Stack<T>>() {};
FastThreadLocal是netty版的ThreadLocal,搭配FastThreadLocalThread,FastThreadLocalMap使用,主要优化jdk ThreadLocal扩容需要rehash,和hash冲突问题。
当获取对象时,就是尝试从Stack栈顶pop出一个对象,如果有,则直接使用。如果没有就尝试从WeakOrderQueue“借”一点过来,放到Stack,如果借不到,那就调用newObject()创建一个。
WeakOrderQueue主要是用来解决多线程问题的,考虑这种情况,线程A创建的对象,可能被线程B使用,那么对象的释放就应该由线程B决定。如果线程B也将对象归还到线程A的Stack,那就出现了线程安全问题,线程A对Stack的读取,写入就需要加锁,影响并发效率。
为了无锁化操作,netty为其它每个线程都设计了一个WeakOrderQueue,各个线程只会操作自己的WeakOrderQueue,不会有并发问题了。其它线程的WeakOrderQueue会通过指针构成一个链表,Stack对象内部通过3个指针指向链表,这样就可以遍历整个链表对象。
站在线程A的角度,其它线程就是B,C,D...,站在线程B的角度,其它线程就是A,C,D...
从上图可以看到,WeakOrderQueue实际不是一个队列,内部是由一些Link对象构成的双向链表,它也是一个链表。
Link对象是一个包含读写索引,和一个长度为16的数组的对象,数组存储的就是DefaultHandler对象。
整个过程是这样的,当本线程从Stack获取不到可用对象时,就会通过cursor指针变量WeakOrderQueue链表,开始从其它线程获取对象。如果找到一个可用的Link,就会将整个Link里的对象迁移到Stack,然后删除链表节点,为了保证效率,每次最多迁移一个Link。如果还获取不到,就通过newObject()方法创建一个新的对象。
Recycler#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;
}
pop方法判断Stack没有对象,就会调用scavenge方法,从WeakOrderQueue迁移对象。scavenge,翻译过来是拾荒,捡的意思。
DefaultHandle<T> pop() {
int size = this.size;
if (size == 0) {
if (!scavenge()) {
return null;
}
size = this.size;
}
//...
}
最终会调用到WeakOrderQueue的transfer方法,这个方法比较复杂,主要是对WeakOrderQueue链表和内部Link链表的遍历。
这里dst就是前面说的Stack对象,可以看到会把element元素迁移过去。
boolean transfer(Stack<?> dst) {
//...
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;
if (dst.dropHandle(element)) {
// Drop the object.
continue;
}
element.stack = dst;
dstElems[newDstSize ++] = element;
}
}
//...
}
应用
我们项目使用了mybatis plus作为orm,其中用得最多的就是QueryWrapper了,每次查询都需要new一个QueryWrapper。例如:
QueryWrapper<User> queryWrapper = new QueryWrapper();
queryWrapper.eq("uid", 123);
return userMapper.selectOne(queryWrapper);
数据库查询是非常频繁的,QueryWrapper的创建虽然不会很耗时,但过多的对象也会给GC带来压力。
QueryWrapper是mp提供的类,它没有池化的实现,不过我们可以参考上面netty DefaultHandle的思路,在它外面再包一层,然后池化包装后的对象。
回收的时候还要注意清空对象的属性,例如上面给uid赋值了123,下个对象就不能用这个条件,否则就乱套了,QueryWrapper提供了clear方法可以重置所有属性。
同时,每次用完都需要手动recycle也是比较麻烦的,开发容易忘记,可以借助AutoCloseable接口,使用try-with-resource的写法,在结束后自动完成回收。
对于修改和删除还有UpdateWrapper和DeleteWrapper,同样思路也可以实现。
有了这些思路,代码就出来了:
public final class WrapperUtils {
private WrapperUtils() {}
private static final Recycler<PooledQueryWrapper> QUERY_WRAPPER_RECYCLER = new Recycler<PooledQueryWrapper>() {
@Override
protected PooledQueryWrapper newObject(Handle<PooledQueryWrapper> handle) {
return new PooledQueryWrapper<>(handle);
}
};
public static <T> PooledQueryWrapper<T> newInstance() {
return QUERY_WRAPPER_RECYCLER.get();
}
static class PooledQueryWrapper<T> implements AutoCloseable {
private QueryWrapper<T> queryWrapper;
private Recycler.Handle<PooledQueryWrapper> handle;
public PooledQueryWrapper(Recycler.Handle<PooledQueryWrapper> handle) {
this.queryWrapper = new QueryWrapper<>();
this.handle = handle;
}
public QueryWrapper<T> getWrapper() {
return this.queryWrapper;
}
@Override
public void close() {
queryWrapper.clear();
handle.recycle(this);
}
}
}
使用如下,可以看到打印出来的hashcode都是一样的,每次执行后都会自动调用close方法,进行QueryWrapper属性重置。
public static void main(String[] args) {
try (PooledQueryWrapper<Case> objectPooledWrapper = WrapperUtils.newInstance()) {
QueryWrapper<Case> wrapper = objectPooledWrapper.getWrapper();
wrapper.eq("age", 1);
wrapper.select("id,name");
wrapper.last("limit 1");
System.out.println(wrapper.hashCode());
}
try (PooledQueryWrapper<Case> objectPooledWrapper = WrapperUtils.newInstance()) {
QueryWrapper<Case> wrapper = objectPooledWrapper.getWrapper();
wrapper.eq("age", 2);
wrapper.select("id,email");
wrapper.last("limit 2");
System.out.println(wrapper.hashCode());
}
try (PooledQueryWrapper<Case> objectPooledWrapper = WrapperUtils.newInstance()) {
QueryWrapper<Case> wrapper = objectPooledWrapper.getWrapper();
wrapper.eq("age", 3);
wrapper.select("id,phone");
wrapper.last("limit 3");
System.out.println(wrapper.hashCode());
}
}
总结
之前我们也分析过apache common pool,这也是一个池化实现,在redis客户端也有应用,但它是通过加锁解决并发问题的,设计没有netty这么精细。
上面的源码来自netty4.1.42,从整体上看整个Recycler的设计还是比较复杂的,主要为了解决多线程竞争和GC问题,导致整个代码复杂度比较高,所以netty在后来的版本中对其进行重构。
不过这不影响我们对它思想的学习,以后也可以借鉴到实际开发中。
更多分享,欢迎关注我的github:https://github.com/jmilktea/jtea
netty Recycler对象池的更多相关文章
- 抓到 Netty 一个隐藏很深的内存泄露 Bug | 详解 Recycler 对象池的精妙设计与实现
欢迎关注公众号:bin的技术小屋,如果大家在看文章的时候发现图片加载不了,可以到公众号查看原文 本系列Netty源码解析文章基于 4.1.56.Final版本 最近在 Review Netty 代码的 ...
- netty源码分析 - Recycler 对象池的设计
目录 一.为什么需要对象池 二.使用姿势 2.1 同线程创建回收对象 2.2 异线程创建回收对象 三.数据结构 3.1 物理数据结构图 3.2 逻辑数据结构图(重要) 四.源码分析 4.2.同线程获取 ...
- Netty轻量级对象池实现分析
什么是对象池技术?对象池应用在哪些地方? 对象池其实就是缓存一些对象从而避免大量创建同一个类型的对象,类似线程池的概念.对象池缓存了一些已经创建好的对象,避免需要时才创建对象,同时限制了实例的个数.池 ...
- Netty 高性能之道 - Recycler 对象池的复用
前言 我们知道,Java 创建一个实例的消耗是不小的,如果没有使用栈上分配和 TLAB,那么就需要使用 CAS 在堆中创建对象.所以现在很多框架都使用对象池.Netty 也不例外,通过重用对象,能够避 ...
- 基于Netty包中的Recycler实现的对象池技术详解
一.业务背景 当项目中涉及到频繁的对象的创建和回收的时候,就会出现频繁GC的情况,这时就出现了池化的技术来实现对象的循环使用从而避免对象的频繁回收,Netty包下的Recycler就实现了这一功能.当 ...
- Netty源码解析 -- 对象池Recycler实现原理
由于在Java中创建一个实例的消耗不小,很多框架为了提高性能都使用对象池,Netty也不例外. 本文主要分析Netty对象池Recycler的实现原理. 源码分析基于Netty 4.1.52 缓存对象 ...
- java对象池化技术
https://blog.csdn.net/tiane5hao/article/details/85957840 文章目录 先写一个简单通用的对象池 通过上面的通用池实现jedis连接池 连接池测试 ...
- 基于Apache组件,分析对象池原理
池塘里养:Object: 一.设计与原理 1.基础案例 首先看一个基于common-pool2对象池组件的应用案例,主要有工厂类.对象池.对象三个核心角色,以及池化对象的使用流程: import or ...
- 设计模式之美:Object Pool(对象池)
索引 意图 结构 参与者 适用性 效果 相关模式 实现 实现方式(一):实现 DatabaseConnectionPool 类. 实现方式(二):使用对象构造方法和预分配方式实现 ObjectPool ...
- Egret中的对象池ObjectPool
为了可以让对象复用,防止大量重复创建对象,导致资源浪费,使用对象池来管理. 对象池具体含义作用,自行百度. 一 对象池A 二 对象池B 三 字符串key和对象key的效率 一 对象池A /** * 对 ...
随机推荐
- 2、数据库:SQL Server部署 - 系统部署系列文章
对于微软的SQL Server的安装,以前已经有写过了,到了2022版本,安装没多大的改变,很多只需要少配置,然后直接下一步即可.现在是2023年了,SQL Server已经出到了2022版本,这篇博 ...
- 【技能篇】解决vs编译器scanf等函数不安全问题【手把手操作-一分钟解决】
[技能篇]解决Vs编译器scanf等函数不安全问题 文章目录 说在前面 博主给大家的福利 解决方案 解决过程 尾声 说在前面 大家刚开始学习编程的时候,使用vs编译器.使用scanf等函数的时候遇到的 ...
- PHP常用类
PHP常用类 一.分页类 <?php /** * 分页类 * 调用方式: * $p=new Page(总条数,显示页码链接数量,当前页码,每页显示条数,[链接]); * print_r($p-& ...
- [FATAL] [DBT-06103] 端口 (1,521) 已在使用
今天参考之前文章 Oracle 19c快速安装部署 在一个新的环境进行安装时,发现配置数据库时报错1521端口被占用: [root@OEL7 media]# /etc/init.d/oracledb_ ...
- Linux中单引号和双引号的区别
区别:单引号属于强引用,它会忽略所有被引起来的字符的特殊处理,被引用起来的字符会被原封不动的使用:而双引号属于弱引用,它会对一些被引起来的字符进行特殊处理.简单来说,单引号直接输出内部字符串,不解析特 ...
- NC16850 [NOI1998]免费馅饼
题目链接 题目 题目描述 SERKOI最新推出了一种叫做"免费馅饼"的游戏:游戏在一个舞台上进行.舞台的宽度为W格,天幕的高度为H格,游戏者占一格.开始时游戏者站在舞台的正中央,手 ...
- idea 灵异事件之maven 缓存
方法一 mvn clean install 方法二 强制刷新maven 1 idea 右侧的maven 窗口: 方法三 强制刷新maven 2 右键项目: 上面虽然是重新导入Maven依赖,按理说,I ...
- GCC项目的文件组织和编译步骤分解
C项目的文件组织和编译 C项目的代码, 由头文件(.h后缀)和C文件(.c后缀)组成 C语言的函数和变量, 分声明和定义两个阶段 头文件和C文件是等价的, 相当于C文件的一部分, 其功能由人为划分, ...
- 使用spring boot jpa进行增删改查
项目地址:https://gitee.com/indexman/spring_boot_in_action 编写实体类User package com.laoxu.springboot.entity; ...
- Aop @AfterReturning因返回类型不一致导致无法执行切面代码
要做返回异常之后,所有操作回滚的操作,本来想着泛型用 Object 就表示所有返回类型是 CommonResult 并且加指定注解的都走这个通知的代码,但是如下配置,无论如何也不生效 进入源码里发现, ...