Skip to content

Split View: [컴파일러] 10. 실행 시간 환경 - 스택, 힙, 가비지 컬렉션

|

[컴파일러] 10. 실행 시간 환경 - 스택, 힙, 가비지 컬렉션

실행 시간 환경 - 스택, 힙, 가비지 컬렉션

컴파일러는 코드를 생성할 때 프로그램이 실행 시간에 어떻게 메모리를 사용하는지 알아야 합니다. 이 글에서는 메모리 구성, 스택 할당, 힙 관리, 그리고 가비지 컬렉션을 다룹니다.


1. 메모리 구성(Storage Organization)

실행 중인 프로그램의 메모리는 일반적으로 다음과 같이 구성됩니다.

높은 주소
+------------------+
|      스택 (Stack) |  <- 함수 호출 정보, 지역 변수
|        |         |
|        v         |
|                  |
|        ^         |
|        |         |
|      힙 (Heap)   |  <- 동적 할당 데이터
+------------------+
| 정적 데이터       |  <- 전역 변수, 상수
+------------------+
| 코드 (Code)      |  <- 프로그램 명령어
+------------------+
낮은 주소

각 영역의 역할

  • 코드 영역: 컴파일된 기계어 명령어가 저장됩니다. 일반적으로 읽기 전용입니다.
  • 정적 데이터 영역: 전역 변수와 상수가 저장됩니다. 프로그램 시작 시 할당되어 종료 시까지 유지됩니다.
  • 스택 영역: 함수 호출 시 활성 레코드가 push되고, 반환 시 pop됩니다.
  • 힙 영역: malloc, new 등으로 동적 할당된 데이터가 저장됩니다.

2. 활성 레코드(Activation Record)

함수가 호출될 때마다 활성 레코드(activation record) 또는 **스택 프레임(stack frame)**이 생성됩니다.

활성 레코드의 구조

높은 주소
+---------------------------+
|  실제 매개변수 (arguments) |
+---------------------------+
|  반환값                    |
+---------------------------+
|  제어 링크 (control link)  |  <- 호출자의 프레임 포인터
+---------------------------+
|  접근 링크 (access link)   |  <- 정적 스코프 지원
+---------------------------+
|  저장된 레지스터            |
+---------------------------+
|  지역 변수                 |
+---------------------------+
|  임시 변수                 |
+---------------------------+
낮은 주소

각 필드의 역할

  1. 실제 매개변수: 호출자가 전달한 인자값입니다.
  2. 반환값: 함수의 결과를 저장하는 공간입니다. 레지스터를 사용하기도 합니다.
  3. 제어 링크(Control Link): 호출자의 활성 레코드를 가리키는 포인터입니다. 함수 반환 시 스택을 복원하는 데 사용합니다.
  4. 접근 링크(Access Link): 비지역 변수에 접근하기 위한 포인터입니다.
  5. 저장된 레지스터: 함수 진입 시 보존해야 할 레지스터 값입니다.
  6. 지역 변수: 함수 내에서 선언된 변수입니다.
  7. 임시 변수: 컴파일러가 생성한 임시 값입니다.

3. 호출 순서(Calling Sequence)

함수 호출과 반환 시 실행되는 코드를 호출 순서라고 합니다.

호출자(Caller)가 하는 일

1. 실제 매개변수를 평가하여 스택에 push
2. 반환 주소를 저장
3. 피호출자로 제어를 이동 (call 명령)

피호출자(Callee)가 하는 일

1. 저장해야 할 레지스터를 스택에 push
2. 이전 프레임 포인터(FP)를 저장 (제어 링크)
3. 새로운 프레임 포인터(FP)를 설정
4. 지역 변수를 위한 공간 할당 (SP 이동)

반환 시 피호출자가 하는 일

1. 반환값을 지정된 위치에 저장
2. SP를 FP로 복원
3. 저장된 레지스터를 복원
4. 이전 FP를 복원
5. 반환 주소로 점프

호출 과정 예시

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

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

스택 변화:

[1] main 호출 시        [2] foo(10) 호출 시      [3] bar(11) 호출 시

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

4. 비지역 변수 접근

중첩 함수(nested function)가 있는 언어에서, 바깥 함수의 변수에 접근해야 할 때가 있습니다.

접근 링크는 정적으로 둘러싸는(enclosing) 함수의 활성 레코드를 가리킵니다.

// Pascal-like 코드
procedure A;
    var x: integer;

    procedure B;
        var y: integer;

        procedure C;
        begin
            x := y + 1;  // A의 x와 B의 y에 접근
        end;

    begin
        C;
    end;

begin
    B;
end;
스택 (호출: A -> B -> C):

+-------+
| A     |  <- 접근 링크: 없음 (최외곽)
| x     |
+-------+
| B     |  <- 접근 링크: A의 레코드를 가리킴
| y     |
+-------+
| C     |  <- 접근 링크: B의 레코드를 가리킴
|       |
+-------+

