Skip to content

VOL 09 / CH 02 / LESSON 02

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 Computes Reachability from Roots ​

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

Mark-sweep marks live objects and reclaims unmarked storage. Mark-compact also moves live objects to reduce fragmentation; copying collectors move survivors into another space. A collector may combine these techniques and run some phases concurrently with the application. Which phases pause it depends on the implementation.

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. A young-generation collection must also account for old objects pointing to young ones. Write barriers and card tables help maintain remembered sets for these references; scanning only young objects would miss them.

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. Raising the maximum heap does not automatically enlarge the live set or remove retention paths. It may delay a capacity failure and reduce the container’s room for non-heap memory. Observe both reachable data and total memory use.

Reference counting: Putting cost on reference updates ​

In the basic reference-counting model, strong-reference changes update a count, and reaching zero triggers reclamation. An implementation may defer updates or reclamation. 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, both counts remain positive even though neither object is reachable from external roots.

CPython combines reference counting with cyclic garbage collection; other Python implementations may use different strategies. Swift ARC and Rust Rc can use weak references to break ownership cycles. 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 ​

Rust ownership schedules destruction: leaving an owner’s scope normally runs drop glue and any applicable Drop implementation. The borrow checker prevents references from outliving the values they borrow and restricts mutable aliasing. These rules prevent use-after-free in safe Rust without requiring a tracing garbage collector.

Destruction is not unconditional: mem::forget, process exit, or abort can skip it. 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, listeners that were never unregistered, 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.

This CPython 3 example makes collection explicit instead of guessing its timing. Save it as gc_demo.py and run python3 gc_demo.py. It temporarily disables automatic cyclic collection and requests collection directly:

python
import gc
import weakref

class Node:
    pass

was_enabled = gc.isenabled()
gc.disable()
try:
    node = Node()
    node.link = node
    cache = {"held": node}
    observed = weakref.ref(node)
    del node
    gc.collect()
    print(observed() is not None)
    cache.clear()
    gc.collect()
    print(observed() is None)
finally:
    if was_enabled:
        gc.enable()

Both output lines are True. The cache retains the object through the first collection. After the cache is cleared, only an unreachable self-reference cycle remains, and the next collection reclaims it. The weak reference does not extend the lifetime. The collector did its job; a cache still holding the object needs a different fix.

Non-Memory Resources Need Explicit Cleanup ​

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. Their timing is unpredictable, and they may never run 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