Skip to content

2.2 Memory Management: Tracing GC, Reference Counting, and Ownership

Memory management strategies address two key questions: when an object becomes unreachable or no longer owned, and who, when, and at what cost bears the responsibility for its reclamation. Automatic management reduces the risk of dangling pointers but does not eliminate resource leaks, latency spikes, or lifecycle errors.

Tracing GC from Root Calculation of Reachability

text
roots: stacks, static fields, runtime handles
  → trace object graph
  → reachable live set
  → reclaim unreachable objects

Mark-sweep marks and sweeps free blocks; mark-compact moves live objects to reduce fragmentation; copying collectors copy live objects into a separate space. Real-world garbage collectors combine these phases and execute them concurrently.

The generational hypothesis observes that most objects have short lifespans, so young-generation collections occur frequently and in small scopes, while long-lived objects are promoted. Cross-generation references require write barriers or card tables (memory sets) to track references from young to old generations, and cannot simply scan the young generation and ignore references pointing to old objects.

Pause, Throughput, and Footprint Are Interdependent

Concurrent GC overlaps some work with application threads, reducing pause time, but at the cost of CPU usage, requiring barriers, and potentially demanding a larger heap. Low-latency collectors still introduce safepoints, root scanning, and degradation scenarios; they do not guarantee "zero pause."

When tuning, focus on: allocation rate, live object set size, promotion rates, pause distribution, concurrent cycle efficiency, heap headroom, and actual container memory. Simply increasing the maximum heap size may delay issues, but it can also expand the live set scanned during GC and increase the radius of OOM failures.

Reference counting: Putting cost on reference updates

Every time a strong reference is added or removed, the reference count is updated, and the object is immediately freed when the count reaches zero. The advantage is predictable garbage collection timing. However, drawbacks include update overhead, atomic operations in multithreaded environments, and cycles: if A references B and B references A, the object remains reachable even if no external references exist, so the count does not drop to zero.

Implementations in languages like Python use cycle detectors to address this. Swift and Rust Rc typically break ownership cycles using weak references. Not all reference counting systems are "pause-free"; cycle detection and cascading destruction can still generate bursts of work.

Ownership: Putting Responsibility into Static Rules

Every value in Rust has an owner, and when that owner goes out of scope, the Drop trait is automatically invoked. The borrow checker ensures that references do not outlive the values they point to and enforces that only one mutable reference can exist at a time. This mechanism guarantees memory safety for ordinary code without requiring garbage collection or tracing mechanisms, no risk of use-after-free bugs.

That said, Rc cycles can still leak, unsafe/FFI interactions still require manual verification, and objects within an arena may persist until the entire region is freed. Ownership does not mean "everything must live on the stack"; Box, Vec, and Arc all rely on the heap for dynamic allocation.

GC Can Also Leak

As long as an object remains reachable from a root, the garbage collector cannot determine that the application no longer needs it. Unbounded caches, unregistered listeners, ThreadLocal variables, ClassLoaders, backlog queues, and erroneous metric labels can all keep objects alive.

Investigate the difference between allocation hotspots and retention paths. The dominator tree in a heap dump reveals which objects retain the most memory; simply identifying the "largest class" by count often misses the actual root causes.

Non-heap Resources Require Explicit Scope

Files, sockets, database transactions, and locks cannot rely on garbage collection. Use RAII, try-with-resources, context managers, or defer to tie resource release to lexical scope, and define clear paths for exception or cancellation handling.

Finalizers and Cleaners can only serve as fallbacks, they are invoked at unpredictable times and may not run at all before process termination. Under high load, depending on finalization to close connections can exhaust file descriptors prematurely.

The next lesson addresses another layer of the "memory model" in multithreaded contexts: when a write by one thread must be observed by another.

References

Built with VitePress | Software Systems Atlas