对象池化的技术的出现都是能够说是不得以而为之,若是咱们有足够快的CPU,足够大的内存,那么对象池化的技术是彻底不必,各类垃圾回收也是不必的;但凡事总有个可是,资源老是有限的,如何在有限资源下发挥出最优效果,也是自人类诞生以来一直在探索的问题。javascript
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];
}
复制代码
若是你是手头比较紧的房产投资人,考虑一下如何投资房产才能使效益最大化?一般来讲有一下几种选择
同理,对于计算机来讲内存和CPU都是珍贵的资源,若是你一开始建立了大量的对象,那么将占据大量的资源,而且颇有可能这些对象一个都不回被复用而且还会使你的内存溢出,服务崩溃。(固然,若是你服务器内存足够大,当我没说)
所以,咱们并能够回收那些再也不须要用到对象,并保存到咱们对象池中,所以要被回收的对象须要有恢复到最初使的状态。(租客再也不续租房子了,咱们须要对房子进行清理一遍租给其余客户)
此外咱们还能够对对象进一步细化进行分类,以知足不一样类型的需求(如单身客户通常都租单人间,有老婆孩子都会租大一点的)
那么对象池化技术在Jetty中都使怎么应用的呢?
咱们知道,不论使用NIO或者BIO都须要提供一个缓冲区以供读写数据,而且这些缓冲区会被频繁的使用到,所以Jetty为缓冲区设计了一个对象池ByteBufferPool
ArrayByteBufferPool是ByteBufferPool的一个实现
默认状况下ArrayByteBufferPool的结构以下图所示
如上图所示Bucket使用线性数组来保存,每一个Bucket
装的都是不一样大小的ByteBuffer
缓冲区,以适应不一样缓冲区大小需求。默认的有64个Bucket
,ByteBuffer
的基础大小称为Factor
在此图中factor
的大小为1024。
为何要对缓冲区大小进行分类?缘由很简单,充分利用资源(你让一个单身汉去租三居室,这不害人吗,有钱的话,当我没说)
而且我使用Fiddler简单统计了一下访问掘金首页过程当中常见的数据包大小。
100b
至 400b
之间(说明若是咱们使用jetty的话仍是有优化空间的,如将factor
调整为512以节省空间)100b
至 1000kb
之间,最大的主要是静态资源(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
的设计思想倍感亲切,都是分而治之
的思想的最好实践。