Apache Commons Pool 2.13.1 配置参数详解

系统梳理 Apache Commons Pool 2.13.1 全部池化配置参数:以探针程序实测默认值为基线,逐项验证 maxTotal/maxIdle/maxWait、testOnBorrow 等检测开关、驱逐参数、LIFO 借出顺序、JMX 与键控池参数的真实行为。

Apache Commons PoolApache Commons Pool2026/10/15

Apache Commons Pool 2.13.1 配置参数详解

目标:摸清 Apache Commons Pool 2.13.1 的全部池化参数——先用探针程序实测出真实默认值,再逐项验证 maxTotal/maxIdle/maxWait、检测开关、驱逐参数、LIFO 借出顺序、JMX 与键控池参数的行为差异;本文全部回显均已在 Ubuntu 22.04 实测通过。

Commons Pool 没有配置文件,所有行为都由「构造池时传入的 Config 对象」决定,默认值藏在 GenericObjectPoolConfig 的常量里。看懂参数的前提是拿到真实默认值,然后对每一类参数做行为级验证。本文环境为 Ubuntu 22.04.5 LTS + OpenJDK 17.0.20.1 + commons-pool2 2.13.1(坐标 org.apache.commons:commons-pool2),工作目录 /root/pool-exp(编译产物在 /root/pool-exp/out,实验源码与 commons-pool2-2.13.1.jar 同目录)。

一、环境信息

本节固定可复现基线。参数实测的全部程序基于同一个对象工厂(模拟昂贵对象)编译运行。

  • 操作系统:Ubuntu 22.04.5 LTS
  • 架构:x86_64
  • JDK:OpenJDK 17.0.20.1(apt 包 openjdk-17-jdk)
  • 构建方式:javac 直编 + java 直跑(不依赖构建工具)
  • 组件版本:commons-pool2 2.13.1
  • Maven 坐标:org.apache.commons:commons-pool2:2.13.1
  • 工作目录:/root/pool-exp(实验源码 + commons-pool2-2.13.1.jar + out/ 编译产物)

编译命令(一次编译全部实验源码):

cd /root/pool-exp && javac -encoding UTF-8 -cp commons-pool2-2.13.1.jar -d out Exp1Core.java Exp3Keyed.java Exp4Eviction.java Exp5Jmx.java Exp6TestFlags.java Exp8LifoFifo.java Exp9Misc.java ConfigProbe.java
Note: Some input files use or override a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

要点:出现 deprecated 提示是正常现象——实验中调用了毫秒版本的驱逐 API(5.4 节有专段说明),不影响编译与运行结果。

二、前置条件

本节确认阅读与复现本文的前提。参数行为验证依赖「能跑起来」的池程序,因此先确认 jar 与 JDK 就位。

  1. 已安装 JDK 8 及以上(2.13.1 的 JDK 基线,官方 RELEASE-NOTES 声明 2.7.x+ 需 Java 8+),本文用 JDK 17。
  2. 已获得 commons-pool2-2.13.1.jar(Maven 依赖或官方 bin 包解压均可,获取方式见安装指南)。
  3. 实验程序在 /root/pool-exp 下可用 java -cp out:commons-pool2-2.13.1.jar 直接运行。

确认 jar 与编译产物就位(实时回显):

ls -l /root/pool-exp/commons-pool2-2.13.1.jar /root/pool-exp/out/ | head -12
-rw-r--r-- 1 root root 156457 Dec 30  2025 /root/pool-exp/commons-pool2-2.13.1.jar

/root/pool-exp/out/:
总用量 76
drwxr-xr-x 2 root root  4096 Oct 15 01:48 .
drwxr-xr-x 7 root root  4096 Oct 15 01:46 ..
-rw-r--r-- 1 root root  1627 Oct 15 01:48 ConfigProbe.class
-rw-r--r-- 1 root root  2343 Oct 15 01:48 Exp1Core$Conn.class
-rw-r--r-- 1 root root  1119 Oct 15 01:48 Exp1Core$ConnFactory.class
-rw-r--r-- 1 root root  4541 Oct 15 01:48 Exp1Core.class

