[OS] 프로세스 메모리 구조 (Code, Data, Heap, Stack)
프로세스 메모리 구조 (Code, Data, Heap, Stack)
📌 개념
프로세스가 실행될 때 운영체제는 RAM을 효율적으로 사용하기 위해 메모리 공간을 역할별로 구분하여 관리한다. 이를 프로세스 메모리 구조(Process Memory Layout) 라고 하며, 크게 Code, Data, Heap, Stack 4가지 영역으로 나뉜다.
🗂️ 전체 메모리 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
14
높은 주소
┌─────────────────┐
│ Stack 영역 │ ← 함수 호출 시 생성, 아래 방향으로 성장 ↓
├─────────────────┤
│ │
│ (빈 공간) │
│ │
├─────────────────┤
│ Heap 영역 │ ← 동적 할당, 위 방향으로 성장 ↑
├─────────────────┤
│ Data / BSS │ ← 전역 변수, 정적 변수
├─────────────────┤
│ Code 영역 │ ← 실행 가능한 기계어 코드
낮은 주소
Stack과 Heap이 서로 반대 방향으로 성장하는 이유
두 영역이 가운데 빈 공간을 공유하며 메모리를 최대한 효율적으로 활용하기 위해서다.
각각 반대 방향에서 자라다가 만나는 순간 메모리 부족(Stack Overflow, Heap Exhaustion)이 발생한다.
1. 메모리 영역별 특징
💻 코드 (Code / Text) 영역
역할: 개발자가 작성한 소스 코드가 컴파일되어, CPU가 실행할 수 있는 기계어 명령어(Instruction) 형태로 저장되는 공간이다.
특징
- 프로그램 실행 중 내용이 변경되지 않으므로 Read-Only(읽기 전용) 으로 보호된다.
- 악의적인 코드 주입 공격(Buffer Overflow 등)을 방어하는 효과도 있다.
if,for, 함수 등 우리가 작성한 모든 로직이 컴파일 과정에서0101...형태의 기계어로 변환되어 이 영역에 저장된다. CPU는 이 영역에서 명령어를 하나씩 읽어 실행한다.
📊 데이터 (Data / BSS) 영역
역할: 프로그램 시작과 동시에 할당되어 프로그램이 종료될 때까지 유지되는 변수들이 저장된다.
| 구분 | 저장 대상 | 예시 |
|---|---|---|
| Data 영역 | 초기화된 전역 변수, 정적(static) 변수 | int count = 0; |
| BSS 영역 | 초기화되지 않은 전역 변수, 정적 변수 | int count; |
BSS(Block Started by Symbol)
초기화되지 않은 변수들을 따로 관리하는 이유는 실행 파일 크기를 줄이기 위해서다.
초기값이 없으면 파일에 실제 값을 저장할 필요 없이 “이 크기만큼 0으로 채워라”는 정보만 기록하면 되기 때문이다.
특징: 프로그램 어디서나 접근할 수 있는 전역적(Global) 인 성격을 가진다.
🏗️ 힙 (Heap) 영역
역할: 런타임(프로그램 실행 중)에 사용자가 직접 크기를 결정하여 동적으로 할당하는 공간이다.
특징
- C언어의
malloc(), Java/JavaScript의new연산자 등을 통해 생성되는 객체나 배열이 여기에 저장된다. - 사용 후 반드시 메모리 해제(free) 를 해줘야 하며, 안 하면 메모리 누수(Memory Leak) 가 발생한다.
- 할당/해제가 자유롭지만 그만큼 단편화(Fragmentation) 가 발생하기 쉽다.
1
2
3
4
// C 언어 예시
int* arr = (int*)malloc(sizeof(int) * 100); // Heap에 할당
// ... 사용 ...
free(arr); // 반드시 해제! 안 하면 Memory Leak 발생
1
2
// Java 예시 (GC가 자동 해제)
String name = new String("Hello"); // Heap에 할당
Java, Python, JavaScript 같은 언어는 가비지 컬렉터(GC, Garbage Collector) 가 사용하지 않는 Heap 메모리를 자동으로 회수한다.
다만, GC가 언제 실행될지 예측이 어렵고 실행되는 동안 Stop-The-World(성능 저하) 가 발생할 수 있다.
📚 스택 (Stack) 영역
역할: 함수가 호출될 때 필요한 지역 변수, 매개변수, 리턴 주소 등이 임시로 저장되는 공간이다.
특징
- 함수가 끝나면 자동으로 메모리가 해제된다 (자동 관리).
- 후입선출(LIFO, Last In First Out) 방식으로 작동하며, 할당 속도가 매우 빠르다.
- 공간이 넘치면 StackOverflow 에러가 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
함수 호출 순서: main() → a() → b()
Stack 상태:
┌──────────────┐
│ b() frame │ ← 가장 최근에 쌓인 것 (TOP)
├──────────────┤
│ a() frame │
├──────────────┤
│ main() frame │ ← 가장 먼저 쌓인 것 (BOTTOM)
└──────────────┘
b() 리턴 → b() frame 제거 → a()로 돌아감 (LIFO)
재귀 함수(Recursive Function) 를 종료 조건 없이 너무 깊이 호출하면
Stack이 가득 차 StackOverflow가 발생하는 것도 이 때문이다.
2. 공유되는 부분 vs 독립적인 부분
프로그램 안에서 여러 개의 실행 흐름인 스레드(Thread) 가 동작할 때,
메모리 공유 방식이 시스템 프로그래밍의 핵심 개념 중 하나다.
🤝 공유되는 영역 (공유 자원)
Code, Data, Heap 영역은 같은 프로세스 내 모든 스레드가 공유한다.
이유
- 하나의 프로그램(프로세스) 안에서 움직이는 스레드들은 같은 코드를 실행해야 하고 (Code)
- 같은 전역 설정이나 상태를 공유해야 하며 (Data)
- 동일한 동적 데이터를 처리해야 효율적이기 때문이다 (Heap)
⚠️ 주의: 특히 Heap과 Data 영역은 여러 스레드가 동시에 수정할 수 있기 때문에,
경쟁 조건(Race Condition) 이 발생할 수 있다. 이를 방지하려면 동기화(Mutex, Semaphore, Lock 등) 처리가 필수다.
1
2
3
4
5
6
Race Condition 예시:
Thread A: count 읽기(5) → ...대기... → count에 6 저장
Thread B: count 읽기(5) → count에 6 저장
결과: count = 6 (기댓값 7과 다름! 데이터 꼬임 발생)
🔒 공유되지 않는 영역 (독립 자원)
Stack 영역은 각 스레드마다 독립적으로 할당된다.
이유: 스레드는 각자 다른 함수를 호출하며 자신만의 실행 흐름을 이어가야 한다.
만약 Stack을 공유한다면:
1
2
3
Thread A가 funcA() 호출 → 리턴 주소를 Stack에 저장
Thread B가 funcB() 호출 → 같은 Stack에 리턴 주소 덮어씀
Thread A가 funcA() 리턴 → 엉뚱한 주소로 점프 → 💥 대참사
따라서 독립적인 실행 흐름 보장을 위해 Stack은 스레드마다 고유하게 할당된다.
3. 한눈에 보는 요약 표
| 메모리 영역 | 저장되는 데이터 | 할당 시점 | 해제 시점 | 스레드 간 공유 |
|---|---|---|---|---|
| Code | 컴파일된 기계어 코드 | 컴파일/링크 시 | 프로그램 종료 시 | ✅ 공유 |
| Data | 전역 변수, 정적(static) 변수 | 프로그램 시작 시 | 프로그램 종료 시 | ✅ 공유 |
| Heap | 동적 할당 데이터 (new, malloc) | 런타임 (실행 중 코드 요청 시) | 명시적 해제 or GC | ✅ 공유 (동기화 필요) |
| Stack | 지역 변수, 매개변수, 리턴 주소 | 함수 호출 시 | 함수 반환 시 | ❌ 스레드별 독립 |
4. 프로세스 vs 스레드 메모리 비교
| 구분 | 프로세스 (Process) | 스레드 (Thread) |
|---|---|---|
| 메모리 공간 | 독립적인 Code/Data/Heap/Stack | Code/Data/Heap 공유, Stack만 독립 |
| 생성 비용 | 높음 (메모리 복사 필요) | 낮음 (Stack만 새로 생성) |
| 통신 방법 | IPC (파이프, 소켓, 공유 메모리 등) | 공유 메모리 직접 접근 |
| 안전성 | 높음 (독립 공간, 서로 영향 없음) | 낮음 (공유 자원 오염 가능) |
| 대표 사용 사례 | 크롬 탭, 서버 워커 프로세스 | 웹 서버 요청 처리, GUI + 백그라운드 작업 |
스레드는 메모리 공유 덕분에 통신 속도가 빠르고 생성 비용이 낮지만,
그만큼 동기화 문제에 항상 주의해야 한다.