← Back to list
Computing & Embedded
#가비지컬렉션#메모리관리#세대별GC#STW#ZGC
Last updated · 2026-10-12

Garbage Collection

1. Overview

A. Definition

Garbage collection (GC) is an automatic memory management technique in which the runtime automatically identifies and reclaims heap objects that the program can no longer reach (unreachable), so that developers do not have to free memory explicitly.

The fundamental reason GC emerged lies in the dangers of manual memory management. In environments like C/C++ where the developer is responsible for allocation and deallocation via malloc/free and new/delete, forgetting to free produces accumulating memory leaks that slowly kill long-running servers, while reusing an already-freed region causes dangling pointers and double frees, leading to undefined behavior and security vulnerabilities (Use-After-Free). Such bugs are hard to reproduce and enormously costly to debug. GC was conceived to move the "responsibility for tracking object lifetimes" from humans to the runtime, raising productivity and stability at the same time.

B. Background and Necessity

The history of GC begins in 1959 when John McCarthy first introduced it while designing LISP to handle cyclic references in list structures. Since then most modern mainstream languages—Smalltalk, Java, C#, Go, JavaScript, Python—have built in GC, and today the majority of server, mobile, and web applications run on top of GC. Behind this is the explosive growth in application scale and lifespan. In large systems where hundreds of libraries and millions of objects are entangled, it is practically impossible for humans to track the ownership of every object, and on servers that run uninterrupted for months even a small leak can be fatal.

That said, GC is not "free." The price of automation is that reclamation work intrudes at run time, during which Stop-The-World (STW) pauses that halt application threads occur, affecting latency and throughput. The history of modern GC technology is therefore a ceaseless response to the question of "how to shrink and hide pause time while preserving the correctness of reclamation." In financial trading systems or interactive services where responsiveness matters, even tens of milliseconds of pause can breach an SLA, so GC selection and tuning become important design decisions from a professional-engineer standpoint.

Moreover, GC is not merely a "function that empties memory"; it is a subsystem tightly interlocked with the allocator, the runtime, and the language design. Which allocation strategy is used (bump pointer vs. segregated free lists), whether object movement is allowed, and whether metadata is embedded in pointers all change which GC algorithms are feasible. To truly understand GC, then, is to possess a mental framework for memory management as a whole, spanning memory allocation, object layout, and runtime barriers.

C. Characteristics of Garbage Collection

GC has several essential characteristics. First, it is reachability-based. "Garbage" is defined not as "objects that will not be used in the future" but as "objects that cannot be reached from the roots along a reference path"; GC does not predict the future and safely determines reclamation targets from the current reference graph alone. Second, approximate conservatism: reachable objects are never reclaimed (safety), but unreachable objects are not always reclaimed immediately (delay is permitted). Third, runtime intrusiveness: when allocation pressure exceeds a threshold, GC intervenes asynchronously in the middle of application flow, making the performance profile discontinuous. Fourth, exploitation of generations and locality, actively leveraging the empirical hypothesis that most objects die quickly (the weak generational hypothesis).

These characteristics mean that GC is not "magic that lets you ignore memory once you have it," but a resource-management subsystem with a clear cost and behavior model. What matters from a professional-engineer standpoint is not whether GC exists, but the ability to quantitatively understand and control when, how much, and in what manner it intervenes to affect application latency and throughput. Below we examine, in turn, the operating principles of GC, modern ultra-low-latency collectors, and the perspective of operating and tuning them in practice.

2. Basic Principles and Overall Structure of Garbage Collection

The starting point of GC is the root set. Roots are "references the program holds directly in hand," such as local variables on thread stacks, CPU registers, and global/static variables. GC starts from these roots, follows references to mark every reachable object, and treats the unmarked remainder as garbage. The structure diagram below shows the whole picture, viewing the heap as a reference graph.

flowchart LR
    subgraph Roots["Root Set"]
        S["Stack locals"]
        G["Global/static vars"]
        R["Registers"]
    end
    S --> A["Object A(reachable)"]
    G --> B["Object B(reachable)"]
    A --> C["Object C(reachable)"]
    B --> C
    D["Object D(garbage)"] --> E["Object E(garbage)"]
    E -.->|"cyclic reference"| D
    style D fill:#f8d7da
    style E fill:#f8d7da