三、参数查看与修改方式

本节说明 Commons Pool 的参数体系:它没有配置文件,参数全部通过代码通道设置;要「看」参数,最快的方式是跑一个探针程序。

3.1 没有配置文件:三条参数通道

通道写法生效时机适用场景
Config 对象new GenericObjectPoolConfig<>() 后 set 各项,构造时传入池构造时绝大多数场景(推荐)
池实例 setterpool.setMaxTotal(16) 等直接调用运行中即时生效动态扩缩容、运维干预
JMX MBean通过 jmxEnabled 注册的 MBean 修改属性运行中即时生效不重启进程的线上调整

三条通道操作的是同一批配置项,本文以 Config 对象为主线逐项验证。

3.2 用探针程序查看真实默认值

探针程序 ConfigProbe.java(文件路径 /root/pool-exp/ConfigProbe.java)直接读取 Config 对象的每个 getter,输出 2.13.1 的出厂默认值:

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
import org.apache.commons.pool2.impl.BaseObjectPoolConfig;

/* 探针:打印 commons-pool2 2.13.1 各配置项真实默认值 */
public class ConfigProbe {
    public static void main(String[] args) {
        GenericObjectPoolConfig<Object> c = new GenericObjectPoolConfig<>();
        System.out.println("== GenericObjectPoolConfig 默认值 ==");
        System.out.println("maxTotal=" + c.getMaxTotal());
        System.out.println("maxIdle=" + c.getMaxIdle());
        System.out.println("minIdle=" + c.getMinIdle());
        System.out.println("maxWaitMillis=" + c.getMaxWaitMillis());
        System.out.println("blockWhenExhausted=" + c.getBlockWhenExhausted());
        System.out.println("fairness=" + c.getFairness());
        System.out.println("lifo=" + c.getLifo());
        System.out.println("testOnCreate=" + c.getTestOnCreate());
        System.out.println("testOnBorrow=" + c.getTestOnBorrow());
        System.out.println("testOnReturn=" + c.getTestOnReturn());
        System.out.println("testWhileIdle=" + c.getTestWhileIdle());
        System.out.println("timeBetweenEvictionRunsMillis=" + c.getTimeBetweenEvictionRunsMillis());
        System.out.println("minEvictableIdleTimeMillis=" + c.getMinEvictableIdleTimeMillis());
        System.out.println("softMinEvictableIdleTimeMillis=" + c.getSoftMinEvictableIdleTimeMillis());
        System.out.println("numTestsPerEvictionRun=" + c.getNumTestsPerEvictionRun());
        System.out.println("evictionPolicyClassName=" + c.getEvictionPolicyClassName());
        System.out.println("jmxEnabled=" + c.getJmxEnabled());
        System.out.println("jmxNamePrefix=" + c.getJmxNamePrefix());
        System.out.println("jmxNameBase=" + c.getJmxNameBase());

        GenericKeyedObjectPoolConfig<Object> k = new GenericKeyedObjectPoolConfig<>();
        System.out.println("== GenericKeyedObjectPoolConfig 默认值 ==");
        System.out.println("maxTotalPerKey=" + k.getMaxTotalPerKey());
        System.out.println("maxIdlePerKey=" + k.getMaxIdlePerKey());
        System.out.println("minIdlePerKey=" + k.getMinIdlePerKey());
        System.out.println("maxTotal(全部键合计)=" + k.getMaxTotal());

        System.out.println("== BaseObjectPoolConfig 常量 ==");
        System.out.println("DEFAULT_MAX_WAIT_MILLIS=" + BaseObjectPoolConfig.DEFAULT_MAX_WAIT_MILLIS);
        System.out.println("DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS=" + BaseObjectPoolConfig.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS);
        System.out.println("DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS=" + BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS);
        System.out.println("DEFAULT_NUM_TESTS_PER_EVICTION_RUN=" + BaseObjectPoolConfig.DEFAULT_NUM_TESTS_PER_EVICTION_RUN);
    }
}

运行(实时回显):

