Java Resource Management
Purpose
Make every resource's release deterministic and owned by exactly one piece of code. The
failure modes: the finally block that discards the real exception and reports the one
thrown by close; the resource that leaks only on the error path, so it survives every
test and exhausts the pool during the first incident; and the callee that closes a stream
its caller still needs, which fails as a Stream has already been operated upon or closed
far from the code that caused it.
Workflow
Use Java 21 for stable-API examples and explicitly marked Java 25 preview semantics only for
StructuredTaskScope. Inspect compiler/runtime, preview policy, driver/pool contracts and the
actual owner before changing lifetimes; do not upgrade a project or enable preview for a cleanup
fix. Missing cancellation/close guarantees must remain explicit unknowns.
- Name the lifetime authority. Prefer one owner that acquires/releases. Borrowed,
reference-counted or shared resources need an explicit protocol instead. A method receiving an
open resource normally borrows it; consuming/closing must be named and documented.
- Make the scope lexical. Acquire in a
try-with-resources header. If the resource
must outlive the method, the method is not the owner — return it, and let the owner's
scope hold it.
- Declare each resource separately.
try (var raw = open(); var buf = wrap(raw)), not
a nested constructor chain: if the outer constructor throws, the inner resource is
already open and nothing references it. This shape may close the raw resource twice when
the wrapper owns it; verify idempotence or use an explicit success-transfer/failure-cleanup
protocol for resources that cannot be released twice.
- Decide what a failing
close means. If the body already failed, try-with-resources
suppresses cleanup failure; if the body succeeded, close failure propagates, for readers too.
A writer's failed flush/close can leave partial or complete writes with uncertain durability.
Do not report success or infer that retrying is safe merely because close threw.
- Check every escape route. A resource captured by a lambda submitted to an executor,
stored in a field, returned inside a
Stream, or held across a CompletableFuture
boundary has left the lexical scope. Either the scope must wait, or ownership must move.
- Verify on the failure path. A test that throws from inside the body and asserts the
resource was closed once. That path is the one that leaks in production.
Rules
- Prefer
try-with-resources for lexically owned AutoCloseables. Application-lifecycle,
conditional-transfer and asynchronous ownership may need an explicit state machine/finally.
Resources close in reverse declaration order,
and an exception from close is suppressed onto the body's exception rather than
replacing it—getSuppressed() recovers it. A naive finally { close(); } can replace the body
exception unless it manually implements equivalent suppression.
- Since Java 9 an existing effectively-final variable can be used directly:
try (existingResource). This does not transfer aliases or make ownership obvious; choose a
local name/Javadoc when it clarifies that the scope closes a borrowed-looking value.
- Implement
Closeable when its stronger idempotence and IOException contract fit; implement
AutoCloseable otherwise. I/O association alone is not decisive—JDBC resources implement
AutoCloseable. Declare the narrowest failure type; avoid throws Exception in a public
implementation unless callers genuinely need that generality.
- Make custom
close idempotent where feasible. Closeable requires it and AutoCloseable
strongly advises it, but third-party/reference-counted release protocols may reject double
release. Never infer idempotence from use in a pool or decorator.
close must not block indefinitely and must not do work that can fail after the point of
no return without an explicit partial-result/durability contract. Where the library can block
indefinitely, document that limitation and the lifecycle escalation policy rather than promise
bounded cleanup. A close that flushes over a network needs the same timeout discipline as any
other remote call — see timeouts-and-deadlines.
- Most streams need no closing; the ones backed by an I/O resource do—
Files.lines,
Files.walk, Files.find, Files.list, and Files.newDirectoryStream. A method that returns such a
stream has handed the caller a resource, and its Javadoc must say so.
ExecutorService has been AutoCloseable since Java 19, and its close() initiates an
orderly shutdown and then blocks until all submitted tasks finish. In
try-with-resources that is a join point, not a cheap release: a long-running task makes
the enclosing method hang there. If the calling thread is interrupted while waiting,
close stops executing tasks as if by shutdownNow, keeps waiting for those already
running, and re-asserts the interrupt before returning. Use it when the block genuinely owns the work; use
explicit shutdown/awaitTermination with a bound when it does not.
- A pooled resource is returned, not destroyed — but the caller's code is identical:
close() on a pooled Connection gives it back. Holding one beyond the operation is the
same defect as leaking it, because the pool is the real bound; connection-pool-sizing owns
the arithmetic.
- Before closing a JDBC
Connection, explicitly commit or roll back an active transaction; JDBC
does not define portable close behaviour with one active. Reset failures can cause a pool to
evict rather than reuse the physical resource.
- Never let a resource escape into an asynchronous stage without moving ownership and cancellation
policy with it.
try (var conn = pool.get()) { return async(conn); } closes the connection before the
future completes; the stage then fails with a closed-resource error under load and not in
the test. Acquire inside the actual task where possible. A whenComplete(close) callback is
insufficient if cancellation completes the exposed future before underlying use stops; release
only after actual use terminates and propagate/suppress close failure deliberately.
- Do not use finalizers, and do not reach for
Cleaner as the primary release mechanism —
it is a safety net that logs a leak, if it runs at all. java-reference-types-and-leaks
covers when a safety net is justified and how to write one that can actually fire.
- Virtual threads remove the thread as the implicit limit on concurrent resources. One
connection per task was bounded by a 200-thread pool; on
newVirtualThreadPerTaskExecutor it is bounded by nothing until the pool refuses. The
bound must become explicit — a semaphore or the pool's own limit; see
concurrency-limiting-and-bulkheads.
- A permit/pool limit bounds active use, not tasks waiting for it. Bound admission/waiters and
acquisition time as well; a timed-out caller must not release a permit while its work still
uses the protected resource.
StructuredTaskScope remains a preview API in Java 25 and changed across previews. Java 25
close() cancels unfinished subtasks, waits for their threads, and reports missing join() or
structural misuse. Cancellation is cooperative; a subtask that ignores interruption can delay
close indefinitely. Pin the JDK/preview contract and do not transplant examples across releases.
Report the owner on success, partial acquisition, body failure, cancellation and close failure;
include the targeted tests actually run and remaining guarantees that depend on a driver/runtime.
References
- Designing an AutoCloseable — read when writing a type
that owns a resource, when wrapping or decorating one, when
close can fail, or when
deciding what a method that returns a resource promises its caller.
- Resources across async, pooled and shutdown boundaries
— read when a resource is used by an executor task, a
CompletableFuture chain or a
structured-concurrency fork, when a pool is exhausted under load, or when resources must
be drained during shutdown.
1---2name: java-resource-management3description: Deterministic release of what a Java program holds open: try-with-resources and the exception semantics that make it non-optional, designing an AutoCloseable (ownership, idempotent close, close that fails), decorators and partially constructed resource chains, resources that cross an async or executor boundary, and the difference between closing a resource and returning one to a pool. Use when a close sits in a finally block, when a resource is created inside a try block or inside a lambda that outlives it, when a method closes something it was handed, when connections or file descriptors leak under load, when ExecutorService or StructuredTaskScope is used in try-with-resources, or when a stream from Files.lines or Files.walk is never closed. Does not cover reachability-driven cleanup — WeakReference, SoftReference, Cleaner and the leaks they hide (java-reference-types-and-leaks) — pool sizing (connection-pool-sizing), or native segment lifetimes (off-heap-memory).4---56# Java Resource Management78## Purpose910Make every resource's release deterministic and owned by exactly one piece of code. The11failure modes: the `finally` block that discards the real exception and reports the one12thrown by `close`; the resource that leaks only on the error path, so it survives every13test and exhausts the pool during the first incident; and the callee that closes a stream14its caller still needs, which fails as a `Stream has already been operated upon or closed`15far from the code that caused it.1617## Workflow1819Use Java 21 for stable-API examples and explicitly marked Java 25 preview semantics only for20StructuredTaskScope. Inspect compiler/runtime, preview policy, driver/pool contracts and the21actual owner before changing lifetimes; do not upgrade a project or enable preview for a cleanup22fix. Missing cancellation/close guarantees must remain explicit unknowns.23241. **Name the lifetime authority.** Prefer one owner that acquires/releases. Borrowed,25 reference-counted or shared resources need an explicit protocol instead. A method receiving an26 open resource normally borrows it; consuming/closing must be named and documented.272. **Make the scope lexical.** Acquire in a `try`-with-resources header. If the resource28 must outlive the method, the method is not the owner — return it, and let the owner's29 scope hold it.303. **Declare each resource separately.** `try (var raw = open(); var buf = wrap(raw))`, not31 a nested constructor chain: if the outer constructor throws, the inner resource is32 already open and nothing references it. This shape may close the raw resource twice when33 the wrapper owns it; verify idempotence or use an explicit success-transfer/failure-cleanup34 protocol for resources that cannot be released twice.354. **Decide what a failing `close` means.** If the body already failed, try-with-resources36 suppresses cleanup failure; if the body succeeded, close failure propagates, for readers too.37 A writer's failed flush/close can leave partial or complete writes with uncertain durability.38 Do not report success or infer that retrying is safe merely because close threw.395. **Check every escape route.** A resource captured by a lambda submitted to an executor,40 stored in a field, returned inside a `Stream`, or held across a `CompletableFuture`41 boundary has left the lexical scope. Either the scope must wait, or ownership must move.426. **Verify on the failure path.** A test that throws from inside the body and asserts the43 resource was closed once. That path is the one that leaks in production.4445## Rules4647- Prefer `try`-with-resources for lexically owned `AutoCloseable`s. Application-lifecycle,48 conditional-transfer and asynchronous ownership may need an explicit state machine/finally.49 Resources close in reverse declaration order,50 and an exception from `close` is _suppressed_ onto the body's exception rather than51 replacing it—`getSuppressed()` recovers it. A naive `finally { close(); }` can replace the body52 exception unless it manually implements equivalent suppression.53- Since Java 9 an existing effectively-final variable can be used directly:54 `try (existingResource)`. This does not transfer aliases or make ownership obvious; choose a55 local name/Javadoc when it clarifies that the scope closes a borrowed-looking value.56- Implement `Closeable` when its stronger idempotence and `IOException` contract fit; implement57 `AutoCloseable` otherwise. I/O association alone is not decisive—JDBC resources implement58 `AutoCloseable`. Declare the narrowest failure type; avoid `throws Exception` in a public59 implementation unless callers genuinely need that generality.60- Make custom `close` idempotent where feasible. `Closeable` requires it and `AutoCloseable`61 strongly advises it, but third-party/reference-counted release protocols may reject double62 release. Never infer idempotence from use in a pool or decorator.63- `close` must not block indefinitely and must not do work that can fail after the point of64 no return without an explicit partial-result/durability contract. Where the library can block65 indefinitely, document that limitation and the lifecycle escalation policy rather than promise66 bounded cleanup. A `close` that flushes over a network needs the same timeout discipline as any67 other remote call — see timeouts-and-deadlines.68- Most streams need no closing; the ones backed by an I/O resource do—`Files.lines`,69 `Files.walk`, `Files.find`, `Files.list`, and `Files.newDirectoryStream`. A method that returns such a70 stream has handed the caller a resource, and its Javadoc must say so.71- `ExecutorService` has been `AutoCloseable` since Java 19, and its `close()` initiates an72 orderly shutdown and then _blocks until all submitted tasks finish_. In73 `try`-with-resources that is a join point, not a cheap release: a long-running task makes74 the enclosing method hang there. If the calling thread is interrupted while waiting,75 `close` stops executing tasks as if by `shutdownNow`, keeps waiting for those already76 running, and re-asserts the interrupt before returning. Use it when the block genuinely owns the work; use77 explicit `shutdown`/`awaitTermination` with a bound when it does not.78- A pooled resource is _returned_, not destroyed — but the caller's code is identical:79 `close()` on a pooled `Connection` gives it back. Holding one beyond the operation is the80 same defect as leaking it, because the pool is the real bound; connection-pool-sizing owns81 the arithmetic.82- Before closing a JDBC `Connection`, explicitly commit or roll back an active transaction; JDBC83 does not define portable close behaviour with one active. Reset failures can cause a pool to84 evict rather than reuse the physical resource.85- Never let a resource escape into an asynchronous stage without moving ownership and cancellation86 policy with it.87 `try (var conn = pool.get()) { return async(conn); }` closes the connection before the88 future completes; the stage then fails with a closed-resource error under load and not in89 the test. Acquire inside the actual task where possible. A `whenComplete(close)` callback is90 insufficient if cancellation completes the exposed future before underlying use stops; release91 only after actual use terminates and propagate/suppress close failure deliberately.92- Do not use finalizers, and do not reach for `Cleaner` as the primary release mechanism —93 it is a safety net that logs a leak, if it runs at all. java-reference-types-and-leaks94 covers when a safety net is justified and how to write one that can actually fire.95- Virtual threads remove the thread as the implicit limit on concurrent resources. One96 connection per task was bounded by a 200-thread pool; on97 `newVirtualThreadPerTaskExecutor` it is bounded by nothing until the pool refuses. The98 bound must become explicit — a semaphore or the pool's own limit; see99 concurrency-limiting-and-bulkheads.100- A permit/pool limit bounds active use, not tasks waiting for it. Bound admission/waiters and101 acquisition time as well; a timed-out caller must not release a permit while its work still102 uses the protected resource.103- `StructuredTaskScope` remains a preview API in Java 25 and changed across previews. Java 25104 `close()` cancels unfinished subtasks, waits for their threads, and reports missing `join()` or105 structural misuse. Cancellation is cooperative; a subtask that ignores interruption can delay106 close indefinitely. Pin the JDK/preview contract and do not transplant examples across releases.107108Report the owner on success, partial acquisition, body failure, cancellation and close failure;109include the targeted tests actually run and remaining guarantees that depend on a driver/runtime.110111## References112113- [Designing an AutoCloseable](references/closeable-design.md) — read when writing a type114 that owns a resource, when wrapping or decorating one, when `close` can fail, or when115 deciding what a method that returns a resource promises its caller.116- [Resources across async, pooled and shutdown boundaries](references/async-and-pooled-resources.md)117 — read when a resource is used by an executor task, a `CompletableFuture` chain or a118 structured-concurrency fork, when a pool is exhausted under load, or when resources must119 be drained during shutdown.