List mutation and garbage collection

Summary

A developer questioned whether the private List<String> field in CircularNavigatorString—populated via List.copyOf()—was being mutated in memory. The concern was whether such mutation would generate garbage, requiring garbage collection (GC). The answer is no: the list is immutable, the index update is a primitive integer mutation on the heap, and no GC overhead occurs beyond standard reference management.

Root Cause

The confusion arises from a misunderstanding of Java’s memory model and the List.copyOf() contract.

  • Immutability: List.copyOf(items) returns a secure, unmodifiable list. You cannot add, remove, or replace elements. The object graph is immutable.
  • State vs. Data: The class maintains two distinct pieces of state:
    1. items: An immutable reference to the data structure (constant memory footprint).
    2. index: A primitive int (32-bit) stored directly in the object header on the heap.
  • Mutation Scope: The “mutation” occurs only in the index variable. When you call next() or previous(), you are calculating a new integer and assigning it to this.index. This is a standard heap-write operation, identical to updating a counter.

Why This Happens in Real Systems

This pattern is common in flyweight navigators or circular buffers where the underlying dataset is static (read-only), but the view position is dynamic.

  • Read-Heavy Workloads: Systems often cache reference data (e.g., country codes, configuration lists) as immutable snapshots to avoid synchronization overhead.
  • Stateful Sessions: In a web request or user session, a single navigator instance tracks the “current” position for that specific user.
  • Memory Efficiency: Storing the list once (static or immutable) and tracking the view with a primitive integer is the most memory-efficient way to implement cyclic iteration. Creating new list copies for every position change would be a memory leak.

Real-World Impact

  • GC Pressure: Zero impact. The items list is referenced once and held for the life of the CircularNavigatorString instance. The index updates do not create new objects, so there is no allocation rate increase or GC churn.
  • Thread Safety: This implementation is not thread-safe. If multiple threads access next() or previous() concurrently, the index integer might update unpredictably (race condition), but it will not corrupt the items list.
  • Memory Leaks: There is no risk of memory leaks here. The object graph is simple: one instance reference holding one immutable list reference.

Example or Code

No complex code is needed to visualize the memory layout. The index is a primitive field stored inline in the object.

public String next() {
    // This calculation happens in the CPU register or stack,
    // then the result is written to the heap where 'this.index' lives.
    index = (index + 1) % items.size();
    return current();
}

How Senior Engineers Fix It

Senior engineers focus on thread safety and access patterns rather than GC concerns for this specific code.

  1. Concurrency Strategy:

    • Single Threaded: Keep as is.
    • Multi-Threaded Read-Only: Keep as is if threads have their own navigator instances.
    • Multi-Threaded Shared: If multiple threads share one navigator instance, replace int index with AtomicInteger or make the methods synchronized. However, for navigation, usually, each thread needs its own state, so creating new navigator instances is preferred over sharing.
  2. Memory Optimization:

    • Ensure List.copyOf() is appropriate. If the source list is massive and only a subset is needed, a Stream or pagination might be better.
    • Ensure the items list is truly immutable. If the input list contains mutable objects, List.copyOf() only freezes the list structure, not the contents.
  3. API Design:

    • Return defensive copies or views. The current items() method returns the immutable list directly, which is safe but exposes the internal structure. If the caller casts the list and tries to modify it (via raw types), they will get an exception. This is acceptable behavior.

Why Juniors Miss It

  1. Confusing State with Data: Junior developers often conflate the collection (the data) with the iterator (the state). They see “mutation” and immediately think “garbage creation.”
  2. Reference vs. Value: The difference between mutating a reference (pointer) and mutating the object it points to is a subtle concept in Java. Updating index changes a value in the object header; it does not touch the array backing the List.
  3. GC Anxiety: There is a common misconception that any write operation to the heap triggers garbage collection. In reality, GC is triggered by allocation and retention of objects. Updating an existing field (index) is not allocation.