什么是对象池技术?对象池应用在哪些地方?

对象池其实就是缓存一些对象从而避免大量创建同一个类型的对象,类似线程池的概念。对象池缓存了一些已经创建好的对象,避免需要时才创建对象,同时限制了实例的个数。池化技术最终要的就是重复的使用池内已经创建的对象。从上面的内容就可以看出对象池适用于以下几个场景:

  1. 创建对象的开销大
  2. 会创建大量的实例
  3. 限制一些资源的使用

如果创建一个对象的开销特别大,那么提前创建一些可以使用的并且缓存起来(池化技术就是重复使用对象,提前创建并缓存起来重复使用就是池化)可以降低创建对象时的开销。

会大量创建实例的场景,重复的使用对象可减少创建的对象数量,降低GC的压力(如果这些对象的生命周期都很短暂,那么可以降低YoungGC的频率;如果生命周期很长,那么可以避免掉这些对象被FullGC——生命周期长,且大量创建,这里就要结合系统的TPS等考虑池的大小了)。

对于限制资源的使用更多的是一种保护策略,比如数据库链接池。除去这些对象本身的开销外,他们对外部系统也会造成压力,比如大量创建链接对DB也是有压力的。那么池化除了优化资源以外,本身限制了资源数,对外部系统也起到了一层保护作用。

如何实现对象池?

开源实现:Apache Commons Pool

自己实现:Netty轻量级对象池实现

Apache Commons Pool开源软件库提供了一个对象池API和一系列对象池的实现,支持各种配置,比如活跃对象数或者闲置对象个数等。DBCP数据库连接池基于Apache Commons Pool实现。

Netty自己实现了一套轻量级的对象池。在Netty中,通常会有多个IO线程独立工作,基于NioEventLoop的实现,每个IO线程轮询单独的Selector实例来检索IO事件,并在IO来临时开始处理。最常见的IO操作就是读写,具体到NIO就是从内核缓冲区拷贝数据到用户缓冲区或者从用户缓冲区拷贝数据到内核缓冲区。这里会涉及到大量的创建和回收Buffer,Netty对Buffer进行了池化从而降低系统开销。

Netty对象池实现分析

上面提到了IO操作中会涉及到大量的缓冲区操作,NIO提供了两种Buffer最为缓冲区:DirectByteBuffer和HeapByteBuffer。Netty在两种缓冲区的基础上进行了池化进而提升性能。

DirectByteBuffer

DirectByteBuffer顾名思义是直接内存(Direct Memory)上的Byte缓存区,直接内存不是JVM Runtime数据区域的一部分,也不是Java虚拟机规范中定义的内存区域。简单的说这部分就是机器内存,分配的大小等都和虚拟机限制无关。JDK1.4中开始我们可以使用native方法在直接内存上来分配内存,并在JVM堆内存上维持一个引用来进行访问,当JVM堆内存上的引用被回收后,这块内存被操作系统回收。

HeapByteBuffer

HeapByteBuffer是在JVM堆内存上分配的Byte缓冲区,可以简单的理解为byte[]数组的一种封装。基于HeapByteBuffer的写流程通常要先在直接内存上分配一个临时的缓冲区,将数据从Heap拷贝到直接内存,然后再将直接内存的数据发送到IO设备的缓冲区,之后回收直接内存。读流程也类似。使用DirectByteBuffer避免了不必要的拷贝工作,所以在性能上会有提升。

DirectByteBuffer的缺点在于分配和回收的的代价相对较大,因此DirectByteBuffer适用于缓冲区可以重复使用的场景。

Netty的池化实现

以Buffer为例,对应直接内存和堆内存,Netty的池化分别为PooledDirectByteBuffer和PolledHeapByteBuffer。

通过PooledDirectByteBuffer的API定义可以看到,它的构造方法是私有的,而创建一个实例的入口是:

  static PooledDirectByteBuf newInstance(int maxCapacity) {

         PooledDirectByteBuf buf = RECYCLER.get();

         buf.reuse(maxCapacity);

         return buf;

     } 

