# Memory & Garbage Collection, Explained

> What actually happens to the objects your code creates: where they live in memory, why some of them have to be cleaned up, and how the two big approaches - freeing memory by hand versus a garbage collector that does it for you - really work.


---

# Memory & Garbage Collection, Explained

You've created plenty of objects. A list here, a user record there, a string you built up in a loop - you wrote `new` or `{}` or just assigned a value and moved on. The thing appeared, did its job, and you never thought about it again. Which is exactly the point: most of the time, you're not *supposed* to think about it.

But "you never think about it" is hiding a real question, and one day it stops hiding. Why does a Java service pause for a fraction of a second under load? Why does a long-running Node process slowly eat more and more memory until it's restarted? Why do C programmers talk about "use-after-free" like it's a horror story? All three are the same topic wearing different clothes: **what happens to the objects you create, and who is responsible for cleaning them up.**

Once you can picture where your objects live and how memory gets reclaimed, these stop being folklore. You'll know why garbage collectors exist, what they're protecting you from, what they cost, and - the part that surprises people - why they don't save you from every kind of memory bug.

## How to read this
- **Want the one big idea?** Read [Phase 3: How Garbage Collection Actually Works](03-how-garbage-collection-works.md) - *reachability* is the concept the whole thing rests on. The first two phases make it land harder.
- **Want it to finally make sense?** Read in order. We start with *where objects live*, then *who cleans them up* (by hand vs. automatically), then *how* the automatic version actually does it.