Pinning down the root set precisely is trickier than it looks. The compiler must supply a stack map that tells, at each execution point, which stack slots and registers actually hold object references, so that GC can perform "precise" collection. In environments lacking such information (e.g., conservatively scanning a native stack), conservative GC is used, which can be imprecise by mistaking integer values that look like pointers for references and keeping dead objects alive. Managed runtimes such as Java and C# adopt precise GC, while some GCs for C/C++ (the Boehm GC) adopt the conservative approach.

In the figure above, D and E reference each other but cannot be reached from the roots, so both are garbage. This example shows why cyclic references matter. A scheme that merely counts "the number of references pointing at me" (reference counting) never drives D and E's counts to zero because they point at each other, so they are never reclaimed; a reachability-based tracing scheme, however, looks only at connectivity from the roots and thus filters out this cyclic garbage precisely.

GC algorithms divide broadly into reference-counting and tracing approaches. The two differ in philosophy on "when and how garbage is determined."

  • Reference counting: Determined the instant a reference is dropped. Latency is flat, but it misses cyclic references and carries a constant update cost.
  • Tracing: Determined by periodically sweeping the graph from the roots. It reclaims cycles precisely but concentrates pauses and load at collection time.

Practical runtimes often mix the two. CPython, for instance, is reference-counting by default but also runs a tracing auxiliary collector for cyclic garbage. The procedure diagram below shows, step by step, the operation of Mark-Sweep, the most representative tracing approach.

flowchart TD
    Start["Allocation failure/threshold reached"] --> STW["STW: halt application threads"]
    STW --> Mark["Mark phase: traverse graph from roots, mark live objects"]
    Mark --> Sweep["Sweep phase: reclaim unmarked objects to free list"]
    Sweep --> Compact{"Fragmentation severe?"}
    Compact -->|"Yes"| Comp["Compact: pack live objects to one side"]
    Compact -->|"No"| Resume["Resume application"]
    Comp --> Resume

In the Mark phase, the graph is traversed depth-/breadth-first from the roots, turning on a mark bit for each reached object. This traversal cost is proportional to the number of live objects and reference density, so the more live objects a heap holds, the more expensive marking becomes. In the Sweep phase, the entire heap is scanned, unmarked objects are returned to free memory, and mark bits are reset for the next collection. Sweep cost is proportional to total heap size. This approach has the advantage of reclaiming cyclic references precisely, but after reclamation external fragmentation—free space scattered into fragments— accumulates. Hence Mark-Sweep-Compact packs live objects to one side of memory after sweeping to secure contiguous free space. Compaction eliminates fragmentation and enables bump-pointer allocation, which completes subsequent allocation by merely incrementing a pointer, but it incurs the cost of updating references due to object movement.

What to remember here is the fact that GC's pause length is not necessarily proportional to heap size. In early collectors, the larger the heap, the longer the full STW time, so on hundreds-of-gigabytes heaps a single Full GC could reach several seconds, effectively stopping the service. Modern concurrent and region-based collectors make breaking this proportionality their core goal, and this is precisely the fundamental motivation for the algorithmic evolution examined in the following sections.

3. Core Algorithms — Reference Counting, Copying, and Generational Collection

A. Reference Counting

Reference counting keeps, in each object's header, "the number of references pointing at me," incrementing by 1 when a reference is created and decrementing by 1 when one is dropped, reclaiming the instant the count reaches 0. It differs fundamentally in philosophy from tracing in that no separate collector thread sweeps the whole heap; instead, the reclamation logic is distributed across the everyday operation of reference assignment. CPython and Swift (ARC, Automatic Reference Counting) are representative.

Its biggest advantage is immediacy and predictability. Because reclamation happens at the very point where garbage arises, memory is returned promptly, and a full STW pause is in principle unnecessary, making the latency distribution flat. Reclamation cost is also finely scattered across the program, easing the "performance cliff" characteristic of tracing where the collector works in bursts. For this reason, ARC is preferred on mobile (iOS/Swift), where responsiveness matters.