java -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -cp out:commons-pool2-2.13.1.jar ConfigProbe
== GenericObjectPoolConfig 默认值 ==
maxTotal=8
maxIdle=8
minIdle=0
maxWaitMillis=-1
blockWhenExhausted=true
fairness=false
lifo=true
testOnCreate=false
testOnBorrow=false
testOnReturn=false
testWhileIdle=false
timeBetweenEvictionRunsMillis=-1
minEvictableIdleTimeMillis=1800000
softMinEvictableIdleTimeMillis=-1
numTestsPerEvictionRun=3
evictionPolicyClassName=org.apache.commons.pool2.impl.DefaultEvictionPolicy
jmxEnabled=true
jmxNamePrefix=pool
jmxNameBase=null
== GenericKeyedObjectPoolConfig 默认值 ==
maxTotalPerKey=8
maxIdlePerKey=8
minIdlePerKey=0
maxTotal(全部键合计)=-1
== BaseObjectPoolConfig 常量 ==
DEFAULT_MAX_WAIT_MILLIS=-1
DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS=-1
DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS=1800000
DEFAULT_NUM_TESTS_PER_EVICTION_RUN=3

💡 技术拓展:确认库默认值的三条路径对比——

路径优点缺点
读源码常量(BaseObjectPoolConfig.DEFAULT_*)权威只反映定义,不反映实例当前值
探针程序读 getter(本文所用)实测、可脚本化、能覆盖运行时 setter 修改需要写几行代码
JMX 控制台读 MBean 属性可看运行中进程需要接 JMX 端口与工具

本文采用 探针程序读 getter,理由是它同时验证了「默认值」与「getter 可读性」,且回显可逐字复现。

四、参数总览(含实测默认值)

本节把 3.2 节探针回显整理成总表,作为后续分项详解的索引。「风险」列标注改动该参数时最容易出的事故。

参数默认值(实测)作用改动风险
maxTotal8池内对象总数上限(在用+空闲)过小→借出排队/超时;过大→下游资源耗尽
maxIdle8空闲对象上限,超出即销毁远小于 maxTotal 会让突发流量反复创建
minIdle0空闲对象保底数量(配合驱逐线程)配 0 时冷启动与驱逐后无热对象
maxWaitMillis-1(无限等待)池空时 borrowObject 的最长等待-1 配合漏还对象会永久阻塞业务线程
blockWhenExhaustedtrue池满时是否阻塞等待false 时池满立即抛 Pool exhausted
fairnessfalse等待线程是否按 FIFO 公平发放true 有额外锁开销
lifotrue空闲队列借出顺序:后进先出/先进先出false(FIFO)利于对象轮转冷却
testOnCreatefalse创建后是否校验开启增加创建延迟
testOnBorrowfalse借出前是否校验开启最安全但每次借出都有校验开销
testOnReturnfalse归还时是否校验开启把坏对象拦截在归还点
testWhileIdlefalse空闲巡检时是否校验需配合驱逐线程周期
timeBetweenEvictionRunsMillis-1(不巡检)驱逐线程巡检周期-1 表示驱逐线程完全不运行
minEvictableIdleTimeMillis1800000(30 分钟)空闲多久后可被驱逐配合巡检周期决定实际驱逐时机
softMinEvictableIdleTimeMillis-1保底 minIdle 的软驱逐阈值-1 表示不启用软驱逐
numTestsPerEvictionRun3每轮巡检最多检查的空闲对象数过大拖慢单轮巡检
evictionPolicyClassNameDefaultEvictionPolicy驱逐判定策略类自定义类必须是 public 且有无参构造
jmxEnabledtrue是否注册 JMX MBean多池同名会注册冲突
jmxNamePrefixpoolMBean ObjectName 的 name 前缀多池务必区分前缀
jmxNameBasenullMBean ObjectName 的自定义基底null 时用默认结构
maxTotalPerKey(Keyed)8每个键的对象数上限过小→单键借出排队
maxIdlePerKey(Keyed)8每个键的空闲上限超出即销毁该键的多余对象
minIdlePerKey(Keyed)0每个键的空闲保底配合巡检周期预热

五、核心参数分类详解

本节对五类核心参数逐项做行为级验证。每项先给参数语义,再给实测回显,最后给验证结论。