## The phases
1. **[Where Objects Live & How They're Allocated](01-where-objects-live.md)** - a quick recap of the stack vs. the heap, then a focus on the heap: where things that outlive a function go, and the hard question it raises - when is it actually *safe* to reclaim a piece of memory?
2. **[Manual vs Automatic Memory](02-manual-vs-automatic.md)** - the two worlds. In C and C++ you free memory by hand (total control, sharp edges like leaks and use-after-free). In Java, Go, Python, and JavaScript a garbage collector frees it for you (safety, at a cost). The trade-off, and where Rust fits as a third way.
3. **[How Garbage Collection Actually Works](03-how-garbage-collection-works.md)** - the core idea of *reachability*, mark-and-sweep at a gentle level, why a garbage-collected program can hiccup, and the uncomfortable truth that memory leaks still happen in these languages - plus the classic cause and its cure.

> This guide is about the *concepts* - the mental model that makes every garbage-collected language readable. Deep, language-specific tuning (generational GC details, choosing a collector, sizing heaps, reading GC logs) is real and worth learning, but it belongs in a per-language follow-up rather than here. For the layers underneath this one, see [What Actually Happens When Your Code Runs](/guides/what-happens-when-code-runs) and [Processes, Memory & the CPU](/guides/processes-memory-and-cpu).


---

# Where Objects Live & How They're Allocated

Before we can talk about cleaning up memory, we have to be clear about where the stuff *is*: every value you create sits somewhere in memory, in one of two neighborhoods with completely different rules - one cleans up after itself automatically, and one does not. That second one is the whole reason garbage collection exists.

> ⏭️ This phase recaps an idea covered in full in [What Actually Happens When Your Code Runs](/guides/what-happens-when-code-runs). If the stack and heap are already solid for you, skim the recap and slow down at *"The hard part."*

## The two-minute recap: stack and heap

**What they actually are.** When your program runs, the operating system hands it a chunk of memory to work in. Your program organizes that chunk into (among other things) two regions that behave very differently:

- **The stack** is a tidy stack of plates. Every time you call a function, a new plate (a *stack frame*) goes on top, holding that function's local variables. When the function returns, its plate comes off - instantly, automatically. The stack is fast and self-cleaning, but it only works for things whose size is known up front and whose life ends when the function does.
- **The heap** is a big open warehouse. You can ask for a space of any size, at any time, and it stays yours until *something* decides to give it back. Nothing comes off automatically when a function returns. The heap is flexible, but that flexibility is exactly what makes cleanup hard.

```mermaid
flowchart LR
  subgraph Stack["STACK (auto)"]
    G["greet()<br/>name = •"]
    M["main()"]
  end
  subgraph Heap["HEAP (until reclaimed)"]
    S["'Sam'"]
    U["User record"]
    L["big list"]
  end
  G -->|pointer| S
```

The arrow is the key detail. On the stack, the variable `name` doesn't usually hold the string "Sam" itself - it holds the *address* of where "Sam" lives on the heap. That address is called a **pointer** (or a *reference*, in higher-level languages).

📝 **Terminology.** A *pointer* / *reference* is a value that holds the memory address of something else. The variable is on the stack; what it points *to* is often on the heap. This is why two variables can refer to the *same* object - they hold the same address.

## Why anything goes on the heap at all

If the stack is so fast and automatic, why not put everything there? Because the stack's great strength is also its limit: **a stack frame dies the moment its function returns.** That's perfect for a temporary counter, useless for anything that needs to *outlive* the function that created it.

Consider building something and handing it back:

```python runnable
def make_user(name):
    user = {"name": name, "logins": 0}   # build a dictionary
    return user                          # hand it back to the caller

u = make_user("Sam")
print(u["name"])    # Sam - still alive long after make_user returned
```
*What just happened:* The dictionary couldn't live on `make_user`'s stack frame, because that frame is gone the instant `make_user` returns. So the object itself lives on the **heap**, and what `make_user` returns is really a reference to it. The variable `u` now holds that reference. The object outlived the function that made it - and that is precisely what the heap is *for*.

This is the everyday pattern behind almost every object you create in a high-level language. Anything you build and pass around - lists, records, strings you grow, objects you store in other objects - lives on the heap, because its lifetime isn't tied to a single function call.

💡 **Key point.** The stack is for values that live and die inside one function call. The heap is for everything that has to outlive the call that created it. Heap memory is the interesting case because *something* has to decide when it's no longer needed.

## The hard part: when is it safe to reclaim?

Here's the problem the rest of this guide exists to solve. A piece of heap memory should be given back - *reclaimed* - once nobody needs it anymore, so the space can be reused. The question sounds trivial and is anything but: **how do you know nobody needs it anymore?**

Reclaim too early and you get a disaster. Suppose two variables point at the same heap object, you free it because one of them is done, and the other variable still thinks it's valid:

```mermaid
flowchart LR
  a[a] --> Obj[heap object]
  b[b] --> Obj
  Obj -->|freed via a| Freed[freed memory]
  b -.->|dangling!| Freed
```

Now `b` is a **dangling pointer** - it points at memory that's been freed and possibly handed to something else entirely. Reading through `b` gives you garbage; writing through it corrupts another part of your program. This is the famous **use-after-free** bug, and it's both nasty to debug and a serious security hole.

Reclaim too *late* - or never - and you get the opposite failure: memory that's no longer needed but never given back. The program's heap usage only grows. That's a **memory leak**, and in a long-running program it ends in the crawl-then-crash you may have read about in [What "Out of Memory" Really Means](/guides/processes-memory-and-cpu).

📝 **Terminology.** *Use-after-free* = using memory through a pointer after that memory has been reclaimed. *Memory leak* = memory that's no longer reachable or needed but never gets reclaimed, so usage grows without bound.

So reclaiming heap memory is a tightrope - too early breaks correctness, too late wastes memory - and getting it *exactly* right requires answering "is anyone still using this?" precisely, every time. That single question is the fork in the road for the next phase. Two whole families of languages exist because they answer it differently: **make the programmer answer it by hand**, or **make the runtime figure it out automatically.**

## Recap

1. Running programs keep values in two main places: the **stack** (fast, automatic, dies when the function returns) and the **heap** (flexible, stays until something reclaims it).
2. A variable on the stack often holds a **pointer/reference** - an address of an object living on the heap. Two variables can hold the same address and point to the same object.
3. Objects go on the **heap** because they need to **outlive** the function that created them.
4. The hard problem is **when to reclaim heap memory**: too early causes **use-after-free** (a dangling pointer), too late or never causes a **memory leak**.
5. How a language answers "is anyone still using this?" - by hand or automatically - is the subject of the next phase.


---

# Manual vs Automatic Memory

We ended the last phase on one precise question: *when is it safe to reclaim a piece of heap memory?* This phase is about the two great answers programming languages have given to it - answers that lead to genuinely different lives as a programmer: different bugs, different performance, different things you have to keep in your head. Neither is "right" - they're a trade-off, and knowing it tells you why your language behaves the way it does.

## World one: manual memory (you are responsible)

**What it actually is.** In languages like C and C++, *you* answer the question. When you need heap memory you ask for it explicitly, and when you're done you hand it back explicitly. The runtime doesn't track who's using what - it does exactly what you tell it, no more.

**What it does in real life.** You allocate, you use, you free. In C the two calls are `malloc` (memory allocate) and `free`:

```c
char *buf = malloc(256);   // ask the heap for 256 bytes; buf points to them
strcpy(buf, "hello");      // use the memory
// ... do work with buf ...
free(buf);                 // hand those 256 bytes back to the heap
buf = NULL;                // good habit: buf no longer points at live memory
```
*What just happened:* You asked the heap for a 256-byte block; `malloc` found a free spot and gave you its address in `buf`. You used it. Then `free(buf)` told the heap "I'm done - reuse this." Setting `buf = NULL` afterward is the disciplined move: it makes sure you can't accidentally use the freed address again.

📝 **Terminology.** *`malloc`* = the C call that allocates a block of heap memory and returns a pointer to it. *`free`* = the C call that returns a previously-allocated block to the heap so it can be reused. (C++ wraps similar machinery in `new`/`delete`, and modern C++ hides much of it behind smart pointers - but the underlying model is the same.)

**The power.** This is as direct as it gets. No hidden bookkeeping, no surprise pauses, no runtime deciding things behind your back - you know exactly when every byte is allocated and freed. For operating systems, game engines, embedded devices, and anything where predictable timing and tight control matter, that directness is the entire point.

**The footguns.** The flip side of "you are responsible" is "you can get it wrong," and the ways to get it wrong are exactly the two failures from Phase 1:

- **Forget to `free`** → the memory is never reclaimed → a **leak**. In a long-running program, every forgotten block adds up until you run out.
- **`free` too early, while something still points at the block** → a **use-after-free**. The dangling pointer reads garbage or corrupts whatever now occupies that memory.
- **`free` the same block twice** (a *double free*) → you corrupt the heap's own bookkeeping, often crashing somewhere far from the real bug.

⚠️ **Gotcha.** The cruelty of these bugs is that they often *don't* crash at the scene of the crime. A use-after-free might work fine in testing and corrupt data only under production load, when the freed memory happens to get reused quickly. The crash lands far from the mistake. This is why manual memory bugs have a reputation for stealing entire days - and why they're a leading source of security vulnerabilities in C and C++ codebases.

## World two: automatic memory (the runtime is responsible)

**What it actually is.** In Java, Go, Python, JavaScript, C#, Ruby, and most modern high-level languages, you *never* hand memory back yourself. You create objects freely; a part of the runtime called the **garbage collector** (GC) periodically figures out which objects are no longer needed and reclaims them for you.

📝 **Terminology.** A *garbage collector* is a component of the language runtime that automatically finds heap objects your program can no longer use ("garbage") and reclaims their memory. "Garbage" has a precise meaning here, which is the whole subject of Phase 3.

**What it does in real life.** Notice what's missing - there's no `free`:

```javascript
function buildReport() {
  const rows = loadRows();        // a big array allocated on the heap
  const summary = summarize(rows);
  return summary;                 // we return summary; rows is now unused
}

const r = buildReport();
// 'rows' is no longer reachable by anyone. We never freed it.
// At some point the garbage collector notices and reclaims it. We don't lift a finger.
```
*What just happened:* `buildReport` allocated a big `rows` array, used it to compute `summary`, and returned only `summary`. The instant `buildReport` returns, nothing in the program can reach `rows` anymore. We wrote no cleanup code. Later, on its own schedule, the garbage collector observes that `rows` is unreachable and reclaims its memory. The "when is it safe to reclaim?" question got answered *for* us.

**The safety.** The two great manual footguns mostly vanish. You can't use-after-free, because the collector won't reclaim an object while anything can still reach it, and you can't double-free, because you don't free at all. For the vast majority of programs - web servers, scripts, apps, data pipelines - this removes an entire category of dangerous, time-eating bugs. That safety is why these languages dominate everyday development.

**The cost.** Nothing is free, and the GC's bill comes due in two ways:

- **Performance and timing.** The collector has to run, and running it takes CPU and sometimes pauses your program briefly (Phase 3 explains why). You give up the fine-grained, predictable control that manual memory gives you.
- **Less control.** You don't decide *exactly* when memory is reclaimed. Usually you don't care. Occasionally - real-time audio, a tight game loop, a latency-sensitive trading system - you care a great deal, and a surprise GC pause is a genuine problem.

## The trade-off, in one plain table

There's no universally better answer here. It's control versus safety, and which one wins depends entirely on what you're building.

```text
                  MANUAL (C, C++)                AUTOMATIC / GC (Java, Go, Python, JS)
   Who frees?     You, explicitly                The runtime's garbage collector
   Control        Total - you know exactly when  Limited - the GC decides when
   Timing         Predictable, no pauses         Occasional GC pauses (usually small)
   Safety         Footguns: leaks, use-after-    Use-after-free / double-free can't
                  free, double-free               happen; leaks are rarer but possible
   Best for       OS kernels, engines, embedded, Apps, servers, scripts, most software
                  anything timing-critical        where dev speed & safety matter most
```

💡 **Key point.** Manual memory trades safety for control; garbage collection trades control for safety. The "right" choice is a property of the *problem*, not a measure of skill. A web team reaching for Go and a kernel team reaching for C are both making the correct call for their constraints.

## A third way: Rust's ownership

It's tempting to think those are the only two options - pay for safety with a garbage collector, or pay for control with footguns. Rust's headline idea is that there's a third path: get memory safety *without* a garbage collector, by having the **compiler** prove at build time when each piece of memory can be freed.

The mechanism is called **ownership**. In Rust, every value has exactly one owner (a variable), and when the owner goes out of scope, the value is freed - automatically, but at a moment the *compiler* determined while compiling, not a moment a runtime collector chooses. The compiler's borrow checker refuses to build code where a reference could outlive the thing it points to, which means use-after-free is caught *before the program ever runs*. No leaks from forgetting to free, no dangling pointers, and no GC pauses - paid for with a stricter compiler that takes time to learn to satisfy.

It's not magic and it's not free; it's a different point on the same trade-off curve. The full story - and where Rust sits among the language families - lives in [Languages, Explained Like a Human](/guides/languages-explained-like-a-human). For our purposes, the takeaway is just this: "manual" and "garbage-collected" aren't the only two boxes.

With the *who* and *why* settled, one question remains: in the automatic world, *how* does the garbage collector actually know which objects are garbage? That's Phase 3.

## Recap

1. The core question - *when is it safe to reclaim memory?* - has two main answers: **you** (manual) or **the runtime** (automatic).
2. **Manual** (C/C++): you call `malloc`/`free` (or `new`/`delete`). Total control and predictable timing, but footguns - leaks, **use-after-free**, double-free - that often crash far from the real bug.
3. **Automatic** (Java/Go/Python/JS): a **garbage collector** reclaims unreachable objects for you. Use-after-free and double-free become impossible; the cost is occasional pauses and less control over timing.
4. The choice is **control vs. safety**, decided by the problem, not by skill.
5. **Rust's ownership** is a third way: memory safety enforced by the **compiler** at build time, with no garbage collector.


---

# How Garbage Collection Actually Works

We've established *that* a garbage collector reclaims unused objects for you. Now the question that makes the whole thing click: how does it know which objects are unused? It can't read your intentions or tell that you're "done" with an object, so it uses a definition that's both simpler and more reliable than intention - and once you have that definition, GC pauses and the surprising persistence of memory leaks both fall right out of it.

## The core idea: reachability

**What it actually is.** The garbage collector doesn't ask "is this object still *needed*?" - it can't know that. It asks a question it *can* answer mechanically: **"can the program still reach this object?"** If there's any chain of references leading from a live variable to the object, it's kept. If no such chain exists, the program has no way to ever touch that object again - so it's **garbage**, and safe to reclaim.

The chains start from a set of always-live places called the **roots**: your currently-running functions' local variables (the stack), global variables, and a few other runtime-held references. The collector starts at the roots and follows every reference, and every reference *those* objects hold, and so on - tracing the whole web of things you can still get to.

📝 **Terminology.** *Reachable* = there exists a chain of references from a root to the object. *Roots* = the starting points the collector trusts as live (local variables on the stack, globals, etc.). *Garbage* = a heap object that is **not** reachable from any root.

```mermaid
flowchart LR
  Roots["ROOTS<br/>(locals, globals)"] --> User --> Address
  Roots --> Cart --> Item1["Item"] --> Item2["Item"]
  Session["old Session<br/>UNREACHABLE → GARBAGE"]
  Buffer["temp Buffer<br/>UNREACHABLE → GARBAGE"]
```

💡 **Key point.** Garbage isn't "stuff you're finished with." It's "stuff you can no longer *reach*." Those are usually the same thing - but the gap between them is exactly where leaks hide, as we'll see at the end.

## Mark-and-sweep, gently

The oldest and most intuitive way to act on reachability is **mark-and-sweep**. Real collectors today are far more elaborate, but almost all of them are sophisticated variations on this same two-step dance, so it's the right thing to picture.

**Step 1 - Mark.** Start at the roots. Follow every reference, marking each object you reach as "in use." Follow the references *those* objects hold, and so on, until you've visited everything reachable. When you're done, every reachable object wears a mark; everything unmarked is, by definition, unreachable.

**Step 2 - Sweep.** Walk through the heap and reclaim every object that *isn't* marked. Those are the unreachable ones - the garbage. Their memory goes back to the pool for reuse. Then clear all the marks, ready for next time.

```mermaid
flowchart LR
  roots["roots"] --> A["A ✓"] --> B["B ✓"]
  roots --> D["D ✓"] --> E["E ✓"]
  C["C (unmarked)<br/>nothing points here → SWEEP"]
```

*MARK: trace from the roots and flag everything reachable (A, B, D, E get a ✓). SWEEP: reclaim everything without a ✓ - here, C - and its memory is free again.*

*What this buys you:* the collector never has to understand your program's logic. It just answers "reachable or not?" mechanically and reclaims the rest. That's why you can create objects with abandon and trust they'll be cleaned up - the cleanup rule doesn't depend on you remembering anything.

## Why a garbage-collected program can hiccup

Here's where the cost from Phase 2 gets concrete. To trace reachability *correctly*, the collector often needs the object graph to hold still while it works - if your code kept rearranging references mid-trace, the collector could miss things or reclaim something live. The simplest way to guarantee a stable snapshot is to briefly **stop your program from running** while the collector does its job. That moment is a **stop-the-world pause** (often shortened to "GC pause").

📝 **Terminology.** A *stop-the-world pause* is a moment when the runtime suspends your application code so the garbage collector can work on a stable view of memory. Your program is frozen for the duration - usually milliseconds, but real.

**Why this matters in real life.** This is the precise reason a Java service can show a latency *spike* under load while its average is fine, or why a game written in a GC language can drop a frame now and then. The program isn't broken and it isn't slow on average - every so often it's paused while the collector runs, and any request that lands during that pause waits it out.

⚠️ **Gotcha.** Don't picture an old "freeze for a full second" collector - modern collectors (Go's, the JVM's G1/ZGC, V8's in Node and browsers) work *hard* to keep pauses tiny, doing much of their work concurrently while your program runs and only stopping the world for brief moments. The pauses are usually small enough to ignore. But "usually small" isn't "never" - and when you're chasing a rare tail-latency spike, "is this a GC pause?" is a question worth asking, not dismissing.