However, reference counting has clear structural weaknesses. First, as seen earlier, it cannot reclaim cyclic references on its own. Two objects pointing at each other never fall below a count of 1 and remain forever, so CPython provides a periodically running cyclic collector and Swift provides developer-specified weak/unowned references as remedies. Second, every reference assignment must update the count, so a constant overhead is layered in, and in multithreading this update must be handled atomically, raising cache-line contention cost. In fact, one reason CPython long retained the GIL (Global Interpreter Lock) is precisely this thread-safety problem of reference counting, and it is also why the recent free-threaded CPython experiment is difficult. Third, when the last reference to a large object graph is dropped, a cascade of many destructions can occur at once, producing a momentary latency spike.

B. Copying Collection (Scavenging)

Copying collection divides the heap into two semi-spaces, From and To, and on collection copies and compacts only the live objects of From into To, then empties all of From at once. During copying it leaves a forwarding pointer at each live object's original location pointing to its new location, so that the multiple references pointing at the same object are all updated consistently to the new address.

The decisive strength of this approach is that cost is proportional only to the number of live objects. Dead objects are not touched at all and the From space is discarded wholesale, so collection time does not grow no matter how much garbage there is. In other words, it has the "faster with more garbage" property and is overwhelmingly favorable in regions with low survival rates. Compaction also completes together with copying, so external fragmentation is eliminated at the source, and because free space is always contiguous, bump-pointer allocation that finishes with a mere pointer increment is possible. A side effect is that objects gather in allocation order, improving reference locality.

On the other hand, the space waste of being able to use only half of available memory is an essential weakness, because one semi-space must always be kept empty. For this reason, applying copying collection to the whole heap is rare; it is generally applied locally only to the "young object region," where low survival rates make its benefit large. The actual structure in which the JVM's Young generation is split into Eden and two Survivor spaces, copying survivors of Eden plus one Survivor into the other Survivor, is precisely a variant of this copying collection—a design that mitigates semi-space waste by adjusting Survivor ratios.

C. Generational Collection (Generational GC)

Generational collection is based on the weak generational hypothesis that "most objects die soon after creation, while objects that have survived a long time will go on living a long time." The heap is divided into Young (Eden+Survivor) and Old (Tenured) generations, and all allocation happens in Eden. The table below summarizes the core of the generational strategy.

Category Young generation (Minor GC) Old generation (Major/Full GC)
Target objects Freshly created short-lived objects Long-lived objects promoted after surviving repeatedly
Collection frequency Very frequent Rare
Main algorithm Copying collection Mark-sweep-compact
Collection cost Short (few live objects) Long (large region, STW possible)
Effect Bulk reclamation with short pauses Cleans up long-lived leaks and fragmentation

Because the Young generation has a low survival rate, it is swept quickly by copying collection, and objects that survive beyond a certain number of times are promoted to Old. The core challenge is inter-generational references. If an Old object points to a Young object, that Old object must also be treated like a root when collecting only Young. To handle this, a write barrier records inter-generational references in a Remembered Set or card table, so that at Minor GC time references are followed without omission without scanning all of Old. Thanks to this structure, generational GC reclaims most garbage with short pauses without rummaging through the entire heap.

As an actual case, a web request-handling server creates and discards numerous temporary DTO and string objects per request, most of which are typical short-lived objects that die right after the response. In such a workload generational GC empties Eden with a Minor GC within a few milliseconds, and it is common for over 90% of all allocated objects to be reclaimed at the first Minor GC by measurement. Conversely, long-lived objects such as sessions and caches gather in Old, so an infrequent Major GC cleans them up all at once. Separating objects by lifetime like this to combine "frequent and cheap" with "occasional and expensive" is the design philosophy of generational GC.

D. Tri-color Marking and the Consistency of Concurrent Marking

To understand concurrent marking, the tri-color marking abstraction is essential. Every object is classified as white (not yet found, a reclamation candidate), gray (found but child references not yet explored), or black (fully explored including children), and until gray is exhausted the process advances by "coloring gray black while moving its children from white to gray." When marking ends, objects remaining white are garbage.