5.1 容量参数:maxTotal / maxIdle / minIdle

maxTotal 是池的天花板;maxIdle 决定归还后允许多少对象留在空闲队列(超出即销毁);minIdle 是保底值。maxIdle 的收敛行为实测(程序借 3 个 maxIdle=1 的池再一次性归还,实时回显):

== [4] maxIdle=1 收敛 (maxTotal=3) ==
借到 id: 1, 2, 3,一次性全部归还:
  还 2 个后 numIdle=1 (上限 1,超出即销毁)
  还 3 个后 numIdle=1, destroyedCount=2, createdCount=3

验证结果:归还第 2 个对象时 numIdle 立即被压到上限 1(多余对象当场销毁);全部归还后 destroyedCount=2(3 个对象中 2 个被销毁,只留 1 个热对象)。要点:maxIdle 不是「最多借出」而是「最多保留」,把它设得远小于 maxTotal 意味着突发流量过后池子会被拆掉重建,第二次流量高峰要重新付创建成本;实践上 maxIdle 通常等于或接近 maxTotal。

5.2 等待参数:maxWait 与 blockWhenExhausted

池空时的行为由 blockWhenExhausted(默认 true,阻塞)与 maxWaitMillis(默认 -1,无限等)共同决定。三组实测(实时回显):

== blockWhenExhausted=true, maxWait=300ms,池被占满后借用 ==
已占用对象 id=1,再次借用(等待上限 300ms)...
捕获 NoSuchElementException, 实际等待=300ms
异常消息: Timeout waiting for idle object, maxWaitDuration=PT0.29994199S

== blockWhenExhausted=true, maxWait=-1(无限等待),800ms 后另一线程归还 ==
等待 800ms 后借到 id=1(复用归还的同一对象)

== blockWhenExhausted=false,池被占满后借用 ==
捕获 NoSuchElementException, 耗时 0ms(未等待)
异常消息: Pool exhausted

💡 技术拓展:池满时的两种失败模型对比——

模型配置组合业务表现适用场景
阻塞等待(默认)blockWhenExhausted=true + maxWaitMillis=N请求挂起 N 毫秒后超时异常削峰,接受排队延迟
无限等待blockWhenExhausted=true + maxWaitMillis=-1挂起直到有对象归还绝不允许失败、且保证必然归还的场景
快速失败blockWhenExhausted=false立即抛 Pool exhausted对延迟极敏感、上游可重试的场景

本文实验覆盖 三种全部,验证结论:默认组合(true + -1)最危险——只要业务漏还一个对象,等待线程就会永久阻塞,生产必须显式设置有限的 maxWait。

5.3 检测开关:testOnCreate / testOnBorrow / testOnReturn / testWhileIdle

四个开关决定 validateObject 钩子在四个时机是否被调用,默认全部关闭。用「空闲对象损坏」场景实测三个常用开关(程序先借出对象、置坏标记、归还,再观察后续借还行为,实时回显):

== 场景 A: testOnBorrow=true,空闲坏对象借出前被拦截 ==
空闲中的 Session-1 已被置为 broken,继续借出:
  [destroyObject] Session-1 被销毁
借到 Session-2(不是损坏的 Session-1=true,validate 调用 3 次)

== 场景 B: 三个开关全关,坏对象不加检测直接借出 ==
借到 Session-1(正是损坏的 Session-1=true,validate 调用 0 次)

== 场景 C: testOnReturn=true,归还时校验失败即销毁 ==
  [destroyObject] Session-1 被销毁
归还损坏的 Session-1 后: numIdle=0, numActive=0(坏对象未进空闲队列)

== 场景 D: testWhileIdle=true + 每 200ms 巡检,空闲坏对象被清除 ==
置坏后等待巡检(巡检间隔 200ms)...
  [destroyObject] Session-1 被销毁
巡检后 numIdle=0(坏对象已被驱逐线程清除)

结果解读:场景 A 中 validate 调用 3 次 的构成是——首次借出校验 1 次、二次借出对坏对象校验失败 1 次、对重建的新对象校验 1 次;坏对象在借出点被销毁,业务拿到的是全新对象。场景 B 证明默认配置下坏对象会原样交付业务。四个开关的取舍见下表:

