An object can become unreachable while a file descriptor, socket, database transaction, or operating-system lock associated with it still has a meaningful external lifetime. Garbage collection can reclaim managed memory after reachability disappears. It does not, by that fact alone, perform the protocol operation that releases an external resource.
The distinction is structural. A collector reasons about references inside a managed heap. A resource such as a file descriptor is an entry maintained by the operating system, and a database transaction is state maintained by another component. Their release semantics come from APIs and protocols outside the collector’s reachability model.
This creates two clocks in code that wraps external resources: the lifetime of the wrapper object and the lifetime of the thing it owns. Treating those clocks as interchangeable makes cleanup timing depend on runtime behavior that may not carry the required semantic guarantee.
Reachability is not ownership
Tracing garbage collectors commonly determine whether managed objects remain reachable from a set of roots. Once an object is unreachable, its storage can eventually be reclaimed. The exact mechanics vary by runtime, but the relevant boundary is stable: reachability describes whether program references can still lead to an object.
Ownership answers a different question. If an object contains a file handle, which operation relinquishes that handle? If it represents a transaction, which operation commits or rolls it back? If it holds a lease, which participant releases or expires that lease?
Those actions are not consequences of memory becoming unused. They are state transitions in another subsystem.
Consider a wrapper with a conceptual shape like this:
Connection {
socket
receive_buffer
protocol_state
}The receive buffer and protocol state may be ordinary managed memory. The socket is not merely another region of heap storage. Closing it can notify the operating system, alter peer-visible connection state, release descriptor capacity, and interact with protocol semantics. Reclaiming the wrapper’s memory cannot be treated as equivalent to executing that close operation.
The same split appears without operating-system handles. A transaction object may occupy little memory while representing locks, snapshots, or pending state in a database. Dropping the last reference to the client-side object does not define a database commit. At most, a client library or runtime may arrange some fallback cleanup behavior, subject to its documented contract.
Finalization has a different contract from deterministic release
Some managed runtimes provide finalizers, cleaners, destructors, or related mechanisms that can run after an object becomes unreachable. These mechanisms can be useful as a defensive fallback, but their semantics must be read precisely.
A finalizer does not turn reachability into a deterministic resource lifetime unless the runtime explicitly guarantees the timing and behavior required by that resource. Many tracing runtimes intentionally leave collection timing unspecified or adaptive. An unreachable object can therefore remain uncollected for an interval that application code cannot use as a precise release deadline.
Even when finalization eventually runs, it introduces another semantic question: what operations are valid from that context? Runtime documentation may restrict ordering, thread affinity, resurrection behavior, exception handling, or access to other objects during finalization. External APIs can impose their own constraints as well.
Deterministic release has a simpler shape. The program reaches a defined boundary and performs the release operation there. Language constructs such as scoped resource blocks, defer-style cleanup, or explicit close calls differ syntactically, but they encode the same useful fact: resource lifetime is attached to control flow rather than to a future collection event.
This does not make explicit cleanup infallible. Code can omit it, cleanup can return an error, and abrupt process termination can bypass in-process logic. The point is narrower: an explicit release boundary states the intended lifetime in program semantics instead of delegating it to memory reclamation.
Memory pressure and resource pressure are different signals
Garbage collectors are generally driven by managed-memory conditions and runtime heuristics. External resource exhaustion can develop on a different axis.
A program can hold many small wrapper objects, each owning an expensive external resource. The wrappers may consume too little managed memory to create strong collection pressure, while the process approaches an operating-system descriptor limit or a remote service’s connection limit. Conversely, a large managed heap can trigger collection even when few external resources exist.
This mismatch means that resource scarcity cannot safely be inferred from heap pressure. The collector has no general metric that maps a kilobyte of wrapper memory to the cost of a socket, transaction, GPU allocation, temporary file, or remote lease.
The consequence is observable without a performance claim: two programs with the same reachable heap size can hold radically different amounts of external state. A memory-oriented reclamation policy therefore lacks enough information to define the correct release point for that state.
Cleanup errors belong to the resource protocol
External release operations can fail or produce information that matters to the caller. A buffered writer may encounter an error while flushing. A transaction rollback can report a transport failure. A close operation may have API-specific error semantics.
If cleanup is deferred to a finalization mechanism with no ordinary caller awaiting the result, there may be no suitable place to propagate such an error through the operation that owned the resource. Logging from a cleanup hook is not equivalent to returning an error to the code responsible for the resource’s work.
This is one reason resource lifetime belongs near the operation that establishes ownership. The code that acquires a resource also has the context to decide what release means, whether errors matter, and which outcome should be visible at the boundary.
The rule is not that every cleanup error must override every earlier error. That choice is API-specific. The stronger statement is that deterministic cleanup preserves a place in normal control flow where the choice can be made.
Process exit is a separate boundary again
Operating systems reclaim many process-owned resources when a process terminates. That behavior is valuable, but it is not a substitute for release during a process’s lifetime.
Process exit can close file descriptors and reclaim virtual memory, yet application protocols can require more specific actions. A database transaction may be rolled back after connection loss according to server behavior. A temporary file can remain in the filesystem unless its removal is separately arranged. A remote lease may persist until an expiry condition. Buffered application data that was never written cannot be recovered merely because the descriptor later closes.
These cases have different contracts, so a single claim such as “the OS cleans it up” discards the distinctions that determine correctness.
Process-level reclamation is best viewed as another containment boundary. It limits some classes of resource leakage after termination. It says little about the intended lifetime of resources while the process continues serving work.
Object lifetime can still help express resource lifetime
Separating memory reclamation from resource release does not make wrapper objects unhelpful. A well-designed wrapper can make ownership visible and concentrate the release protocol in one abstraction.
The important design question is what the wrapper promises. If its API exposes a scoped lifetime or an explicit release operation, callers can align resource ownership with a control-flow boundary. If the wrapper also has a finalization fallback, that fallback can guard against abandonment without becoming the primary lifetime mechanism.
Reference-counted runtimes add a useful variation. When destruction is tied to the reference count reaching zero, cleanup can be more predictable than tracing collection under the runtime’s documented semantics. Yet aliasing still matters: an extra retained reference extends the object’s lifetime, and cycles can alter reclamation behavior in systems that do not collect them automatically. Deterministic resource semantics therefore depend on the language and runtime contract, not on the generic label “garbage collected.”
The boundary to preserve
Managed memory and external resources often meet inside the same object, which makes their lifetimes look unified. They are not. Reachability determines whether managed state remains accessible. Ownership determines which participant must perform an external release operation.
Keeping that boundary explicit produces a sharper model of cleanup. Garbage collection answers when memory may be reclaimed under the runtime’s rules. Resource management answers when an external capability, reservation, or protocol state is no longer needed.
Those answers can coincide in a particular language or abstraction, but coincidence is a property to verify in its contract. It is not a property supplied by garbage collection itself.