对象的创建和销毁在一定程度上会消耗系统的资源,虽然jvm的性能在近几年已经得到了很大的提高,对于多数对象来说,没有必要利用对象池技术来进行对象的创建和管理。但是对于有些对象来说,其创建的代价还是比较昂贵的,比如线程、tcp连接、rpc连接、数据库连接等对象,因此对象池技术还是有其存在的意义。
Apache-commons-pool-1.6提供的对象池主要有两种:一种是带Key的对象池,这种带Key的对象池是把相同的池对象放在同一个池中,也就是说有多少个key就有多少个池;另一种是不带Key的对象池,这种对象池是把生产完全一致的对象放在同一个池中,但是有时候,单用对池内所有对象一视同仁的对象池,并不能解决的问题。例如:对于一组某些参数设置不同的同类对象——比如一堆指向不同地址的 java.net.URL对象或者一批代表不同语句的java.sql.PreparedStatement对象,用这样的方法池化,就有可能取出不合用的对象。
1、对象池:
1)对象池接口介绍:
如果让我们去设计一个对象池接口,会给用户提供哪些核心的方法呢?
borrowObject(),returnObject()是两个核心方法,一个是’借’,一个是’还’。那么我们有可能需要对一个已经借到的对象置为失效(比如当我们的远程连接关闭或产生异常,这个连接不可用需要失效掉),invalidateObject()也是必不可少的。对象池刚刚创建的时候,我们可能需要预热一部分对象,而不是采用懒加载模式以避免系统启动时候的抖动,因此addObject()提供给用户,以进行对象池的预热。有创建就有销毁,clear()和close()就是用来清空对象池(觉得叫purge()可能更好一点)。除此之外,我们可能还需要一些简单的统计,比如getNumIdle()获得空闲对象个数和getNumActive()获得活动对象(被借出对象)的个数。如下表:
方法名
作用
borrowObject()
从池中借对象
returnObject()
还回池中
invalidateObject()
失效一个对象
addObject()
池中增加一个对象
clear()
清空对象池
close()
关闭对象池
getNumIdle()
获得空闲对象数量
getNumActive()
获得被借出对象数量 2)在commons-pool中有两类对象池接口(带key和不带key),一个是ObjectPool,另一个是KeyedObjectPool;此外,为了方便他们分别还对应了ObjectPoolFactory、KeyedObjectPoolFactory两个接口(这两个接口在功能上和他们都一样,只是使用形式上不一样)
3)对象池空间划分:
一个对象存储到对象池中,其位置不是一成不变的。空间的划分可以分为两种,一种是物理空间划分,一种是逻辑空间划分。不同的实现可能采用不同的技术手段,Commons Pool实际上采用了逻辑划分。如下图所示:
从整体上来讲,可以将空间分为池外空间和池内空间,池外空间是指被’出借’的对象所在的空间(逻辑空间)。池内空间进一步可以划分为idle空间,abandon空间和invalid空间。idle空间就是空闲对象所在的空间,空闲对象之间是有一定的组织结构的(详见后文)。abandon空间又被称作放逐空间,用于放逐被出借超时的对象。invalid空间其实就是对象的垃圾场,这些对象将不会在被使用,而是等待被gc处理掉。
4)对象池的放逐与驱逐:
下面我们会多次提到驱逐(eviction)和放逐(abandon),这两个概念是对象池设计的核心。
先来看驱逐,我们知道对象池的一个重要的特性就是伸缩性,所谓伸缩性是指对象池能够根据当前池中空闲对象的数量(maxIdle和minIdle配置)自动进行调整,进而避免内存的浪费。自动伸缩,这是驱逐所需要达到的目标,他是如何实现的呢?实际上在对象池内部,我们可以维护一个驱逐定时器(EvictionTimer),由timeBetweenEvictionRunsMillis参数对定时器的间隔加以控制,每次达到驱逐时间后,我们就选定一批对象(由numTestsPerEvictionRun参数进行控制)进行驱逐测试,这个测试可以采用策略模式,比如Commons Pool的DefaultEvictionPolicy,代码如下:
@Override
public boolean evict(EvictionConfig config, PooledObject underTest,
int idleCount) {
if ((config.getIdleSoftEvictTime() < underTest.getIdleTimeMillis() &&
config.getMinIdle() < idleCount) ||
config.getIdleEvictTime() < underTest.getIdleTimeMillis()) {
return true;
}
return false;
}对于符合驱逐条件的对象,将会被对象池无情的驱逐出空闲空间,并丢弃到invalid空间。之后对象池还需要保证内部空闲对象数量需要至少达到minIdle的控制要求。
我们在看来放逐,对象出借时间太长(由removeAbandonedTimeout控制),我们就把他们称作流浪对象,这些对象很有可能是那些用完不还的坏蛋们的杰作,也有可能是对象使用者出现了什么突发状况,比如网络连接超时时间设置长于放逐时间。总之,被放逐的对象是不允许再次回归到对象池中的,他们会被搁置到abandon空间,进而进入invalid空间再被gc掉以完成他们的使命。放逐由removeAbandoned()方法实现,分为标记过程和放逐过程,代码实现并不难,有兴趣的可以直接翻翻源代码。
驱逐是由内而外将对象驱逐出境,放逐则是由外而内,将对象流放。他们一内一外,正是整个对象池形成闭环的核心要素。
5)对象池的常见配置一览:
配置参数
意义
默认值
maxTotal
对象总数
8
maxIdle
最大空闲对象数
8
minIdle
最小空闲对象书
0
lifo
对象池借还是否采用lifo
true
fairness
对于借对象的线程阻塞恢复公平性
false
maxWaitMillis
借对象阻塞最大等待时间
-1
minEvictableIdleTimeMillis
最小驱逐空闲时间
30分钟
numTestsPerEvictionRun
每次驱逐数量
3
testOnCreate
创建后有效性测试
false
testOnBorrow
出借前有效性测试
false
testOnReturn
还回前有效性测试
false
testWhileIdle
空闲有效性测试
false
timeBetweenEvictionRunsMillis
驱逐定时器周期
false
blockWhenExhausted
对象池耗尽是否block
true
2、池化对象:
1)池化对象接口:(被池化的对象需要实现该接口)
池化对象就是对象池中所管理的基本单元。我们可以思考一下,如果直接将我们的原始对象放到对象池中是否可以?答案当然是可以,但是不好,因为如果那样做,我们的对象池就退化成了容器Collection了,之所以需要将原始对象wrapper成池对象,是因为我们需要提供额外的管理功能,比如生命周期管理。commons pool采用了PooledObject接口和KeyedPooledObject接口用于表达池对象,它主要抽象了池对象的状态管理和一些诸如状态变迁时所产生的统计指标,这些指标可以配合对象池做更精准的管理操作。
2)池化对象状态:
说到对池对象的管理,最重要的当属对状态的管理。对于状态管理,我们熟知的模型就是状态机模型了。池对象当然也有一套自己的状态机,我们先来看看commons pool所定义的池对象都有哪些状态:
状态
解释
IDLE
空闲状态
ALLOCATED
已出借状态
EVICTION
正在进行驱逐测试
EVICTION_RETURN_TO_HEAD
驱逐测试通过对象放回到头部
VALIDATION
空闲校验中
VALIDATION_PREALLOCATED
出借前校验中
VALIDATION_RETURN_TO_HEAD
校验通过后放回头部
INVALID
无效对象
ABANDONED
放逐中
RETURNING
换回对象池中 这里只需知道:放逐(
ABANDONED)指的是不在对象池中的对象超时流放;驱逐(
EVICTION)指的是空闲对象超时销毁;VALIDATION是有效性校验,主要校验空闲对象的有效性。注意与驱逐和放逐之间的区别。我们通过一张图来看看状态之间的变迁。
我们看到上图的’圆圈’表示的就是池对象,其中中间的英文简写是其对应的状态。虚线外框则表示瞬时状态。比如RETURNING和ABANDONED。这里我们省略了VALIDATION_RETURN_TO_HEAD,VALIDATION_PREALLOCATED,EVICTION_RETURN_TO_HEAD,因为这对于我们理解池对象状态变迁并没有太多帮助。针对上图,我们重点关注四个方面:
IDLE->ALLOCATED 即上图的borrow操作,除了需要将状态置为已分配,我们还需要考虑如果对象池耗尽了怎么办?是继续阻塞还是直接异常退出?如果阻塞是阻塞多久?
ALLOCATED->IDLE 即上图的return操作,我们需要考虑的是,如果池对象还回到对象池,此时对象池空闲数已经达到上界或该对象已经无效,我们是否需要进行特殊处理?
IDLE->EVICTION 与 ALLOCATED->ABANDONED 请参考后文
IDLE->VALIDATION 是testWhileIdle的有效性测试所需要经历的状态变迁,他是指每隔一段时间对池中所有的idle对象进行有效性检查,以排除那些已经失效的对象。失效的对象将会弃置到invalid空间。
3)池化对象生命周期控制:
只搞清楚了池化对象的状态和状态转移是不够的,我们还应该能够对池对象生命周期施加影响。Commons Pool通过PooledObjectFactory接口和KeyedPooledObjectFactory对对象生命周期进行控制。该接口有如下方法:
方法
解释
makeObject
创建对象
destroyObject
销毁对象
validateObject
校验对象
activateObject
重新初始化对象
passivateObject
反初始化对象 我们需要注意,池对象必须经过创建(makeObject())和初始化过程(activateObject())后才能够被我们使用。我们看一看这些方法能够影响哪些状态变迁。
参考:
http://kriszhang.com/object-pool/
对象池源码:https://github.com/apache/commons-pool
对象池wiki:https://commons.apache.org/proper/commons-pool/