Storage organisation: static, stack and heap
Where the compiler puts the data of a running program: static storage fixed at compile time, the stack of activation records, and the heap; what each language feature forces (recursion needs a stack, not a heap; procedures returned as values and dynamic data structures need a heap; static allocation alone cannot support recursion), dangling references, the layout of a process, and how much stack a recursion uses.
Why it exists
The compiler must decide where every piece of data lives while the program runs, and the answer is not the same for all data: a global variable exists for the whole run, a local variable only while its procedure runs (and a recursive procedure has several copies at once), and a node allocated with malloc lives until the program frees it, whatever the procedure that allocated it did. The compiler organises memory into three kinds of storage for these three lifetimes. GATE asks which kind a given variable uses, which language features force the stack or the heap (and the classic wrong beliefs: recursion needs a heap, recursion needs garbage collection), and what goes wrong when the wrong kind is used.
Prerequisites
You need the activation record (also called a frame) that one call of a procedure occupies, with its parameters, locals and links, from activation records.
Precise definitions
Three kinds of storage.
| Static | Stack | Heap | |
|---|---|---|---|
| holds | code, constants, string literals, global and static variables | activation records: parameters, local variables, return address, links | dynamically allocated objects (malloc, new) |
| address fixed | at compile (or link or load) time | at run time, relative to the frame; offsets known at compile time | at run time, chosen by the allocator |
| lifetime | the whole run | from the call to the return of the procedure: last in, first out | from allocation until freed (or collected) |
| size known | at compile time | the size of a frame is known at compile time (except variable-sized arrays) | at run time |
| cost | none at run time | a push and a pop: very cheap | an allocator call, fragmentation, a free or a collection |
The memory of a process is laid out with the code and the static data at the bottom, the heap above them growing towards higher addresses, and the stack at the top growing towards lower addresses (on most machines), with unused memory between them; the C memory layout page shows it, and the operating system maps it (process address space).
Static allocation. A compiler that allocates all data statically gives every procedure one fixed record. That works only if a procedure never has two activations alive at once, that is, without recursion (early Fortran), and without variable-sized arrays or dynamic data. With recursion the second activation would overwrite the first one's parameters and locals.
Stack allocation. A record is pushed when the procedure is called and popped when it returns. Because calls and returns nest, the records are freed in the reverse order of their creation, so a stack is enough: recursion is implemented with a stack, not with a heap, and needs no garbage collection. Nested procedures with static scoping add access links or a display to reach the records of enclosing procedures (the page on activation records). With dynamic scoping no heap is needed either.
Heap allocation. Needed for data whose lifetime is not nested in the calls: objects created by a procedure that outlive it (dynamic data structures: linked lists, trees), and the environment of a procedure that is returned as a value (a closure): the returned procedure refers to variables of the record in which it was created, and that record must survive the return of its procedure, so it cannot be on the stack. A language that allows a function to return a function as its result cannot use a pure stack discipline for its activation records.
Dangling reference. A reference to storage that has been freed or popped. Stack allocation makes it easy to create one: returning the address of a local variable (return &x;) refers to a record that is popped when the procedure returns; the same applies to a returned nested procedure that uses its creator's variables. Heap storage freed too early, or freed twice, causes the same errors.
Heap management. The allocator keeps a free list; free returns a block. Over time the free space is split into many small pieces (external fragmentation); allocators merge neighbouring free blocks and choose a block by first fit, best fit or similar. A language can free dead objects automatically: garbage collection (reference counting, mark and sweep, copying collection) finds the objects that can no longer be reached. It is needed to avoid leaks in a language with a heap and no explicit free; it is not needed for recursion.
Which feature needs what.
| feature | needs |
|---|---|
| global variables, constants, literals | static storage |
| procedures that may call themselves (recursion) | stack (a separate record per activation); not a heap |
| nested procedures, static scope | stack plus access links or a display |
| arrays whose size is known at run time only | stack (variable-sized array in the record) or heap |
dynamic data structures (malloc, new) | heap |
| functions returned as values, with their environment | heap for the environment |
| dynamic scoping | no heap needed |
| garbage collection | only for languages that allocate on the heap without free |
How it works
The same programs under three policies. A tiny language with recursion, nested procedures and procedure values is run three times: with static allocation (one record per procedure), stack allocation (a record is popped on return) and heap allocation (records are kept). Recursion fails with static allocation; a returned procedure fails on the stack ("dangling") and, with static allocation, two counters share one variable; the heap gets all of them right.
The same programs under three storage policies for activation records: start
Computed by a script, not typed| program | static: one record per procedure | stack: popped on return | heap: kept while referenced |
|---|---|---|---|
| fact(4), a recursive procedure | |||
| main calls make_counter once; the nested procedure inc, returned as a value, is called twice | |||
| main calls make_counter twice and uses both counters |
Step 0
A tiny language with recursion, nested procedures and procedure values, run by the machine of scripts/cd_storage.py. Static: every activation of a procedure shares one record. Stack: a record is popped at the return and a later access to it is a dangling reference.
How much stack does a recursion need? The space is the depth of the recursion, not the number of calls.
Recursion: calls made against records alive at once (fib): start
Computed by a script, not typed| call | calls in all | deepest chain = records alive at once | stack at 32 bytes per record |
|---|---|---|---|
| fib(1) | |||
| fib(2) | |||
| fib(3) | |||
| fib(5) | |||
| fib(8) | |||
| fib(10) | |||
| fib(15) |
Step 0
Space on the stack is proportional to the DEPTH of the recursion, not the number of calls. The counts are the same as those measured by the C program of the page.
Worked examples
EX 1Why static allocation cannot support recursion
EasyThe factorial is written twice. In the first, the parameter lives in one static variable (the single record of the procedure); in the second it is an ordinary parameter on the stack.
#include <stdio.h>
static int n_static; /* the one static record of fact_static: its parameter */
static int fact_static(int arg) {
int r;
n_static = arg;
if (n_static <= 1) return 1;
r = fact_static(n_static - 1); /* the deeper activations overwrite n_static */
return n_static * r;
}
static int fact_stack(int n) {
int r;
if (n <= 1) return 1;
r = fact_stack(n - 1);
return n * r;
}
int main(void) {
printf("fact(4) with one static copy of the parameter: %d\n", fact_static(4));
printf("fact(4) with automatic (stack) storage: %d\n", fact_stack(4));
return 0;
}fact(4) with one static copy of the parameter: 1
fact(4) with automatic (stack) storage: 24Reading it. After fact_static(n - 1) returns, n_static holds 1 (the value written by the deepest call), so every level multiplies by 1 and the answer is 1. With automatic storage each activation has its own n, and the result is . So static allocation of all data areas makes recursion impossible; dynamic allocation of activation records (the stack) is what makes it work. A heap and a garbage collector are not needed.
EX 2Space: calls against records alive at once
MediumThe recursive Fibonacci function makes exponentially many calls, but its stack is shallow. The program counts the calls and the deepest chain of active calls.
#include <stdio.h>
static long calls;
static int depth, deepest;
static long fib(int n) {
long r;
calls++;
depth++;
if (depth > deepest) deepest = depth;
r = n < 2 ? 1 : fib(n - 1) + fib(n - 2);
depth--;
return r;
}
int main(void) {
int ns[7] = {1, 2, 3, 5, 8, 10, 15};
for (int i = 0; i < 7; i++) {
calls = 0;
deepest = 0;
fib(ns[i]);
printf("fib(%2d): %5ld calls, deepest chain %2d\n", ns[i], calls, deepest);
}
return 0;
}fib( 1): 1 calls, deepest chain 1
fib( 2): 3 calls, deepest chain 2
fib( 3): 5 calls, deepest chain 3
fib( 5): 15 calls, deepest chain 5
fib( 8): 67 calls, deepest chain 8
fib(10): 177 calls, deepest chain 10
fib(15): 1973 calls, deepest chain 15Reading it. fib(15) makes 1973 calls, but at no moment are more than 15 activations alive: each call returns before its sibling starts, so the stack holds only the chain from the original call down to the current one. With a 32-byte record the stack for fib(15) needs bytes. The stack space of a recursion is proportional to its depth; the time is proportional to the number of calls. A recursion that goes a million levels deep (a naive list traversal) can overflow the stack although each frame is small.
EX 3Where does each variable live?
MediumThree small tests of the three kinds of storage, and of the direction in which the stack grows on this machine.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
static uintptr_t deepest_address;
static void deepest(int n) {
volatile int local = n; /* one record per activation, on the stack */
if (n == 0) deepest_address = (uintptr_t)&local; /* the address is only compared, and only while the record exists */
else deepest(n - 1);
}
static int next_static(void) {
static int count = 0; /* static storage: one copy, kept between calls */
return ++count;
}
static int next_automatic(void) {
int count = 0; /* automatic: a new copy in every activation */
return ++count;
}
static int *make(void) {
int *p = malloc(sizeof *p); /* heap: survives the return of make */
if (p) *p = 42;
return p;
}
int main(void) {
volatile int outer = 0;
deepest(5);
uintptr_t a = (uintptr_t)&outer, b = deepest_address;
printf("a local of a deeper activation has a %s address than one of its caller\n", b < a ? "LOWER" : "HIGHER");
int s1 = next_static(), s2 = next_static(), s3 = next_static(); /* separate statements: the order of evaluating arguments is unspecified */
int a1 = next_automatic(), a2 = next_automatic(), a3 = next_automatic();
printf("static counter, three calls: %d %d %d\n", s1, s2, s3);
printf("automatic counter, three calls: %d %d %d\n", a1, a2, a3);
int *h = make();
if (!h) return 1;
printf("heap block read after make() returned: %d\n", *h);
free(h);
return 0;
}a local of a deeper activation has a LOWER address than one of its caller
static counter, three calls: 1 2 3
automatic counter, three calls: 1 1 1
heap block read after make() returned: 42Reading it. The first line is a fact about this machine and its compiler, not the language: each deeper call puts its record at a lower address (the stack grows downwards on most machines). The static variable keeps its value between calls (one copy for the whole run), the automatic one starts again in every activation (a new record each time). The heap block was allocated inside make and is still valid after make has returned, because the heap is not tied to the calls. Which storage a variable gets follows from its declaration: global or static: static storage; local or parameter: the stack record; malloc: the heap.
EX 4Which statements are true?
GATE level(1) A language without global variables and nested procedures, but with recursion, can be implemented with static allocation. False: recursion needs a record per activation. (2) Recursion cannot be implemented with dynamic allocation of activation records. False: that is exactly how it is done. (3) Automatic garbage collection is essential to implement recursion. False: a stack of records is freed by popping. (4) Both a heap and a stack are essential to implement recursion. False: the stack alone is enough. (5) A language that supports recursion necessarily needs heap storage. False. (6) Languages that allow dynamic data structures necessarily need the heap. True. (7) Languages that permit a function to return a function as its result cannot be implemented with a stack-based scheme alone. True: the environment must survive the return. (8) Static allocation of all data areas makes it impossible to implement recursion. True. (9) A multi-level access link (display) is needed only if the language has nested procedures. True. (10) Languages with dynamic scoping necessarily need a heap. False. A related fact from the same chapter: the symbol table is used in later phases too (type checking, code generation), and the rule "a variable must be declared before use" is checked in semantic analysis, not in syntax analysis.
EX 5Dangling references
GATE levelA procedure make_counter declares count, defines a nested procedure inc that increments count, and returns inc. After the call returns, inc is called. On the stack the record of make_counter (which holds count) has been popped, so the call to inc refers to freed storage: a dangling reference (the table: "DANGLING"). With heap allocation the record stays alive and the counter works: 12 for two calls in the first program. With static allocation it also works for one counter, but a second call to make_counter reuses the same record and resets the first counter's variable: two counters share one count and give 123 instead of 121. In C the same error is return &x; for a local x: the address refers to a frame that is gone.
Cheat box
Cheat box
- Static: globals,
static, constants, literals, code: address fixed at compile time, lives for the whole run, no recursion by itself. - Stack: activation records, LIFO, one record per call: this implements recursion, nested calls, local variables.
- Heap:
mallocandnew; lifetime not nested in calls; closures that escape; needsfreeor garbage collection. - Recursion does not need a heap or garbage collection. Dynamic data structures do need the heap.
- Returning a function with its environment heap for the environment. Returning
&localdangling reference. - Stack space depth of recursion frame size, not the number of calls.
- Layout: code and static data low, heap grows up, stack grows down.
Traps and GATE patterns
Connections
Across subjects
The activation record of activation records is the unit that the stack holds; parameter passing decides what is copied into it. A C program sees the same three areas in the C memory layout, and malloc is the heap of dynamic memory; the operating system provides them as segments of the process address space.
PYQs
4 previous year questions from your book. I re-solved each one myself (0 by running or simulating, 4 by reasoning only); the book's key is shown next to my answer. All PYQs
Which of the following are true? (I) A programming language which does not permit global variables of any kind and has no nesting of procedures/functions, but permits recursion can be implemented with static storage allocation (II) Multi-level access link (or display) arrangement is needed to arrange activation records only if the programming language being implemented has nesting of procedures/functions. (III) Recursion in programming languages cannot be implemented with dynamic storage allocation (IV) Nesting procedures/functions and recursion require a dynamic heap allocation scheme and cannot be implemented with a stack- based allocation scheme for activation records (V) Programming languages which permit a function to return a function as its result cannot be implemented with a stack-based storage allocation scheme for activation records.
- (a)II and V only
- (b)I, III and IV only
- (c)I, II and IV only
- (d)II, III and V only
Consider the following statements. I. Symbol table is accessed only during lexical analysis and syntax analysis. II. Compilers for programming languages that support recursion necessarily need heap storage for memory allocation in the run-time environment. III. Errors violating the condition 'any variable must be declared before its use' are detected during syntax analysis. Which of the above statements is/are TRUE?
- (a)II only
- (b)I only
- (c)I and III only
- (d)None of I, II and III
Which of the following statements are Correct? 1. Static allocation of all data areas by a compiler makes it impossible to implement recursion. 2. Automatic garbage collection is essential to implement recursion. 3. Dynamic allocation of activation records is essential to implement recursion. 4. Both heap and stack are essential to implement recursion.
- (a)1 and 2 only
- (b)2 and 3 only
- (c)3 and 4 only
- (d)1 and 3 only
Which languages necessarily need heap allocation in the runtime environment?
- (a)Those that support recursion
- (b)Those that use dynamic scoping
- (c)Those that allow dynamic data structures
- (d)Those that use global variables
Self-check
Question 1
Can recursion be implemented with only static allocation? With a stack but no heap? With a heap but no stack?
Show answerHide
Static only: no. Stack but no heap: yes. A heap alone can also work (each activation record allocated from it), but nothing requires it.
Question 2
Where is each of these stored: a global array, a local int, a malloced node, a string literal?
Show answerHide
Global array: static. Local int: stack frame. malloced node: heap. String literal: static (read-only) storage.
Question 3
fib(20) makes 21891 calls. How many activation records are alive at most at once?
Show answerHide
20 (the chain fib(20), fib(19), ..., fib(1)).
Question 4
Why can a function that returns a nested function not keep its activation record on the stack?
Show answerHide
The returned function uses variables of its creator's record; the record would be popped at the return and the later calls would use freed storage. The record must be kept (on the heap).
Question 5
Does a language with dynamic scoping need a heap?
Show answerHide
No. Dynamic scoping looks names up along the chain of callers, which are all on the stack.
Revision card
One-screen revision
- Static: fixed for the run; stack: activation records, LIFO, recursion; heap: dynamic data, escaping closures,
freeor GC. - Recursion needs the stack, not a heap or a collector; static alone cannot support it.
- Dangling reference: stack record popped (returned
&local, escaping closure on the stack). - Stack space depth frame size. Layout: heap grows up, stack grows down.
References
- ·Aho, Lam, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (2nd edition), Chapter 7 Run-Time Environments (storage organisation, stack allocation of space, heap management, introduction to garbage collection)
- ·Appel, Modern Compiler Implementation, Chapter 6 Activation Records
- ·Louden, Compiler Construction: Principles and Practice, Chapter 7 Runtime Environments
Verification
The three storage policies are run on a tiny language with recursion, nested procedures and procedure values (scripts/cd_storage.py): the outcomes of the table (a recursive factorial is wrong under static allocation, a closure returned from a call is a dangling reference under stack allocation and wrong when two counters share one static record) are compared by scripts/enum-checks/cd_t2_check.py with direct models of each policy, with 200 random recursive procedures under stack and heap allocation (value and stack depth), and with the C programs printed on the page (a recursive factorial whose parameter lives in static storage prints 1, the same with automatic storage prints 24; the number of calls and the deepest chain of fib(n), counted by the program, equal the table); tests/cd-t2-traces.test.ts recomputes the table. That the stack grows towards lower addresses on the machine of the run is a property of the machine, printed by one program, not of the language. The statements about which language features need which storage are the standard textbook ones; garbage collection is described, not simulated. Textbook chapter titles are from memory. The independent second pass has not been run, so the page stays draft. SECOND PASS (2026-11-02, done by the assistant, not a human check): every Self-check question was solved before the printed answer was read and all agreed; the definitions, worked examples, cheat box and traps were read claim by claim and the numbers re-derived by hand or by a small script (token counts, FIRST and FOLLOW sets, LL(1) tables and moves, LR item sets, state counts and parses, operator-precedence values, attribute evaluation and printing schemes, activation-record counts and access links, parameter-passing results, three-address code, jumping and backpatched code, leaders and blocks, value numbering and DAGs, gen/kill and data flow sets, Sethi-Ullman labels and live ranges); long C programs were not re-read line by line (they are compiled and compared by npm run verify:c, and their printed outputs were checked against the prose); tables and animations are machine output and were read, not re-run; No other defect found. Status raised to self-checked; verified needs a textbook or human check.
last verified: never
My notes (saved in this browser)