The problem arises when marking and the application run concurrently. If an already-blackened object is changed to reference a new white object and the gray path that pointed at that white disappears, a fatal error occurs in which a live object is mistaken for garbage and reclaimed. To prevent this, GC intercepts reference changes with a write barrier to maintain the invariant. Representative strategies are incremental update, which grays the new reference being changed, and SATB (Snapshot-At-The-Beginning), which preserves the old reference being dropped; G1 adopts SATB. Thanks to this mechanism, concurrent collectors such as ZGC and Shenandoah can complete marking safely while barely stopping the application.

4. Modern GC That Reduces Pause Time — Concurrent, Parallel, and Region-based

Whereas early GC was fully STW, modern GC either collects concurrently with the application or chops work into fine pieces to spread out pauses. Comparing the major collectors centered on the Java ecosystem gives the following.

Collector Strategy Target pause Features
Serial GC Single-threaded STW Relatively long Small heaps, single-CPU environments
Parallel GC Multi-threaded STW Long (throughput-first) Batch, throughput optimization
CMS (deprecated) Concurrent mark-sweep Medium Fragmentation, concurrent-mode-failure issues
G1 GC Region-based Predictive (hundreds of ms → tens of ms) Splits heap into regions, garbage-heavy regions first
ZGC / Shenandoah Concurrent compaction Aiming below 1ms Load/colored pointers, ultra-low latency on large heaps

G1 (Garbage First) finely divides the heap into equal-sized regions, tracks each region's garbage ratio, and collects from the regions with the highest reclamation efficiency first (garbage-first), thereby trying to predictively keep the user-specified target pause time. ZGC and Shenandoah go a step further, performing even the compaction that moves objects concurrently with the application, binding pauses to around 1ms regardless of heap size. The core technologies that make this possible are the load barrier and the colored pointer, which embeds state bits in pointers, transparently remapping to the latest address when the application reads an object that is being moved.

Operators must be aware that failure modes exist even for concurrent, region-based GC. Because the application keeps allocating even while concurrent collection proceeds, if collection speed cannot keep up with allocation speed, the Old space fills first, producing a concurrent mode failure or promotion failure. At that point the runtime falls back to a full STW Full GC as a last resort, and this very moment is observed as the longest pause in an ultra-low-latency service. Therefore the core of concurrent GC tuning is not "making pauses zero" but adjusting heap headroom and the collection-start threshold so the collector does not fall behind allocation pressure. In practice the frequency of this failure event is taken from the GC log as a key metric for adjusting heap size and collector parameters.

A concept that must be pointed out here is STW and the safepoint. For GC to see the reference graph consistently, all threads must briefly halt at "a point where the heap state is consistent," and this point is the safepoint. Even concurrent GC brings all threads to a safepoint for brief moments such as the start and end of marking, so the accurate statement is not "there is no STW at all" but "STW is very short and not proportional to heap size." Also, logical leaks that arise from not dropping references to long-lived objects (e.g., objects remain registered in caches/listeners so GC cannot reclaim them) are problems that occur even with GC, clearly showing the limitation that GC reclaims only "unreachable," not "useless."

5. Advanced Topics — Recent Trends and Practical Application Cases

GC technology is evolving rapidly along the axes of ultra-low latency, large capacity, and observability. In the Java camp, ZGC has been extended to generational form (Generational ZGC, official in JDK 21) to capture low latency and throughput together, and Shenandoah has also introduced a generational mode. This is a trend to ease the old conventional wisdom that "concurrent GC sacrifices throughput" by combining it with the weak generational hypothesis. Meanwhile, Go uses a concurrent mark-sweep GC designed with low latency as the top priority, giving up compaction and instead suppressing fragmentation with a size-classed allocator. Go's abandonment of compaction well illustrates that "every choice has a price." Without compaction, pauses are short, but it must accept fragmentation and allocation-path cost, which it fills with allocator design. In the case of .NET, server GC, workstation GC, and background GC can be switched to suit the situation, so GC behavior can be changed according to throughput/latency goals even within the same runtime. Recently a GC pacer that adjusts collection timing based on allocation rate, and a soft memory limit that reacts to actual hardware memory pressure, have been introduced, responding to the practical need to link GC with the memory limits of cloud containers.

