前言
我们知道,创建Java对象会涉及到对象初始化、内存分配、类加载等多个步骤。当对象比较重量级时,频繁创建对象会带来可观的性能开销,所以在上古时代(大雾)就产生了对象池化(object pooling)的技术。顾名思义,对象池中维护了一批已经创建好的重量级对象,要使用时就从中取出一个,用完就归还到池里,通过复用对象来提高效率。
我们在日常工作中时刻都在应用池化技术:线程池、数据库连接池、网络连接池等等。自行实现对象池要考虑的细节太多,所以Apache Commons Pool提供了可以开箱即用的通用对象池实现,Jedis、DBCP等我们耳熟能详的组件都充分利用了它。本文择重点分析一下Commons Pool 2的设计思路。
对象池化的三要素
Commons Pool 2把面向接口编程的理念发挥得淋漓尽致,其三要素都可以很容易地从接口规范中看出来。下面分别讨论之。
池化对象
顾名思义,PooledObject就是对象池中存放着的对象。它除了维护对象本身之外,还会持有一些与对象生命周期相关的属性,如当前的状态、创建时间、空闲时间、最近借用/归还时间等等。对象的实际分配与销毁、空闲检测、状态转换等也由PooledObject来实现。接口代码如下,比较容易理解。
public interface PooledObject extends Comparable> {
T getObject();
long getCreateTime();
long getActiveTimeMillis();
default long getBorrowedCount() {
return -1;
}
long getIdleTimeMillis();
long getLastBorrowTime();
long getLastReturnTime();
long getLastUsedTime();
@Override
int compareTo(PooledObject other);
@Override
boolean equals(Object obj);
@Override
int hashCode();
@Override
String toString();
boolean startEvictionTest();
boolean endEvictionTest(Deque> idleQueue);
boolean allocate();
boolean deallocate();
void invalidate();
void setLogAbandoned(boolean logAbandoned);
default void setRequireFullStackTrace(final boolean requireFullStackTrace) {
// noop
}
void use();
void printStackTrace(PrintWriter writer);
PooledObjectState getState();
void markAbandoned();
void markReturning();
}
Commons Pool 2提供了两种PooledObject的实现,一是默认的DefaultPooledObject,二是基于软引用的PooledSoftReference,如下图所示。
对象池
对象池的作用自然是管理PooledObject。客户端可以通过对应的方法来借用或者归还PooledObject,另外也可以向池中添加或者从池中销毁PooledObject。普通ObjectPool使用一个池子管理所有的对象,而另外一种KeyedObjectPool则使用key标记的不同池子管理所有的对象(但仍然要求对象的类型相同)。篇幅限制,本文只讨论ObjectPool,该接口的源码如下。
public interface ObjectPool extends Closeable {
void addObject() throws Exception, IllegalStateException,
UnsupportedOperationException;
default void addObjects(final int count) throws Exception {
for (int i = 0; i < count; i++) {
addObject();
}
}
T borrowObject() throws Exception, NoSuchElementException,
IllegalStateException;
void clear() throws Exception, UnsupportedOperationException;
@Override
void close();
int getNumActive();
int getNumIdle();
void invalidateObject(T obj) throws Exception;
void returnObject(T obj) throws Exception;
}
Commons Pool 2提供了3种ObjectPool的实现,分别是通用的GenericObjectPool、基于软引用的SoftReferenceObjectPool、基于动态代理的ProxiedObjectPool。本文接下来的分析主要关注GenericObjectPool。
池化对象工厂
是用于产生和控制池化对象的工厂类,是唯一一个需要完全由用户自行实现的组件。与对象池相对地,有负责普通ObjectPool的工厂PooledObjectFactory,以及负责KeyedObjectPool的KeyedPooledObjectFactory。PooledObjectFactory接口的源码如下。
public interface PooledObjectFactory {
PooledObject makeObject() throws Exception;
void destroyObject(PooledObject p) throws Exception;
boolean validateObject(PooledObject p);
void activateObject(PooledObject p) throws Exception;
void passivateObject(PooledObject p) throws Exception;
}
上面的5个方法分别用来创建、销毁、校验、(在借用时)激活和(在归还时)钝化对象。
池化对象状态机
PooledObject的状态由PooledObjectState枚举来定义,一共有10种状态,列举如下。
- IDLE:位于对象池的空闲队列中,未被使用。
- ALLOCATED:正在使用。
- EVICTION:位于空闲队列中,且正在做空闲检测,有可能会被驱逐。
- EVICTION_RETURN_TO_HEAD:对象正在做空闲检测的同时被借用,此状态是一个中间状态,当空闲检测结束后,应该插回空闲队列。
- VALIDATION:位于空闲队列中,且正在被校验。
- VALIDATION_PREALLOCATED、VALIDATION_RETURN_TO_HEAD:对象正在校验的同时被借用,此两个状态都是中间状态,当校验结束后,应该插回空闲队列。不过,前者是在配置了testOnBorrow时出现,后者是在配置了空闲检测时出现。
- INVALID:对象无效化(即没有通过空闲检测或者有效性校验),即将或已经被销毁。
- ABANDONED:对象被标记为弃用,将被无效化。
- RETURNING:对象使用完毕,正在被归还到池中。
根据默认实现DefaultPooledObject的逻辑(源码很简单,就不再贴了),可以画出其状态机如下图,每条边上都是DefaultPooledObject对应的方法。注意VALIDATION、VALIDATION_PREALLOCATED、VALIDATION_RETURN_TO_HEAD三个状态未被使用。
对象池参数简介
我们在之前使用Jedis、DBCP等连接池时,对对象池的相关参数应该有相当的了解,下面简要列举一些。
- maxTotal:池化对象的最大数量。
- maxIdle/minIdle:空闲对象的最大、最小数量。
- lifo:空闲对象队列的出入队方式,可配置为后进先出(LIFO)和先进先出(FIFO)。
- maxWaitMillis:借用对象时可以等待的最长时间。
- blockWhenExhausted:当池中对象耗尽后,借用对象的操作是否阻塞。
- testOnCreate/testOnBorrow/testOnReturn:创建/借用/归还对象时,是否校验对象的有效性。
- testWhileIdle:是否校验空闲对象的有效性。
- timeBetweenEvictionRunsMillis:空闲检测的周期。
- numTestsPerEvictionRun:每次运行空闲检测时,最多被检测的空闲对象数量。
- minEvictableIdleTimeMillis:空闲对象被回收掉的最小空闲时长。
在下文介绍对象借用和归还过程时,部分参数还会出现。
通用对象池GenericObjectPool实现要点
以下从对象的存储、创建、借用和归还四个方面作简要的分析。
对象存储
GenericObjectPool使用一个ConcurrentHashMap存储全部对象,保证线程安全性。
private final Map, PooledObject> allObjects = new ConcurrentHashMap<>();
注意IdentityWrapper只是简单地用System.identityHashCode()方法覆盖了默认的hashCode()实现,从而保证key的唯一性。
空闲队列则使用框架内自行实现的双端阻塞队列LinkedBlockingDeque。关于JDK中阻塞队列的经典实现,可以参见笔者之前写的关于LinkedBlockingQueue的文章。
private final LinkedBlockingDeque> idleObjects;
上文讲配置参数时已经说过,空闲队列是可以配置FIFO和LIFO两种出入队方式的,在队头和队尾都能插入元素,所以双端队列是必要的。
创建池化对象
create()方法比较简单,就是调用了PooledObjectFactory.makeObject()方法,将其加入ConcurrentHashMap中,并且保证总对象数不超过maxTotal的限制。
private PooledObject create() throws Exception {
int localMaxTotal = getMaxTotal();
long newCreateCount = createCount.incrementAndGet();
if (localMaxTotal > -1 && newCreateCount > localMaxTotal || newCreateCount > Integer.MAX_VALUE) {
createCount.decrementAndGet();
return null;
}
final PooledObject p;
try {
p = factory.makeObject();
} catch (Exception e) {
createCount.decrementAndGet();
throw e;
}
AbandonedConfig ac = this.abandonedConfig;
if (ac != null && ac.getLogAbandoned()) {
p.setLogAbandoned(true);
}
createdCount.incrementAndGet();
allObjects.put(p.getObject(), p);
return p;
}
借用对象
borrowObject()方法的完整源码如下。
public T borrowObject(long borrowMaxWaitMillis) throws Exception {
assertOpen();
AbandonedConfig ac = this.abandonedConfig;
if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
(getNumIdle() < 2) &&
(getNumActive() > getMaxTotal() - 3) ) {
removeAbandoned(ac);
}
PooledObject p = null;
// Get local copy of current config so it is consistent for entire
// method execution
boolean blockWhenExhausted = getBlockWhenExhausted();
boolean create;
long waitTime = System.currentTimeMillis();
while (p == null) {
create = false;
if (blockWhenExhausted) {
p = idleObjects.pollFirst();
if (p == null) {
p = create();
if (p != null) {
create = true;
}
}
if (p == null) {
if (borrowMaxWaitMillis < 0) {
p = idleObjects.takeFirst();
} else {
p = idleObjects.pollFirst(borrowMaxWaitMillis,
TimeUnit.MILLISECONDS);
}
}
if (p == null) {
throw new NoSuchElementException(
"Timeout waiting for idle object");
}
if (!p.allocate()) {
p = null;
}
} else {
p = idleObjects.pollFirst();
if (p == null) {
p = create();
if (p != null) {
create = true;
}
}
if (p == null) {
throw new NoSuchElementException("Pool exhausted");
}
if (!p.allocate()) {
p = null;
}
}
if (p != null) {
try {
factory.activateObject(p);
} catch (Exception e) {
try {
destroy(p);
} catch (Exception e1) {
// Ignore - activation failure is more important
}
p = null;
if (create) {
NoSuchElementException nsee = new NoSuchElementException(
"Unable to activate object");
nsee.initCause(e);
throw nsee;
}
}
if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
boolean validate = false;
Throwable validationThrowable = null;
try {
validate = factory.validateObject(p);
} catch (Throwable t) {
PoolUtils.checkRethrow(t);
validationThrowable = t;
}
if (!validate) {
try {
destroy(p);
destroyedByBorrowValidationCount.incrementAndGet();
} catch (Exception e) {
// Ignore - validation failure is more important
}
p = null;
if (create) {
NoSuchElementException nsee = new NoSuchElementException(
"Unable to validate object");
nsee.initCause(validationThrowable);
throw nsee;
}
}
}
}
}
updateStatsBorrow(p, System.currentTimeMillis() - waitTime);
return p.getObject();
}
代码很长,但是思路也非常清晰。简要叙述流程:
- 确认对象池是打开的,并根据AbandonedConfig的条件清理要被丢弃的对象(具体逻辑略去)。
- 如果blockWhenExhausted参数为true,就以阻塞的方式从空闲队列中获取对象,获取不到则创建。如果仍然无法得到对象,就根据maxWaitMillis的设定,再次以阻塞方式从空闲队列中获取对象,超时则抛出异常。
- 如果blockWhenExhausted参数为false,就以非阻塞的方式从空闲队列中获取对象,获取不到则创建。如果仍然无法得到对象,直接抛出异常。
- 得到可用的池化对象之后,将其激活。如果配置了testOnCreate或者testOnBorrow参数,则还要进行校验。激活或者校验不通过都会将对象销毁。
- 返回池化对象。
归还对象
returnObject()的完整源码如下。
public void returnObject(T obj) {
PooledObject p = allObjects.get(new IdentityWrapper(obj));
if (p == null) {
if (!isAbandonedConfig()) {
throw new IllegalStateException(
"Returned object not currently part of this pool");
} else {
return; // Object was abandoned and removed
}
}
synchronized(p) {
final PooledObjectState state = p.getState();
if (state != PooledObjectState.ALLOCATED) {
throw new IllegalStateException(
"Object has already been returned to this pool or is invalid");
} else {
p.markReturning(); // Keep from being marked abandoned
}
}
long activeTime = p.getActiveTimeMillis();
if (getTestOnReturn()) {
if (!factory.validateObject(p)) {
try {
destroy(p);
} catch (Exception e) {
swallowException(e);
}
try {
ensureIdle(1, false);
} catch (Exception e) {
swallowException(e);
}
updateStatsReturn(activeTime);
return;
}
}
try {
factory.passivateObject(p);
} catch (Exception e1) {
swallowException(e1);
try {
destroy(p);
} catch (Exception e) {
swallowException(e);
}
try {
ensureIdle(1, false);
} catch (Exception e) {
swallowException(e);
}
updateStatsReturn(activeTime);
return;
}
if (!p.deallocate()) {
throw new IllegalStateException(
"Object has already been returned to this pool or is invalid");
}
int maxIdleSave = getMaxIdle();
if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
try {
destroy(p);
} catch (Exception e) {
swallowException(e);
}
} else {
if (getLifo()) {
idleObjects.addFirst(p);
} else {
idleObjects.addLast(p);
}
if (isClosed()) {
// Pool closed while object was being added to idle objects.
// Make sure the returned object is destroyed rather than left
// in the idle object pool (which would effectively be a leak)
clear();
}
}
updateStatsReturn(activeTime);
}
简要叙述流程:
- 获取原始对象对应的池化对象实例。如果实例为空且配置了AbandonedConfig,说明已经被丢弃,无需再归还。
- 检查池化对象的状态,只有处于ALLOCATED状态才能被归还。
- 如果配置了testOnReturn参数,则校验对象的有效性,不能通过校验则直接销毁。通过校验之后,再钝化与解分配此对象。
- 检查当前空闲队列中的对象数量是否达到了maxIdle的阈值,若达到阈值,说明无法再归还,直接销毁。
- 根据空闲队列的LIFO/FIFO方式,将被归还的对象放到队列的头部或尾部。
The End
关于连接池的借用、归还和空闲检测,之前已经简单提到过了,参见MySQL连接的8小时问题。
民那晚安。