generic-object-pool is a lightweight generic object pool, providing object lifecycle management, metrics, claim / release mechanism and object invalidation, as well as auto initialize a core pool and auto expiry policies.
The work that became generic-object-pool began in June 2019 with a fork of KBOP, created by Jeremy Unruh. It was subsequently modernized and extensively redesigned for Simple Java Mail's SMTP connection pooling, and has continued as an independent library. KBOP's original work is used under the MIT License; its complete copyright and license notice is included in NOTICE.txt.
Maven Dependency Setup
<dependency>
<groupId>com.github.bbottema</groupId>
<artifactId>generic-object-pool</artifactId>
<version>2.6.0</version>
</dependency>For JPMS applications, the published JAR declares the stable automatic module name
org.bbottema.genericobjectpool.
2.6.0 (12 October 2026)
- #26: Pause automatic pool top-up with
Allocator.isBackgroundAllocationAllowed(), for example while your application handles a service outage. Existing allocators keep automatic top-up enabled; ordinary claims, existing resources and expiration are unchanged. - Returning
falsepostpones spare-resource creation without an allocation error or reserved capacity. Your application decides when to allow it again; the pool does not add retries or health checks. See Pausing automatic top-up for an example. AllocationContext.getClaimIdentity()correlates one acquisition across pool integration layers. Narrowing its budget preserves the identity; starting another claim creates a new one. Java 8 and existing APIs remain supported.
2.5.1 (17 September 2026)
- #24, #25: Keep creation-age and last-allocation timeout policies distinct when their thresholds or randomized bounds match, so combined policies retain both rules and each rule keeps its own expiry state.
- Matching configurations of the same policy type remain equal. Java 8, existing APIs, and the JPMS module name are unchanged.
// basic pool with no eager loading and no expiry policy
PoolConfig<Foo> poolConfig = PoolConfig.<Foo>builder()
.maxPoolsize(10)
.build();
GenericObjectPool<Foo> pool = new GenericObjectPool<>(poolConfig, new MyFooAllocator());// more advanced pool with eager loading and auto expiry
PoolConfig<Foo> poolConfig = PoolConfig.<AtomicReference<Integer>>builder()
.corePoolsize(20) // keeps 20 objects eagerly allocated at all times
.maxPoolsize(20)
// deallocate after 30 seconds, but every time an object is claimed the expiry timeout is reset
.expirationPolicy(new TimeoutSinceLastAllocationExpirationPolicy<Foo>(30, TimeUnit.SECONDS))
.build();
GenericObjectPool<Foo> pool = new GenericObjectPool<>(poolConfig, new MyFooAllocator());The pool normally creates spare resources in the background to maintain corePoolsize. You can pause that work without
stopping callers from borrowing resources. For example, your application might avoid opening spare service connections
while it handles an outage, then permit them again after recovery.
Override Allocator.isBackgroundAllocationAllowed() and return your application's current decision. Here, MyFooAllocator
is the application's allocator from the pool examples above, and AtomicBoolean makes the flag safe across threads:
AtomicBoolean automaticTopUp = new AtomicBoolean(true);
Allocator<Foo> allocator = new MyFooAllocator() {
@Override
public boolean isBackgroundAllocationAllowed() {
return automaticTopUp.get();
}
};
PoolConfig<Foo> config = PoolConfig.<Foo>builder()
.corePoolsize(2)
.maxPoolsize(10)
.build();
GenericObjectPool<Foo> pool = new GenericObjectPool<>(config, allocator);
automaticTopUp.set(false); // Application decides not to create spare resources right now.
// Normal claim/release calls still work, including allocation needed by an explicit claim.
automaticTopUp.set(true); // The maintenance worker can top up again on a later pass.The numbers are example pool sizes, not limits supplied by the library. With no core pool size configured, there is no
automatic top-up to pause. Returning false is not an allocation error and keeps no pool capacity reserved. Existing
resources, expiration and disposal continue normally; an allocation already underway may still finish.
Keep the check quick and thread-safe: the maintenance worker polls it outside pool bookkeeping locks and checks again
immediately before creating a resource. A runtime exception in the check is logged and postpones top-up. This is not a
circuit breaker for explicit claims, and it does not schedule retries or detect service recovery for you. Existing allocators
need no changes: the default returns true. This API is available from 2.6.0.
Claiming objects from the pool (blocking):
// borrow an object and block until available
PoolableObject<Foo> obj = pool.claim();Claiming objects from the pool (blocking until timeout):
PoolableObject<Foo> obj = pool.claim(1, TimeUnit.SECONDS); // null if timed outClaiming an already available object matching a predicate:
PoolableObject<Foo> obj = pool.claimMatching(
poolable -> poolable.idleAgeMs() >= TimeUnit.MINUTES.toMillis(5),
1,
TimeUnit.SECONDS);
if (obj != null) {
try {
obj.getAllocatedObject().ping();
obj.release();
} catch (IOException e) {
obj.invalidate();
}
}The predicate is evaluated while the pool claim lock is held, so keep it fast and side-effect free. Run slow work such as ping/keep-alive checks after the object has been claimed.
Releasing Objects back to the Pool:
PoolableObject<Foo> obj = pool.claim();
obj.release(); // make available for reuse
// or
obj.invalidate(); // remove from pool, deallocatingInvalidation wakes ordinary callers that are already waiting for capacity. With a core size of zero, a waiting caller can create a replacement; a configured core pool also replenishes itself. Final cleanup remains asynchronous and need not finish before a replacement can be used. Matching claims still only take available objects: they do not allocate replacements themselves.
For example, a cancelled export job should stop waiting for another database connection. Create the control before starting its worker, then retain it in the job's stop handler:
ClaimControl claimControl = new ClaimControl();
ClaimOptions options = ClaimOptions.withTimeout(30, TimeUnit.SECONDS)
.withClaimControl(claimControl);
// On the job's worker; still an ordinary blocking call.
PoolableObject<Foo> resource = pool.claim(options);
// null: budget expired. CancellationException: stop requested. InterruptedException: worker interrupted.
if (resource != null) {
try {
resource.getAllocatedObject().doWork();
} finally {
resource.release();
}
}
// In the stop handler, potentially on another thread:
claimControl.requestCancellation();Creating a control does not cancel anything. A control is thread-safe and one-shot; options are immutable and reusable.
Each call starts a new monotonic budget covering selection by an outer pool, lock waiting, availability and preparation.
claimMatching(predicate, options) has the same contract but never allocates resources. Zero budget starts no preparation.
The legacy timeout methods retain their existing wait semantics; they do not gain a total deadline implicitly.
Pool integrations can correlate one running claim across layers using context.getClaimIdentity(). The opaque object
is unique to each ClaimOptions.start() call; limitedTo(...) keeps the same identity while narrowing its budget.
For example, register a caller's observation by that identity before claimWithContext(context), then remove it in
finally after the claim settles. It is not a resource ID or a reusable job ID. Ordinary callers need no extra wiring.
Cancellation ends at handoff: requesting it after claim returns does not revoke the borrowed resource. It does not
interrupt an executor thread. Allocator callbacks run on the claiming thread (core replenishment remains pool-owned),
and allocation/reuse/release preparation remains serialized per pool. Final deallocation runs on the cleanup worker.
Override allocate(AllocationContext) or allocateForReuse(resource, AllocationContext) to cooperate: check
context.throwIfCancellationRequested(), use context.getRemainingTime(unit) for your own I/O timeout, and optionally
register a quick, non-blocking abort action with context.onCancellation(...). An earlier request invokes that handler
immediately. Handler runtime exceptions are logged and ignored. Close registrations when their resource is no longer
owned; the pool also detaches them before handoff. An allocator must clean up anything it creates but never returns.
An old allocator that blocks in allocate() remains supported, but cannot be forcibly stopped: cancellation is recorded
promptly, then the claim waits for allocation to exit and disposes any late resource instead of handing it off. Required
cleanup can also outlast the acquisition budget. New context-aware claims settle only after that cleanup finishes;
cleanup failures are reported (or suppressed on the original failure), not treated as successful disposal. A preparation
exception after cancellation was requested is retained as the cause of CancellationException.
invalidate() still only schedules cleanup. If physical disposal matters, use
resource.getDisposalCompletion().toCompletableFuture().get(). A healthy release() does not complete this stage;
the resource is still reusable. Each returned stage is detached from the pool's internal signal. Keep non-async stage
callbacks short because they can run on the cleanup worker.
Future<?> shutdownSequence = pool.shutdown();
// wait for shutdown to complete
shutdownSequence.get();
// until timeout
shutdownSequence.get(10, TimeUnit.SECONDS);Implementing a simple Allocator to create your objects when populating the pool either eagerly or lazily.
Every method except allocate is optional:
static class FooAllocator extends Allocator<Foo> {
/**
* Initial creation and initialization.
* Called when claim comes or when pool is eagerly loading for core size.
*/
@Override
public AtomicReference<Integer> allocate() {
return new Foo();
}
}More comprehensive life cycle management:
static class FooAllocator extends Allocator<Foo> {
/**
* Initial creation and initialization.
* Called when claim comes or when pool is eagerly loading for core size.
*/
@Override
public AtomicReference<Integer> allocate() {
return new Foo();
}
/**
* Uninitialize an instance which has been released back to the pool, until it is claimed again.
*/
@Override
protected void deallocateForReuse(Foo object) {
object.putAtRest();
}
/**
* Reinitialize an object so it is ready to be claimed.
*/
@Override
protected void allocateForReuse(Foo object) {
object.reinitialize();
}
/**
* Clean up an object no longer needed by the pool.
*/
@Override
protected void deallocate(Foo object) {
object.clear();
}
}PoolMetrics metrics = pool.getPoolMetrics();
metrics.getCurrentlyClaimed(); // currently claimed by threads and not released yet
metrics.getCurrentlyWaitingCount(); // currently waiting threads that want to claim
metrics.getCorePoolsize(); // number of instances to auto allocated (eager loading)
metrics.getMaxPoolsize(); // max number of objects allowed at all times
metrics.getCurrentlyAllocated(); // available + claimed objects
metrics.getTotalAllocated(); // total number of allocations during pool's existence
metrics.getTotalClaimed(); // total number of claims during pool's existenceFor idle maintenance, PoolableObject#idleAgeMs() reports how long an object has been available for claiming. It returns 0 while the object is claimed.
If for some reason you need to have more control over how threads are created, you can provide you own ThreadFactory:
PoolConfig<Foo> poolConfig = PoolConfig.<AtomicReference<Integer>>builder()
.threadFactory(new MyCustomThreadFactory())
.build();You can expire objects based on age since creation or age since last allocation. For these use:
TimeoutSinceCreationExpirationPolicyTimeoutSinceLastAllocationExpirationPolicy
You can also spread the expiry around in a bandwidth to avoid having everything expire at the same time, hogging system resources. for these use:
SpreadedTimeoutSinceCreationExpirationPolicySpreadedTimeoutSinceLastAllocationExpirationPolicy
You can also combine multiple expirations, by passing instances of them as a set to:
CombinedExpirationPolicies
Since 2.5.1, creation-age and last-allocation policies remain distinct even when their timeout values or randomized bounds match. Matching configurations of the same policy type still represent one rule.
Finally, you can extend any of these or create your own from scratch by implementing:
ExpirationPolicy
To aid you in creating your own expiry policy, you can calculate and store an expiry age on the poolable object:
poolableObject.getExpiries().put(this, calculatedAge);
Long previouslyCalculateAge = poolableObject.getExpiries().get(this);You can always extend one the abstract classes SpreadedTimeoutExpirationPolicy and TimeoutExpirationPolicy, which do this for you.