C에서 x에 접근: 접근 링크를 2번 따라감 (C -> B -> A)
C에서 y에 접근: 접근 링크를 1번 따라감 (C -> B)

접근 링크를 따라가는 횟수는 중첩 깊이의 차이와 같습니다.

디스플레이(Display)

디스플레이는 접근 링크 체인을 따라가는 대신, 각 중첩 깊이의 현재 활성 레코드에 대한 포인터 배열을 유지합니다.

디스플레이 배열:
  display[1] = A의 활성 레코드 포인터
  display[2] = B의 활성 레코드 포인터
  display[3] = C의 활성 레코드 포인터

C에서 x에 접근: display[1]로 바로 접근 (O(1))
C에서 y에 접근: display[2]로 바로 접근 (O(1))

디스플레이의 장점은 비지역 변수 접근이 항상 상수 시간이라는 것입니다. 단, 함수 진입/탈출 시 디스플레이 갱신 비용이 있습니다.


5. 힙 관리(Heap Management)

힙은 동적으로 할당되고 해제되는 데이터를 저장합니다.

힙 할당이 필요한 경우

  • 객체의 크기가 컴파일 시간에 알 수 없는 경우
  • 객체의 수명이 함수 호출 범위를 넘는 경우
  • 데이터 구조(연결 리스트, 트리 등)의 동적 생성

메모리 할당 전략

1. 퍼스트 핏(First Fit):
   충분히 큰 첫 번째 빈 블록을 사용

   [사용중][  빈 블록  ][사용중][ 빈 블록 ]
                ^
            여기에 할당

2. 베스트 핏(Best Fit):
   요청 크기에 가장 가까운 빈 블록을 사용
   -> 외부 단편화 가능성이 높음

3. 버디 시스템(Buddy System):
   2의 거듭제곱 크기의 블록으로 분할/합병
   -> 내부 단편화 가능성이 있지만 합병이 빠름

단편화(Fragmentation)

외부 단편화 (External Fragmentation):
  빈 공간의 총합은 충분하지만, 연속 블록이 부족

  [사용][빈][사용][빈][사용][빈]
        ^^^       ^^^       ^^^
    각각 100B이지만 200B 요청 불가

내부 단편화 (Internal Fragmentation):
  할당된 블록 내에 사용되지 않는 공간

  요청: 100B, 할당: 128B -> 28B 낭비

6. 가비지 컬렉션(Garbage Collection) 기초

가비지 컬렉션(GC)은 더 이상 사용되지 않는 메모리를 자동으로 회수하는 기법입니다.

가비지란?

**루트 집합(root set)**에서 도달할 수 없는 객체를 가비지라고 합니다.

루트 집합: 스택 변수, 전역 변수, 레지스터

    [루트] --> [객체A] --> [객체B]
                 |
                 v
              [객체C]

              [객체D] --> [객체E]   <- 도달 불가 = 가비지!

7. 참조 카운팅(Reference Counting)

각 객체에 참조 카운트를 유지하여, 카운트가 0이 되면 회수합니다.

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

참조 카운팅의 장단점

장점:
  - 즉시 회수: 참조가 0이 되면 바로 메모리 반환
  - 일시 정지 없음: GC 정지(stop-the-world) 없음
  - 구현이 간단

단점:
  - 순환 참조 처리 불가:
    [A] --> [B]
     ^       |
     +-------+
    A.refcount = 1, B.refcount = 1 (영원히 0이 안 됨)

  - 카운트 갱신 오버헤드: 대입마다 카운트 갱신
  - 캐시 효율 저하: 참조 갱신 시 원격 메모리 접근

8. 추적 기반 가비지 컬렉션

마크 앤 스윕(Mark-and-Sweep)

2단계로 동작합니다.

마크 단계 (Mark Phase):
  1. 루트 집합에서 시작
  2. 도달 가능한 모든 객체에 "마크" 표시
  3. DFS 또는 BFS로 객체 그래프 순회

스윕 단계 (Sweep Phase):
  1. 힙의 모든 객체를 순회
  2. 마크되지 않은 객체를 회수
  3. 마크된 객체의 마크를 제거
마크 전:
  [A*] -> [B*] -> [C*]
                    |
                    v
  [D ] -> [E ]    [F*]

  * = 루트에서 도달 가능

스윕 후:
  [A] -> [B] -> [C]
                  |
                  v
  [빈] -> [빈]  [F]

  D, E는 회수됨

마크 앤 스윕의 특징

장점:
  - 순환 참조도 정확히 회수
  - 오버헤드가 쓰레기의 양에 비례

단점:
  - Stop-the-World: GC 실행 중 프로그램 일시 정지
  - 단편화: 회수 후 메모리가 조각남

9. 복사 컬렉터(Copying Collector)

힙을 두 개의 반공간(semispace)으로 나누어 사용합니다.

[From 공간]              [To 공간]
+---------+              +---------+
| A  B  C |  복사 -->    | A  C    |
| (빈) D  |              | (빈)    |
+---------+              +---------+

