A compiler does not just translate expressions — it decides, for every name and every value in the program, where in memory it will live and for how long. Before code generation can emit a single load or store, the compiler needs a storage organization: a plan that carves the running program's address space into regions, each with its own allocation discipline. Get this plan right and recursion, dynamic data structures, and global constants all coexist happily. Get it wrong and you get dangling pointers, leaks, or a stack that eats the heap alive.
The
A classical run-time image divides memory into four regions, laid out from low addresses to high. The two at the bottom have sizes fixed before the program runs; the two at the top grow and shrink while it runs — and, cunningly, they grow toward each other so that whichever one needs more room can borrow from the shared gap between them.
Reveal the column region by region. At the bottom sits the code (or text) segment — the machine instructions themselves, fixed in size and usually read-only. Above it the static (or global) area holds data whose size and address are both known at compile time: global variables, string literals, static locals. Then comes the heap, which grows upward as the program allocates dynamic objects, and — from the very top of the address space downward — the stack, which grows downward as calls nest.
This is a genuinely elegant design decision, not an accident. The compiler cannot know in advance how a program will divide its dynamic memory between the two: a deeply recursive numerical routine is stack-hungry and heap-light, while a program building a huge graph is the reverse. If you gave each a fixed budget you would waste memory in one and starve the other.
By putting the heap at the bottom of the dynamic area growing up, and the stack at the top growing down, both draw from a single shared pool of free addresses in the middle. Either can expand as far as it likes until they meet — and that meeting point, not an arbitrary partition, is the true out-of-memory condition. One pool, allocated on demand from both ends: maximum flexibility from a single contiguous span.
The compiler chooses a home for each object by asking two questions. First, is its size known at compile time? A fixed-size record can be placed statically or on the stack; an array whose length is a run-time value, or a structure that must outlive its creator, is forced onto the heap. Second, what is its lifetime? — how long must the storage remain valid?
| Kind of object | Lifetime | Size known at compile time? | Lives in |
|---|---|---|---|
| Global / static variable | Whole program run | Yes | Static area |
| String / numeric literal | Whole program run | Yes | Static area |
| Local variable, parameter | One activation of its procedure | Usually yes | Stack |
| Compiler temporary | Within one expression / call | Yes | Stack (or register) |
new / malloc object | Arbitrary — until freed / unreachable | Often no | Heap |
Notice the pattern: lifetime that nests with the call structure ⇒ stack;
lifetime independent of the call structure ⇒ heap; lifetime = the whole
program ⇒ static. A language like C makes the programmer pick (static, an
automatic local, or malloc); a language like Java hides the choice but the compiler still
makes exactly these decisions underneath.
Walk through where each name in this fragment ends up. The reasoning is exactly what a compiler's storage planner does:
counter and GREETING exist for the program's entire life, so they go in the
static area. n and sum live exactly as long as one call to build,
so they ride the stack and vanish when the call returns. But the object { value: n } is
returned — it must survive after build's frame is gone — so it cannot live on the
stack; it is allocated on the heap. This "does it escape the frame?" test is precisely what real
escape-analysis passes compute.
The essential difference between static and stack storage is reuse. A static slot is written once and keeps its address forever; a stack slot's address is handed out on entry and reclaimed on exit, so a later call reuses the very same bytes. This short model shows the stack pointer bumping up on each entry and back down on each exit, while a static counter simply persists.
Watch g and h receive the identical address: stack storage is recycled the
instant a frame is popped, which is exactly why a local's value is meaningless after its procedure
returns.
String and numeric literals go in the static area, and in most systems that region is mapped
read-only by the loader. That is why, in C, taking char *s = "hi"; and
then writing s[0] = 'H'; is undefined behaviour that typically segfaults: the compiler put
"hi" in a read-only static page, and the store faults. It also lets the compiler
share identical literals — two occurrences of "hello" may point at the same
bytes. Statically-known, program-lifetime, immutable: the perfect tenant for the static segment.
The classic catastrophe is returning a pointer to a local. A local variable has automatic lifetime: its storage is on the stack and is reclaimed the moment its procedure returns. If you return the address of that local, you hand back a pointer into a frame that no longer exists — a dangling pointer. The next call overwrites those bytes and your pointer now reads garbage.
The fix is to match storage to lifetime: an object that must outlive its creating call needs dynamic (heap) allocation, not automatic (stack) allocation. Languages with garbage collection sidestep the trap by heap-allocating any object that escapes; C and C++ leave the choice — and the danger — to you.