As a practical application case, consider a JVM container in a Kubernetes environment. In the past the JVM did not recognize the container's cgroup memory limit and sized the heap based on total host memory, triggering OOM Kills; the latest JVM, with container awareness (UseContainerSupport), reads the cgroup limit and sets the heap as a ratio (MaxRAMPercentage). Services where tail latency matters, such as interactive API gateways and payment servers, have many reported cases of switching from G1 to ZGC to lower P99 pauses from hundreds of ms to single-digit ms. Conversely, when total throughput is key, as in nightly batch and data pipelines, Parallel GC is still advantageous. Thus there is no "correct GC"; the key is selection matched to the workload's latency, throughput, and memory characteristics, and measurement-based tuning.

Observability of GC operations has also become important. Collecting GC logs to monitor allocation rate, Minor/Major frequency, promotion rate, and pause distribution (P99/P99.9) and detecting promotion failures, concurrent mode failures, and frequent Full GCs early have become basics of SRE practice. For example, if the tail of response latency in some service suddenly lengthens, even without application code changes the cause is often that a surge in allocation rate made Minor GC excessively frequent, or a memory leak filled Old so Full GC repeats. Combining GC logs with heap dump analysis (e.g., tracking large-object holders via the dominator tree) can pinpoint the cause precisely, so GC visibility serves as a first-line diagnostic tool for responding to performance incidents.

6. Considerations and Implications

From a professional-engineer standpoint, GC must be treated as an architectural decision that governs system performance, cost, and stability, beyond mere memory automation.

  • Latency vs. throughput trade-off: Concurrent, region-based GC reduces pauses but consumes CPU/throughput through write/load barriers and extra threads. The nature of the SLA (average throughput or tail latency) must be defined first before selecting a collector, and the choice must always be validated with load testing and GC log analysis.
  • Balance of heap size and memory cost: GC is more efficient the more free space there is, since frequency drops, but memory is directly tied to cloud cost. Rather than blindly enlarging the heap, a design that lowers allocation rate (object reuse, pooling, streaming) is the fundamental prescription, and in containers especially the cgroup limit and GC must be linked.
  • Logical leaks and design responsibility: Even with GC, leaks occur where objects are bound in global caches, event listeners, or ThreadLocal and not reclaimed. Explicit lifetime management (weak references, explicit release, cache-expiration policies) must be included in the design, and dependence on GC cannot be an exemption from memory design.
  • Linkage with language/runtime selection: In control and high-frequency trading domains where real-time behavior is extremely important, GC pauses themselves are not permitted, so Rust's ownership-based GC-free model or off-heap/object-reuse techniques are chosen. Conversely, for enterprise backends where productivity and stability come first, GC languages are reasonable. GC must be designed together as part of a language/runtime strategy matched to the problem domain and non-functional requirements.
  • The two-sidedness of the security perspective: GC structurally eliminates memory-safety vulnerabilities such as Use-After-Free and double free, greatly reducing the attack surface. On the other hand, the occurrence pattern of GC pauses can be observed as a side channel, and sensitive information (such as cryptographic keys) can be exposed via memory dumps by remaining on the heap until reclamation. Therefore security-sensitive data needs the supplement of being explicitly overwritten with zeros right after use, or managed off-heap.
  • Outlook: The generalization of generational concurrent GC, strengthened awareness of containers/NUMA/large heaps, machine-learning-based automatic tuning of GC parameters, and the coexistence of ownership models (Rust) with automatic GC will remain core trends for the time being. The professional engineer needs to establish in the organization an operating philosophy that views GC not as "something that just works" but as a target for observation, measurement, and tuning.

References


In one line: Garbage collection is a memory-management technique that automatically reclaims garbage objects based on reachability; it has evolved from reference-counting, tracing, copying, and generational strategies into concurrent, region-based ultra-low-latency collectors such as G1 and ZGC, and tuning the trade-off of latency, throughput, and memory cost on a measurement basis is the professional-engineer core.