开关校验时机优点代价推荐组合
testOnBorrow每次借出前坏对象绝不交给业务每次借出多一次校验往返对象昂贵/易坏时开启
testOnReturn归还时坏对象在归还点就地销毁,不占池位归还路径变重与 testOnBorrow 二选一或叠加
testWhileIdle空闲巡检时批量清理、摊薄开销清理滞后(依赖巡检周期)长期空闲易失效的对象开启
testOnCreate创建后拦截出厂即坏的对象创建路径变重极少开启(工厂已保证创建质量)

要点:四个开关可叠加;生产最常见组合是 testOnBorrow=true + testWhileIdle=true,前者保正确性、后者清积压。

5.4 驱逐参数:timeBetweenEvictionRuns / minEvictableIdleTime / softMinEvictableIdleTime / numTestsPerEvictionRun / evictionPolicyClassName

驱逐子系统的两个前提:timeBetweenEvictionRunsMillis=-1(默认)表示巡检线程不启动,一切驱逐参数形同虚设;判定逻辑默认由 evictionPolicyClassName=DefaultEvictionPolicy 完成。先用默认参数对照(实时回显):

== 步骤 3: 对照默认策略 minEvictableIdleTimeMillis(30 分钟) ==
默认 timeBetweenEvictionRunsMillis=-1, minEvictableIdleTimeMillis=1800000ms, evictionPolicyClassName=org.apache.commons.pool2.impl.DefaultEvictionPolicy
空闲 700ms 后默认策略下 numIdle=1(远小于 30 分钟,不驱逐)

再自定义策略:巡检周期 200ms + 「空闲超 600ms 即驱逐」的自定义 EvictionPolicy(实时回显):

== 步骤 1: 借 3 个 Job 并立即归还(全部进入空闲) ==
归还后 numIdle=3, numActive=0

== 步骤 2: 保持空闲 1.6 秒,观察驱逐线程(每 200ms 巡检) ==
  [EvictionPolicy] Job-1 空闲 798ms > 600ms -> 判定驱逐 (第 1 次)
  [destroyObject] 销毁 Job-1
  [EvictionPolicy] Job-2 空闲 804ms > 600ms -> 判定驱逐 (第 2 次)
  [destroyObject] 销毁 Job-2
  [EvictionPolicy] Job-3 空闲 804ms > 600ms -> 判定驱逐 (第 3 次)
  [destroyObject] 销毁 Job-3
1.6s 后 numIdle=0, 驱逐判定次数=3

验证结果:巡检线程按 200ms 周期运转,实际驱逐发生在空闲 798ms——即空闲时长超过 600ms 阈值后的第一轮巡检点;驱逐判定与销毁一一对应,3 个空闲对象全部被清出。要点:实际驱逐时间 = 阈值向上对齐到巡检周期,把巡检周期设得过大会让 minEvictableIdleTime 失去精度;softMinEvictableIdleTimeMillis 则是「保证 minIdle 存量」的软阈值版本——空闲超过软阈值时,只驱逐超出 minIdle 的部分。

⚠️ 注意:毫秒版本的 setter/getter 在 2.13.1 中已标记 @Deprecated,官方推荐 Duration 版本。实测编译告警(实时回显):

Exp4Eviction.java:46: warning: [deprecation] setTimeBetweenEvictionRunsMillis(long) in BaseGenericObjectPool has been deprecated
        pool.setTimeBetweenEvictionRunsMillis(200);
            ^

对应的新写法是 pool.setTimeBetweenEvictionRuns(Duration.ofMillis(200)),语义完全一致;本文实验沿用毫秒版本以保证与大量现存代码一致。

5.5 借出顺序:lifo 与 fairness

lifo=true(默认)时归还的对象放在空闲队列头部、借出优先拿最近归还的;lifo=false 退化为 FIFO 轮转。同一程序分别在两种模式下借还 3 个对象(实时回显):