可见RECYCLER是池化的核心,创建对象时都通过RECYCLER.get来获得一个实例(Recycler就是Netty实轻量级池化技术的核心)。

Recycler实现分析(源码分析)

 /**

  * 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实现了轻量级的对象池。

Recycler的API非常简单:

get():获取一个实例

recycle(T, Handle<T>):回收一个实例

newObject(Handle<T>):创建一个实例

get流程

     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的简化流程(这里先不深究细节):

  1. 拿到当前线程对应的stack
  2. 从stack中pop出一个元素
  3. 如果不为空则返回,否则创建一个新的实例

可以大概明白Stack是对象池化背后存储实例的数据结构:如果能从stack中拿到可用的实例就不再创建新的实例。

recycle流程

一个“池子”最核心的就是做两件事情,第一个是上面的Get,即从池子中拿出一个可用的实例。另一个就是在用完后将数据放回到池子中(线程池、连接池都是这样)。

 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方法主要做了一些参数验证。

DefaultHandle的recycle方法流程如下:

  1. 如果当前线程是当前stack对象的线程,那么将实例放入stack中,否则:
  2. 获取当前线程对应的Map<Stack, WeakOrderQueue>,并将实例加入到Stack对应的Queue中。

从获取实例和回收实例的代码可以看出,整个对象池的核心实现由ThreadLocal和Stack及WrakOrderQueue构成,接着来看Stack和WrakOrderQueue的具体实现,最后概括整体实现。

Stack实体

Stack<T>

  parent:Recycler                                    // 关联对应的Recycler

  thread:Thread                                      // 对应的Thread

  elements:DefaultHandle<?>[]           // 存储DefaultHandle的数组

  head:WeakOrderQueue                     // 指向WeakOrderQueue元素组成的链表的头部“指针”

  cursor,prev:WrakOrderQueue          // 当前游标和前一元素的“指针”

pop实现

 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;

     }
  1. 如果size为0(这里的size表示stack中可用的元素),尝试进行scavenge。
  2. 返回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实现

 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流程要更加简单,直接将回收的元素放到队尾(实际是一个数组)。

WeakOrderQueue实体

WeakOrderQueue

  head,tail:Link                       // 内部元素的指针(WeakOrderQueue内部存储的是一个Link的链表)

  next:WeakOrderQueue     // 指向下一个WeakOrderQueue的指针

  owner:Thread                      // 对应的线程

WeakOrderQueue核心包含两个方法,add方法将元素添加到自身的“队列”中,transfer方法将自己拥有的元素“传输”到Stack中。

Linke结构如下

 private static final class Link extends AtomicInteger {
private final DefaultHandle<?>[] elements = new DefaultHandle[LINK_CAPACITY]; private int readIndex;
private Link next;
}

Link内部包含了一个数组用于存放实例,同时标记了读取位置的索引和下一个Link元素的指针。

结合Link的结构,Weak的结构如下:

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已满则创建一个新的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方法收件根据stack的容量和自身拥有的实例数,计算出最终需要转移的实例数。之后就是数组的拷贝和指标的调整。

基本上所有的流程有个大致的了解,下面从整体的角度回顾一下Netty对象池的实现。

整体实现

结构

整个设计上核心的几点:

  1. Stack相当于是一级缓存,同一个线程内的使用和回收都将使用一个Stack
  2. 每个线程都会有一个自己对应的Stack,如果回收的线程不是Stack的线程,将元素放入到Queue中
  3. 所有的Queue组合成一个链表,Stack可以从这些链表中回收元素(实现了多线程之间共享回收的实例)

引用自网上的一幅图。

Netty轻量级对象池实现分析的更多相关文章

  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中,通常会有多个IO线程独立工作,基于NioEventLoop的实现,每个IO线程负责轮询单独的Selector实例来检索IO事件,当IO事件来临的时候,IO线程开始处理IO事件. ...

  4. Netty 高性能之道 - Recycler 对象池的复用

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

  5. Java 中的对象池实现

    点赞再看,动力无限.Hello world : ) 微信搜「程序猿阿朗 」. 本文 Github.com/niumoo/JavaNotes 和 未读代码博客 已经收录,有很多知识点和系列文章. 最近在 ...

  6. 7. SOFAJRaft源码分析—如何实现一个轻量级的对象池?

    前言 我在看SOFAJRaft的源码的时候看到了使用了对象池的技术,看了一下感觉要吃透的话还是要新开一篇文章来讲,内容也比较充实,大家也可以学到之后运用到实际的项目中去. 这里我使用Recyclabl ...

  7. Netty源码解析 -- 对象池Recycler实现原理

    由于在Java中创建一个实例的消耗不小,很多框架为了提高性能都使用对象池,Netty也不例外. 本文主要分析Netty对象池Recycler的实现原理. 源码分析基于Netty 4.1.52 缓存对象 ...

  8. 基于Apache组件,分析对象池原理

    池塘里养:Object: 一.设计与原理 1.基础案例 首先看一个基于common-pool2对象池组件的应用案例,主要有工厂类.对象池.对象三个核心角色,以及池化对象的使用流程: import or ...

  9. Apache Common-pool2对象池分析和应用

    Apache Common-pool2包提供了一个通用的对象池技术的实现.可以很方便的基于它来实现自己的对象池,比如DBCP和Jedis他们的内部对象池的实现就是依赖于Common-pool2. 对象 ...

随机推荐

  1. PHP 10 : 流程控制

    原文:PHP 10 : 流程控制 感觉PHP和其他语言相似.说说PHP提供的流程控制关键字吧. 条件 ifelseelseifswitch 循环 whiledo{} while()breakconti ...

  2. jquery 超简单的点赞效果

    1.HTML(可以优化一下,尽量少些几个标签.....) <div id="dianz"> <b class="cz"><em&g ...

  3. SQL远程备份

    原文:SQL远程备份 set ANSI_NULLS ON set QUOTED_IDENTIFIER ON go   -- ====================================== ...

  4. mysql查询字段值为数字

    原文:mysql查询字段值为数字 我想查询字段值为数字的sql如下:select * from tj_item_result where tj_value REGEXP '^[0-9]'

  5. 转: js快速分享代码

    这是一款简单易用的文章分享工具,您只需将下面的html代码拷贝到模板中就可以实现文章快速分享功能.如果您想分享你的博客.个人网站或者企业网站等等,下面是两款不错的分享工具,值得拥有! 1. [html ...

  6. div高度自适外层div高度随里层div高度自适

    尝试过许多办法 其中一网友的最靠谱就是在外层div样式添加两个标签(不能少) clear:both;  overflow:auto;

  7. Javascript模块化编程之Why

    说到模块化编程,大家比较容易想到Java, C++等语言,感觉这事和Javascript沾不上一丁点边.虽说Javascript看上去好像同Java有莫大的关系,但那也只是一厢情愿,不过是挂羊头卖狗肉 ...

  8. AutoPostBack通过现象看本质

    在做人事档案管理系统时遇到一个功能需要实现前台数据(实时)与后台进行交互,解决这个问题首先想到的是应用控件的AutoPostBack属性.本以为这个问题就这样解决了(不用javascript.jque ...

  9. ORACLE总结系列1--network文件夹里的admin的三个文件信息

    sqlnet.ora 作用类似于linux或者其他unix的 nsswitch.conf文件,通过这个文件来决定怎么样找一个连接中出现的连接字符串(connect descriptor) 假如sqln ...

  10. WinForm中使MessageBox实现可以自动关闭功能

    WinForm 下我们可以调用MessageBox.Show 来显示一个消息对话框,提示用户确认等操作.在有些应用中我们需要通过程序来自动关闭这个消息对话框而不是由用户点击确认按钮来关闭.然而.Net ...