살아있는 객체만 To 공간으로 복사
-> 단편화 없음, 연속 메모리

Cheney 알고리즘

BFS 방식으로 살아있는 객체를 복사합니다.

1. To 공간의 시작 위치에 scan과 free 포인터 설정
2. 루트에서 참조하는 객체를 To 공간으로 복사
3. scan 포인터가 가리키는 객체의 참조를 처리:
   - 참조하는 객체가 From에 있으면 To로 복사
   - 참조를 새 위치로 갱신
4. scan이 free를 만나면 완료

복사 컬렉터의 특징

장점:
  - 단편화 없음 (압축 효과)
  - 할당이 매우 빠름 (포인터 증가만)
  - 살아있는 객체에만 비용 발생

단점:
  - 메모리의 절반만 사용 가능
  - 살아있는 객체가 많으면 복사 비용이 큼
  - 포인터 갱신 필요

10. 세대별 가비지 컬렉션(Generational GC)

세대별 GC는 "대부분의 객체는 젊어서 죽는다(generational hypothesis)"는 관찰에 기반합니다.

세대 구분

+------------------+
|  Old Generation  |  (오래 살아남은 객체)
|  (Major GC 대상) |  <- GC 빈도 낮음
+------------------+
|                  |
| Young Generation |  (새로 생성된 객체)
|  (Minor GC 대상) |  <- GC 빈도 높음
|  +----+ +------+ |
|  |Eden| |Survivor||
|  +----+ +------+ |
+------------------+

동작 방식

1. 새 객체는 Young 세대에 할당
2. Young 세대가 가득 차면 Minor GC 실행:
   - 살아있는 객체를 Survivor 영역으로 복사
   - 여러 번 살아남은 객체는 Old 세대로 승격(promotion)
3. Old 세대가 가득 차면 Major GC 실행:
   - 전체 힙을 대상으로 GC (비용이 큼)

기억 집합(Remembered Set)

Old 세대에서 Young 세대를 참조하는 경우를 추적해야 합니다.

Old 세대의 객체 A가 Young 세대의 객체 B를 참조하면:
  -> 기억 집합에 A를 추가
  -> Minor GC 시 기억 집합의 객체도 루트로 취급

이를 통해 Minor GC가 Old 세대 전체를 스캔하지 않아도 됩니다.

실제 GC 구현 예시

Java HotSpot JVM:
  Young: Eden + Survivor0 + Survivor1 (복사 수집)
  Old: Mark-Compact 또는 CMS/G1/ZGC

Go:
  세대 없이 동시(concurrent) 마크 앤 스윕
  삼색 표시 (tri-color marking) 사용

Python:
  참조 카운팅 + 세대별 GC (순환 참조 처리용)

11. 현대 GC의 발전

동시 GC(Concurrent GC)

프로그램 실행과 GC를 동시에 수행합니다.

[프로그램 스레드]  -------실행-------실행-------실행------->
[GC 스레드]       --마크--|-스윕---|---------|-마크--|-->

-> Stop-the-World 시간을 최소화

증분 GC(Incremental GC)

GC 작업을 작은 단위로 나누어 프로그램과 번갈아 실행합니다.

[프로그램] [GC] [프로그램] [GC] [프로그램] [GC] [프로그램]
   10ms   2ms    10ms    2ms    10ms    2ms    10ms

-> 긴 일시 정지 대신 짧은 일시 정지 여러 번

현대 컬렉터

Java G1 (Garbage First):
  - 힙을 동일 크기의 리전(region)으로 분할
  - 가비지가 많은 리전을 우선 수집
  - 일시 정지 목표 시간 설정 가능

Java ZGC / Shenandoah:
  - 매우 짧은 일시 정지 (수 밀리초 이내)
  - 대용량 힙 (수 테라바이트) 지원
  - 동시 압축(concurrent compaction)

정리

개념핵심 내용
활성 레코드함수 호출 시 생성되는 스택 프레임
제어 링크호출자의 프레임을 가리키는 포인터
접근 링크정적 스코프의 바깥 함수를 가리키는 포인터
디스플레이각 중첩 깊이의 현재 활성 레코드 포인터 배열
힙 관리동적 할당/해제, 단편화 관리
참조 카운팅참조 수를 세어 0이면 회수, 순환 참조 문제
마크 앤 스윕도달 가능한 객체 표시 후 나머지 회수
복사 컬렉터살아있는 객체만 새 공간으로 복사
세대별 GC객체 수명에 따라 구분하여 효율적 수집

실행 시간 환경에 대한 이해는 효율적인 코드를 생성하는 데 필수적입니다. 이 시리즈에서 다룬 컴파일러의 각 단계(어휘 분석, 구문 분석, 의미 분석, 중간 코드 생성, 실행 시간 환경)는 현대 프로그래밍 언어와 도구의 기반이 되는 핵심 기술입니다.

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

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.