-- 默认 LIFO 后进先出(lifo=true) --
依次借出 Token-1, Token-2, Token-3
按 1→2→3 顺序归还,随后连续借出:
  借出顺序: Token-3 -> Token-2 -> Token-1

-- 显式 FIFO 先进先出(lifo=false) --
依次借出 Token-1, Token-2, Token-3
按 1→2→3 顺序归还,随后连续借出:
  借出顺序: Token-1 -> Token-2 -> Token-3

结果解读:LIFO 下借出序号是归还序号的完全倒序(3→2→1),FIFO 下与归还序号一致(1→2→3)。fairness=true 是另一个维度的开关:它约束的是「多个线程同时等对象」时按等待先后发放(FIFO 公平锁),与 lifo 管的「空闲队列内部顺序」正交,默认 false 换取更高吞吐。

5.6 JMX 参数:jmxEnabled / jmxNamePrefix / jmxNameBase

JMX 默认开启(jmxEnabled=true),池对象在构造时即向平台 MBeanServer 注册,ObjectName 默认为 org.apache.commons.pool2:type=GenericObjectPool,name=pool。实测注册、改名与关闭(实时回显):

== [1] 默认配置创建池(jmxEnabled 默认 true) ==
[构造后] org.apache.commons.pool2:* 匹配到 1 个 MBean:
  ObjectName = org.apache.commons.pool2:type=GenericObjectPool,name=pool  类 = org.apache.commons.pool2.impl.GenericObjectPool

== [2] 通过 JMX 读池指标 ==
borrow 2 个后: NumActive=2, NumIdle=0, MaxTotal=4
return 1 个后: NumActive=1, NumIdle=1

== [3] 自定义 JMX 名称前缀 ==
[带前缀池构造后] org.apache.commons.pool2:* 匹配到 2 个 MBean:
  ObjectName = org.apache.commons.pool2:type=GenericObjectPool,name=pool  类 = org.apache.commons.pool2.impl.GenericObjectPool
  ObjectName = org.apache.commons.pool2:type=GenericObjectPool,name=order-db-pool  类 = org.apache.commons.pool2.impl.GenericObjectPool

== [4] 关闭 JMX(jmxEnabled=false) ==
jmxEnabled=false 的池创建后 MBean 总数保持 2 个(未新增)

验证结果:注册发生在池构造时(不是首次借出);MBean 实时反映 NumActive/NumIdle;jmxNamePrefix 可把 name 改成语义化名称(order-db-pool);jmxEnabled=false 的池不注册。要点:同一 JVM 内多个池必须用不同 jmxNamePrefix,否则 ObjectName 冲突导致注册失败。

5.7 Keyed 池专属参数:maxTotalPerKey / maxIdlePerKey / minIdlePerKey

GenericKeyedObjectPool 把上述容量概念按键拆分:maxTotalPerKey 约束单键在用数量,键与键之间互不影响。实测(每键上限 2,mysql 键借满后再借、redis 键照常,实时回显):

