Skip to content
Published on

[Compiler] 10. Runtime Environments - Stack, Heap, and Garbage Collection

Authors

Runtime Environments - Stack, Heap, and Garbage Collection

When generating code, a compiler must understand how a program uses memory at runtime. This article covers memory organization, stack allocation, heap management, and garbage collection.


1. Storage Organization

The memory of a running program is typically organized as follows:

High Address
+------------------+
|      Stack       |  <- Function call info, local variables
|        |         |
|        v         |
|                  |
|        ^         |
|        |         |
|      Heap        |  <- Dynamically allocated data
+------------------+
| Static Data      |  <- Global variables, constants
+------------------+
| Code             |  <- Program instructions
+------------------+
Low Address

Role of Each Region

  • Code region: Stores compiled machine instructions. Typically read-only.
  • Static data region: Stores global variables and constants. Allocated at program start and maintained until termination.
  • Stack region: Activation records are pushed on function call and popped on return.
  • Heap region: Stores data dynamically allocated via malloc, new, etc.

2. Activation Records

An activation record or stack frame is created each time a function is called.

Structure of an Activation Record

High Address
+---------------------------+
|  Actual parameters (args) |
+---------------------------+
|  Return value             |
+---------------------------+
|  Control link             |  <- Caller's frame pointer
+---------------------------+
|  Access link              |  <- For static scope support
+---------------------------+
|  Saved registers          |
+---------------------------+
|  Local variables          |
+---------------------------+
|  Temporary variables      |
+---------------------------+
Low Address

Role of Each Field

  1. Actual parameters: Argument values passed by the caller.
  2. Return value: Space to store the function's result. Registers may be used instead.
  3. Control Link: Pointer to the caller's activation record. Used to restore the stack on function return.
  4. Access Link: Pointer for accessing non-local variables.
  5. Saved registers: Register values that must be preserved upon function entry.
  6. Local variables: Variables declared within the function.
  7. Temporary variables: Temporary values generated by the compiler.

3. Calling Sequences

The code executed during function call and return is called the calling sequence.

What the Caller Does

1. Evaluate actual parameters and push onto the stack
2. Save the return address
3. Transfer control to the callee (call instruction)

What the Callee Does

1. Push registers that need saving onto the stack
2. Save previous frame pointer (FP) (control link)
3. Set new frame pointer (FP)
4. Allocate space for local variables (move SP)

What the Callee Does on Return

1. Store return value in designated location
2. Restore SP to FP
3. Restore saved registers
4. Restore previous FP
5. Jump to return address

Calling Sequence Example

void main() {             void foo(int x) {
    foo(10);                  int y = x + 1;
    // ...                    bar(y);
}                         }

                          void bar(int a) {
                              int b = a * 2;
                          }

Stack changes:

[1] When main called    [2] When foo(10) called  [3] When bar(11) called

+-------+              +-------+              +-------+
| main  |              | main  |              | main  |
+-------+              +-------+              +-------+
                       | foo   |              | foo   |
                       | x=10  |              | x=10  |
                       | y=11  |              | y=11  |
                       +-------+              +-------+
                                              | bar   |
                                              | a=11  |
                                              | b=22  |
                                              +-------+

4. Non-Local Variable Access

In languages with nested functions, there are times when variables of enclosing functions need to be accessed.

An access link points to the activation record of the statically enclosing function.

// Pascal-like code
procedure A;
    var x: integer;

    procedure B;
        var y: integer;

        procedure C;
        begin
            x := y + 1;  // Access A's x and B's y
        end;

    begin
        C;
    end;

begin
    B;
end;
Stack (calls: A -> B -> C):

+-------+
| A     |  <- Access link: none (outermost)
| x     |
+-------+
| B     |  <- Access link: points to A's record
| y     |
+-------+
| C     |  <- Access link: points to B's record
|       |
+-------+

C accessing x: follow access links 2 times (C -> B -> A)
C accessing y: follow access links 1 time (C -> B)

The number of access links followed equals the difference in nesting depth.

Displays

A display maintains a pointer array to the current activation record at each nesting depth, instead of following access link chains.

Display array:
  display[1] = pointer to A's activation record
  display[2] = pointer to B's activation record
  display[3] = pointer to C's activation record

C accessing x: direct access via display[1] (O(1))
C accessing y: direct access via display[2] (O(1))

The advantage of displays is that non-local variable access is always in constant time. However, there is a cost for updating the display on function entry/exit.


5. Heap Management

The heap stores data that is dynamically allocated and deallocated.

When Heap Allocation Is Needed

  • When the size of an object is unknown at compile time
  • When an object's lifetime exceeds the function call scope
  • Dynamic creation of data structures (linked lists, trees, etc.)

Memory Allocation Strategies

1. First Fit:
   Use the first free block that is large enough

   [In use][  Free block  ][In use][ Free block ]
                ^
            Allocate here

2. Best Fit:
   Use the free block closest in size to the request
   -> Higher possibility of external fragmentation

3. Buddy System:
   Split/merge blocks of power-of-2 sizes
   -> Possible internal fragmentation but fast merging

Fragmentation

External Fragmentation:
  Total free space is sufficient, but no contiguous block is large enough

  [Used][Free][Used][Free][Used][Free]
        ^^^       ^^^       ^^^
    100B each, but 200B request cannot be fulfilled

Internal Fragmentation:
  Unused space within an allocated block

  Request: 100B, Allocated: 128B -> 28B wasted

6. Garbage Collection (GC) Basics

Garbage collection automatically reclaims memory that is no longer in use.

What Is Garbage?

Objects unreachable from the root set are called garbage.