This is also the straight answer to "is a garbage collector slower?" - not in a way most programs notice, but it spends some CPU on collection and introduces those occasional pauses, which is exactly the timing cost we put on the trade-off table in Phase 2.

## The uncomfortable truth: leaks still happen

Here's the part that surprises people, and it's the most useful thing in this guide. A garbage collector frees you from use-after-free and double-free - but it does **not** make memory leaks impossible. Go back to the key point: the collector keeps everything **reachable**. So if your program holds a reference to an object you're truly finished with, the collector sees a live chain to it and dutifully keeps it. Forever, if you let it.

A leak in a GC language isn't "forgot to free." It's **"forgot to let go."** You're keeping a reference, somewhere, to something you no longer need - so it stays reachable, so it never gets collected, so memory creeps up.

**The classic cause: the collection that only grows.** The textbook example is a long-lived container that you keep adding to and never remove from:

```javascript
const cache = {};   // lives for the whole life of the program

function handleRequest(req) {
  // We stash every request's data, keyed by a unique id...
  cache[req.id] = req.payload;
  // ...but we never delete old entries. cache grows forever.
}
```
*What just happened:* Every request adds an entry to `cache`, and nothing ever removes one. Because `cache` is reachable from a root (it's a global-ish long-lived variable) and `cache` references every payload it ever stored, *every payload stays reachable* - so the collector, correctly, never reclaims any of them. The garbage collector is doing its job perfectly. Your program is the one holding on. Memory climbs request after request: a textbook leak, in a "memory-safe" language.

This is the GC-language version of the slow-growth-then-crash you can read about in [What "Out of Memory" Really Means](/guides/processes-memory-and-cpu) - resident memory that climbs and never comes back down even when the app is idle.

🪖 **War story.** A long-running Node service got mysteriously slower and heavier every day until a nightly restart "fixed" it - no crash, no error, just memory creeping up and GC working harder over a bigger and bigger live heap. The cause was a module-level cache, keyed by user session, that nobody ever evicted from - each day's users were, technically, still reachable. The collector was blameless; the code never let go. The fix wasn't "tune the GC," it was bounding the cache (evict old entries / use a size limit). Once you know leaks come from *holding references*, you go looking for the thing that's holding on, not for a flag to flip.

**The cure.** Because the disease is "holding a reference too long," the cures are all variants of "let go":

- **Remove entries** from long-lived collections when you're done with them (`delete cache[id]`, `map.remove(key)`, clear the list).
- **Bound your caches** - cap the size, or use an eviction policy (LRU) so old entries leave on their own.
- **Drop listeners and callbacks** you registered. A forgotten event listener keeps its whole closure - and everything *that* references - alive. (This is the most common leak in front-end JavaScript.)
- **Use weak references** where the language offers them (`WeakMap`/`WeakSet` in JS, weak references in Java/Python) when you want to reference an object *without* keeping it alive on that account.

⚠️ **Gotcha.** The tell for this kind of leak is the same one from the OOM guide: not a big number at a single moment, but **resident memory that climbs and never falls** even when the app is idle. If a long-running process gets heavier over hours or days, suspect a collection or a listener list that only ever grows.

## Recap

1. **Reachability is the whole idea.** The collector keeps every object reachable from a **root** (locals, globals) and treats the rest as **garbage** - not "what you're done with," but "what you can no longer reach."
2. **Mark-and-sweep** is the intuitive mechanism: trace from the roots and mark everything reachable, then sweep away everything unmarked. Modern collectors are clever variations on this.
3. **GC pauses** (stop-the-world) happen because the collector needs a stable view of memory; they're why a GC'd program can hiccup. Modern collectors keep them small, but "small" isn't "zero."
4. **Leaks still happen in GC languages** - not from forgetting to free, but from **forgetting to let go**: a live reference (an ever-growing cache, a forgotten listener) keeps an object reachable, so it's never collected.
5. **The cure is to release references:** remove from long-lived collections, bound caches, drop listeners, reach for weak references. The tell is resident memory that climbs and never comes back down.

You now have the mental model the whole topic rests on. When you create an object, you can picture where it lives (the heap), who's responsible for cleaning it up (you, or the collector), and - in the automatic world - exactly how the collector decides what to keep and why your program might occasionally pause or quietly leak. None of it is folklore anymore.

> **Where next.** This guide is the concept level. The deep, practical layer - reading GC logs, generational collection, choosing and tuning a collector, sizing a heap - is genuinely language-specific and deserves its own follow-up. For the foundations beneath this one, the rest of [What Actually Happens When Your Code Runs](/guides/what-happens-when-code-runs) and [Processes, Memory & the CPU](/guides/processes-memory-and-cpu) are the right neighbors.

---

Allocate objects, drop a root, then run the collector to see mark-and-sweep decide what lives and what gets freed:

```playground-gc
```