== [1] 多键借还:键 mysql 与 redis 各借 2 个 ==
mysql 键借到: Handle[mysql#1], Handle[mysql#2]
redis 键借到: Handle[redis#3]
getNumActive(mysql)=2, getNumActive(redis)=1, getNumIdle(mysql)=0

== [2] 单键上限:mysql 已借 2 个(上限 2),第 3 个等待 200ms ==
捕获 NoSuchElementException, 实际等待=200ms
异常消息: Timeout waiting for idle object, borrowMaxWaitMillis=200
redis 键不受影响: pool.borrowObject("redis") 仍可用=true

验证结果:mysql 键借满 2 个后第 3 次借出按 maxWait 超时失败(异常消息里的 borrowMaxWaitMillis=200 是键控池路径的独立参数),而 redis 键的借用完全不受影响。要点:键控池没有全局 maxTotal 硬上限(默认 -1),资源规划必须按「热点键可能占满 maxTotalPerKey × 键数量」评估。

六、生产环境参数模板

本节给出一套经过实测验证的「昂贵连接类对象」生产参数模板,每项附选择依据(默认值均来自第四章总表)。

GenericObjectPoolConfig<Conn> cfg = new GenericObjectPoolConfig<>();
cfg.setMaxTotal(32);                          // 上游资源(如数据库连接数)预算内取值
cfg.setMaxIdle(32);                           // 与 maxTotal 持平,避免突发过后拆池重建
cfg.setMinIdle(4);                            // 保底热对象,配合巡检周期预热
cfg.setMaxWait(Duration.ofMillis(3000));      // 必设有限值,防漏还对象拖死业务线程
cfg.setTestOnBorrow(true);                    // 借出前校验,保证交付质量
cfg.setTestWhileIdle(true);                   // 空闲巡检校验,清理长期闲置的坏对象
cfg.setTimeBetweenEvictionRuns(Duration.ofSeconds(30));  // 巡检周期
cfg.setMinEvictableIdleTime(Duration.ofMinutes(5));      // 空闲 5 分钟可驱逐
cfg.setJmxEnabled(true);                      // 保留监控入口
cfg.setJmxNamePrefix("order-db-pool");        // 多池场景必改,防 ObjectName 冲突
GenericObjectPool<Conn> pool = new GenericObjectPool<>(factory, cfg);

逐项选择依据:

  • maxWait=3s:2.13.1 默认 -1(无限等待)在生产不可接受,实测漏还对象会让业务线程永久挂起(5.2 节)。
  • testOnBorrow + testWhileIdle:5.3 节实测的组合,前者保正确性、后者清积压,是检测开关的常见生产搭配。
  • maxIdle=maxTotal:5.1 节实测 maxIdle 收敛会当场销毁超限对象,压平可避免突发流量反复付创建成本。
  • 巡检 30s + 空闲 5min 驱逐:驱逐实际发生点是「阈值向上对齐巡检周期」(5.4 节实测),周期取秒级即可保证驱逐精度。
  • JMX 前缀语义化:5.6 节实测多池同名会冲突,前缀按业务命名。

七、常见问题(FAQ)

Q1:maxWait 设 -1 有什么风险? 默认值就是 -1(无限等待)。实测(5.2 节)池空时 borrowObject 会挂起直到有对象归还;若业务漏还,挂起永不结束。生产必须设有限值并在借出侧用 try-finally 保证归还。

Q2:开了驱逐参数为什么对象一直不被驱逐? 检查 timeBetweenEvictionRunsMillis 是否还是默认 -1(探针实测默认值,见第三章)——它为 -1 时巡检线程根本不启动,minEvictableIdleTimeMillis 再小也不会触发驱逐。

Q3:testOnBorrow 会不会太慢? 会引入每次借出一次 validateObject 的开销,是否可接受取决于校验成本(如一次轻量 ping)。折中方案是只开 testWhileIdle(批量摊薄),或对低成本校验开 testOnBorrow、对高成本校验改用应用层重试。

Q4:多个池启动报 MBean 注册冲突? ObjectName 默认都是 name=pool(5.6 节实测),同 JVM 第二个池注册会失败。给每个池设置不同的 jmxNamePrefix 即可。

Q5:borrowObject 抛 Timeout waiting for idle object 怎么排查? 先看 getNumActive() 是否长期等于 maxTotal(对象泄漏/漏还),再评估 maxTotal 是否低于真实并发峰值。排查与参数关联详见 5.1/5.2 节实测。

Q6:毫秒版 setter 报 deprecated 警告要紧吗? 2.13.1 中 setTimeBetweenEvictionRunsMillis 等毫秒 API 已废弃但功能完好(5.4 节实测告警与行为),新代码建议直接用 Duration 版本。

八、参考链接

  1. Commons Pool 2.13.1 Javadoc(GenericObjectPoolConfig/BaseObjectPoolConfig):https://commons.apache.org/proper/commons-pool/apidocs/
  2. Commons Pool 配置参数官方文档(池配置章节):https://commons.apache.org/proper/commons-pool/
  3. Commons Pool 2.13.1 源码(默认值常量定义):https://downloads.apache.org/commons/pool/source/
  4. Maven 中央仓库 commons-pool2 2.13.1:https://repo1.maven.org/maven2/org/apache/commons/commons-pool2/2.13.1/