Root set: stack variables, global variables, registers

    [Root] --> [Object A] --> [Object B]
                 |
                 v
              [Object C]

              [Object D] --> [Object E]   <- Unreachable = Garbage!

7. Reference Counting

Maintains a reference count for each object; when the count reaches 0, the object is reclaimed.

a = new Object();   // Object refcount = 1
b = a;              // Object refcount = 2
a = null;           // Object refcount = 1
b = null;           // Object refcount = 0 -> Reclaim!

Pros and Cons of Reference Counting

Pros:
  - Immediate reclamation: memory returned as soon as reference hits 0
  - No pauses: no GC stop-the-world
  - Simple implementation

Cons:
  - Cannot handle circular references:
    [A] --> [B]
     ^       |
     +-------+
    A.refcount = 1, B.refcount = 1 (never reaches 0)

  - Count update overhead: count updated on every assignment
  - Cache efficiency degradation: remote memory access on reference updates

8. Tracing-Based Garbage Collection

Mark-and-Sweep

Operates in two phases:

Mark Phase:
  1. Start from root set
  2. Mark all reachable objects
  3. Traverse object graph using DFS or BFS

Sweep Phase:
  1. Traverse all objects in the heap
  2. Reclaim unmarked objects
  3. Clear marks on marked objects
Before marking:
  [A*] -> [B*] -> [C*]
                    |
                    v
  [D ] -> [E ]    [F*]

  * = reachable from root

After sweeping:
  [A] -> [B] -> [C]
                  |
                  v
  [Free] -> [Free]  [F]

  D, E are reclaimed

Characteristics of Mark-and-Sweep

Pros:
  - Correctly reclaims circular references
  - Overhead proportional to the amount of garbage

Cons:
  - Stop-the-World: program pauses during GC execution
  - Fragmentation: memory becomes fragmented after reclamation

9. Copying Collector

Divides the heap into two semispaces.

[From Space]             [To Space]
+---------+              +---------+
| A  B  C |  copy -->    | A  C    |
| (free) D|              | (free)  |
+---------+              +---------+

Only live objects are copied to the To space
-> No fragmentation, contiguous memory

Cheney's Algorithm

Copies live objects using a BFS approach.

1. Set scan and free pointers at the start of To space
2. Copy objects referenced from roots to To space
3. Process references of the object pointed to by scan:
   - If referenced object is in From, copy to To
   - Update reference to new location
4. Complete when scan meets free

Characteristics of Copying Collectors

Pros:
  - No fragmentation (compaction effect)
  - Very fast allocation (just pointer increment)
  - Cost incurred only for live objects

Cons:
  - Only half the memory is usable
  - High copy cost if many objects are live
  - Pointer updates required

10. Generational Garbage Collection

Generational GC is based on the observation that "most objects die young" (generational hypothesis).

Generation Divisions

+------------------+
|  Old Generation  |  (long-lived objects)
|  (Major GC target)|  <- Low GC frequency
+------------------+
|                  |
| Young Generation |  (newly created objects)
|  (Minor GC target)|  <- High GC frequency
|  +----+ +------+ |
|  |Eden| |Survivor||
|  +----+ +------+ |
+------------------+

How It Works

1. New objects are allocated in the Young generation
2. When Young generation is full, Minor GC runs:
   - Copies live objects to Survivor area
   - Objects surviving multiple collections are promoted to Old generation
3. When Old generation is full, Major GC runs:
   - GC over the entire heap (expensive)

Remembered Sets

References from the Old generation to the Young generation must be tracked.

If object A in Old generation references object B in Young generation:
  -> Add A to the remembered set
  -> Treat remembered set objects as roots during Minor GC

This allows Minor GC to avoid scanning the entire Old generation.

Real GC Implementation Examples

Java HotSpot JVM:
  Young: Eden + Survivor0 + Survivor1 (copying collection)
  Old: Mark-Compact or CMS/G1/ZGC

Go:
  Concurrent mark-and-sweep without generations
  Uses tri-color marking

Python:
  Reference counting + generational GC (for circular reference handling)

11. Modern GC Advances

Concurrent GC

Performs GC concurrently with program execution.

[Program threads]  -------run-------run-------run------->
[GC threads]       --mark--|-sweep--|---------|-mark--|-->

-> Minimizes Stop-the-World time

Incremental GC

Divides GC work into small units and interleaves with program execution.

[Program] [GC] [Program] [GC] [Program] [GC] [Program]
   10ms   2ms    10ms    2ms    10ms    2ms    10ms

-> Multiple short pauses instead of one long pause

Modern Collectors

Java G1 (Garbage First):
  - Divides heap into equal-sized regions
  - Collects regions with the most garbage first
  - Configurable pause time target

Java ZGC / Shenandoah:
  - Very short pauses (within a few milliseconds)
  - Supports large heaps (multiple terabytes)
  - Concurrent compaction

Summary

ConceptKey Content
Activation RecordStack frame created on function call
Control LinkPointer to caller's frame
Access LinkPointer to enclosing function for static scope
DisplayPointer array to current activation record per nesting depth
Heap ManagementDynamic allocation/deallocation, fragmentation management
Reference CountingCounts references, reclaims at 0, circular reference problem
Mark-and-SweepMarks reachable objects, reclaims the rest
Copying CollectorCopies only live objects to new space
Generational GCDivides by object lifetime for efficient collection

Understanding runtime environments is essential for generating efficient code. The compiler phases covered in this series (lexical analysis, syntax analysis, semantic analysis, intermediate code generation, runtime environments) are the foundational technologies behind modern programming languages and tools.