对象池化的艺术

概述

对象池化的技术的出现都是能够说是不得以而为之,若是咱们有足够快的CPU,足够大的内存,那么对象池化的技术是彻底不必,各类垃圾回收也是不必的;但凡事总有个可是,资源老是有限的,如何在有限资源下发挥出最优效果,也是自人类诞生以来一直在探索的问题。javascript

Tomcat-高效的环形队列

Tomcat是在Java技术体系中经常使用的Web容器,其采用的NIO(非阻塞I/O)模型相较于传统的BIO(阻塞I/O)来讲得到了更高的性能。其NIO模型以下图所示。 css

Acceptor用于阻塞的接收链接,在接收到链接以后选择一个Poller来执行后续I/O任务处理。(Poller数量是固定的)html

若是是你,你会如何实现这个选择Poller的过程呢?不妨先考虑一下java

在Tomcat中会将Poller保存在一个环形队列中,并经过一个原子变量来循环获取队列中的下一个元素, 以下图所示。后端

图片来自于点击访问,本身画的实在太丑了数组

环形队列在物理意义上是以线性数组(链表亦可)的方式进行保存的,并不是是真的是圆形的方式存在在内存中。浏览器

咱们可使用javascript来快速体验一下环形数组.缓存

let pollers = [1,2,3,4,5,6]
let index = 0
let getNext = function(){
    return pollers[Math.abs(index++) % pollers.length]
}
for(let i = 0 ; i < 10086 ; i++){
    console.log(getNext())
}
复制代码

能够看出环形队列的实现就在于取余操做能够将咱们的索引index给限制pollers.length范围内,使得咱们永远能够取到队列中下一个元素,若是队列被取完了,则会回到队列的头部从新开始遍历。安全

仅仅如此吗?在JS中你这样子玩彻底没问题,由于浏览器的JavaScript是单线程的执行不会遇到并发问题,但做为一名后端程序猿,并发以及线程安全是你必须考虑到。bash

分析代码能够发现,咱们须要维护一个索引index来标志当前所在的位置,所以若是咱们将index用原子类保存,这样就不会遇到线程安全问题也不用加锁。

所以Poller对象池的实现其实挺简单的,以下代码所示,pollerRotater是一个原子类,能够保证咱们无锁,且线程安全的获取下一个索引(原子类的相关介绍,能够看这位老哥的文章)

public Poller getPoller0() {
        int idx = Math.abs(pollerRotater.incrementAndGet()) % pollers.length;
        return pollers[idx];
    }
复制代码

Jetty-精打细算的房产投资者

若是你是手头比较紧的房产投资人,考虑一下如何投资房产才能使效益最大化?一般来讲有一下几种选择

  • 高位接盘,借遍了亲戚朋友顺便掏空了六个钱包,结果血本无归
  • 买二手房,你接手了各类类型房子并改形成了各类类型的出租房(单身公寓、两居、三居室等等)租给客户,因而你每一年都有了固定的收入(和城中村的二房东聊过,一年几十万是有的)

同理,对于计算机来讲内存和CPU都是珍贵的资源,若是你一开始建立了大量的对象,那么将占据大量的资源,而且颇有可能这些对象一个都不回被复用而且还会使你的内存溢出,服务崩溃。(固然,若是你服务器内存足够大,当我没说)

所以,咱们并能够回收那些再也不须要用到对象,并保存到咱们对象池中,所以要被回收的对象须要有恢复到最初使的状态。(租客再也不续租房子了,咱们须要对房子进行清理一遍租给其余客户)

此外咱们还能够对对象进一步细化进行分类,以知足不一样类型的需求(如单身客户通常都租单人间,有老婆孩子都会租大一点的)

那么对象池化技术在Jetty中都使怎么应用的呢?

咱们知道,不论使用NIO或者BIO都须要提供一个缓冲区以供读写数据,而且这些缓冲区会被频繁的使用到,所以Jetty为缓冲区设计了一个对象池ByteBufferPool

ArrayByteBufferPool

ArrayByteBufferPool是ByteBufferPool的一个实现

默认状况下ArrayByteBufferPool的结构以下图所示

如上图所示Bucket使用线性数组来保存,每一个Bucket装的都是不一样大小的ByteBuffer缓冲区,以适应不一样缓冲区大小需求。默认的有64个BucketByteBuffer的基础大小称为Factor在此图中factor的大小为1024。

为何要对缓冲区大小进行分类?缘由很简单,充分利用资源(你让一个单身汉去租三居室,这不害人吗,有钱的话,当我没说)

而且我使用Fiddler简单统计了一下访问掘金首页过程当中常见的数据包大小。

  • 请求的数据包大都在 100b400b 之间(说明若是咱们使用jetty的话仍是有优化空间的,如将factor调整为512以节省空间)
  • 响应的数据报在100b1000kb之间,最大的主要是静态资源(js、css)等彻底能够放在CDN上来减轻Web容器的压力

如你所看到,对ByteBuffer按大小进行分类可让咱们充分利用资源,而且经过调整factor参数来减小内存的占用来实现进一步的优化。

注意 实际保存ByteBuffer的是ConcurrentLinkedDeque,由于名字太长因此用其接口来表示

那么,如何根据缓冲区呢大小获取相应的Bucket,使用如下公式便可
Bucket索引=(目标缓冲区大小 - 1 ) / factor

在本例中,factor是1024, 若是要想要一个10086大小的缓冲区应有
(10086 - 1)/1024 = 10 即Bucekt得数组索引为10,是Bucekt数组中的第十一个元素其ByteBuffer的大小为1024*11

至于为何要将缓冲区大小减一,相信你稍微思考一下便知晓

值得注意是,ArrayByteBufferPool并不会在一开始就当即为全部Bucket分配ByteBufferPool。而是在须要使用的时候先判断有没有目标大小的ByteBuffer,若是有则从相应的Bucekt中取一个返回给调用方,若是没有则新建一个 。在不须要使用的时候由调用方主动归还给ArrayByteBufferPool

除此以外, 还能够为ArrayByteBufferPool指定最大内存(避免耗尽内存形成内存溢出),当缓存的ByteBuffer的大小总和超过这个值的时候会执行清理工做,将旧的Bucekt清除掉。

有兴趣的能够阅读相应类的源码

org.eclipse.jetty.io.ArrayByteBufferPool
复制代码

总结

分而治之能够说是人类解决问题基本方法论。若是你了解ConcurrentHashMap的分段锁,那么你就应该会对Jetty的ByteBufferPool的设计思想倍感亲切,都是分而治之的思想的最好实践。

相关文章
相关标签/搜索