가상 메모리와 페이지 교체 알고리즘 – OS 메모리 관리


가상 메모리와 페이지 교체 알고리즘은 운영체제 CS 면접에서 가장 자주 등장하는 핵심 주제입니다. 우리가 사용하는 컴퓨터에서 여러 프로그램이 동시에 실행될 수 있는 이유, RAM이 4GB밖에 없는데 16GB짜리 게임이 돌아가는 이유, 메모리가 부족할 때 시스템이 갑자기 느려지는 이유가 모두 가상 메모리 메커니즘 안에 있습니다. 이 글에서는 가상 메모리의 탄생 배경과 페이징 구조, 주소 변환 과정, 페이지 교체 알고리즘의 종류와 성능 비교, 스래싱 발생 원인과 방어 전략까지 운영체제 메모리 관리의 전체 그림을 체계적으로 정리합니다.


목차

  1. 가상 메모리란 무엇인가 – 탄생 배경과 핵심 개념
  2. 페이징 구조와 주소 변환 – 페이지 테이블과 TLB
  3. 페이지 폴트 처리 과정 – 느린 메모리 접근의 전 과정
  4. 페이지 교체 알고리즘 완전 비교 – FIFO·OPT·LRU·Clock
  5. 스래싱(Thrashing) – 메모리 관리 실패의 끝
  6. 실전 관점 – 현대 OS와 언어별 메모리 관리 전략

1. 가상 메모리란 무엇인가 – 탄생 배경과 핵심 개념

물리 메모리만 사용할 때의 문제점

초기 컴퓨터는 프로그램이 물리 메모리(RAM) 주소를 직접 사용했습니다. 이 방식은 세 가지 심각한 문제를 낳았습니다.

① 용량 한계: 프로그램 크기가 물리 RAM을 초과하면 실행 자체가 불가능합니다. 4GB RAM에서 8GB짜리 프로그램은 아예 뜨지 않는 방식입니다.

② 보안 취약: 프로세스 A가 프로세스 B의 메모리 주소를 직접 접근할 수 있습니다. 악성 프로그램이 다른 프로세스의 데이터를 마음대로 읽고 쓸 수 있어 시스템 전체가 위험에 노출됩니다.

③ 단편화(Fragmentation): 프로그램이 시작·종료를 반복하면 물리 메모리에 크고 작은 빈 공간이 불규칙하게 생겨, 총 여유 공간은 충분해도 연속 공간이 부족해 새 프로그램을 올리지 못하는 외부 단편화 문제가 발생합니다.

[외부 단편화 예시]

물리 메모리 (총 16MB):
┌──────┬──────┬──────┬──────┐
│ 4MB  │ 2MB  │ 4MB  │ 6MB  │
│사용중 │ 빈칸 │사용중 │ 빈칸 │
└──────┴──────┴──────┴──────┘
  ← 여유 메모리 합계: 8MB →

새 프로그램 (6MB 연속 필요):
  → 빈 공간 합계 8MB > 6MB이지만
  → 연속된 6MB 공간 없음 → 실행 불가 ❌

가상 메모리의 핵심 아이디어

**가상 메모리(Virtual Memory)**는 각 프로세스에게 실제 물리 메모리와 무관한 독립적인 가상 주소 공간을 부여하는 기법입니다. 프로세스는 자신이 메모리 전체를 독점한 것처럼 착각하며 동작하지만, 실제로는 OS와 하드웨어(MMU)가 협력해 가상 주소를 물리 주소로 변환합니다.

[가상 메모리 핵심 구조]

프로세스 A          프로세스 B
가상 주소 공간       가상 주소 공간
┌──────────┐        ┌──────────┐
│ 0x0000   │        │ 0x0000   │  ← 동일 가상 주소도
│   ...    │        │   ...    │    서로 다른 물리 주소
│ 0xFFFF   │        │ 0xFFFF   │
└──────────┘        └──────────┘
      ↓ MMU(페이지 테이블)  ↓
┌─────────────────────────────────┐
│         물리 메모리 (RAM)        │
│  [A의 페이지][B의 페이지][빈칸]  │
└─────────────────────────────────┘
      ↕ 필요 시 스왑
┌─────────────────────────────────┐
│        디스크 스왑 공간          │
│ [A의 비활성 페이지][B의 일부]    │
└─────────────────────────────────┘

가상 메모리가 제공하는 세 가지 핵심 이점은 다음과 같습니다.

① 메모리 격리(Isolation): 각 프로세스는 독립된 가상 주소 공간을 가지므로 다른 프로세스의 메모리에 접근할 수 없습니다. 한 프로세스가 오동작해도 다른 프로세스와 OS 커널이 보호됩니다.

② 용량 초과 실행(Overcommit): 프로그램 전체를 RAM에 올리지 않고, 현재 필요한 페이지만 올려두고 나머지는 디스크에 저장합니다. 물리 RAM보다 훨씬 큰 프로그램도 실행할 수 있습니다.

③ 편리한 메모리 할당: 페이지 단위로 메모리를 관리하므로 외부 단편화 문제가 사라집니다. 물리적으로 분산된 페이지들이 가상 주소 공간에서는 연속된 것처럼 보입니다.


2. 페이징 구조와 주소 변환 – 페이지 테이블과 TLB

페이지와 프레임의 개념

가상 메모리를 구현하는 가장 보편적인 방법이 **페이징(Paging)**입니다. 페이징은 가상 주소 공간과 물리 메모리를 모두 동일한 크기의 블록으로 나눕니다.

가상 주소 공간의 블록 → 페이지(Page)
물리 메모리의 블록   → 프레임(Frame) 또는 페이지 프레임

일반적인 페이지 크기: 4KB (x86-64 기준)
→ 4GB 가상 주소 공간 = 약 1,048,576개 페이지

[페이지 ↔ 프레임 매핑]
가상 페이지 0  →  물리 프레임 5
가상 페이지 1  →  디스크 (현재 RAM에 없음)
가상 페이지 2  →  물리 프레임 2
가상 페이지 3  →  물리 프레임 9

가상 주소 → 물리 주소 변환 과정

CPU가 가상 주소를 사용하면, MMU(Memory Management Unit) 하드웨어가 페이지 테이블을 참조해 물리 주소로 변환합니다.

[가상 주소 구조 (32비트, 페이지 크기 4KB 기준)]

가상 주소 32비트:
┌──────────────────┬──────────────┐
│  페이지 번호(VPN) │  오프셋(Off) │
│     20 bits      │   12 bits    │
└──────────────────┴──────────────┘
         ↓ 페이지 테이블 조회
┌──────────────────┬──────────────┐
│  프레임 번호(PFN) │  오프셋(Off) │
│     20 bits      │   12 bits    │
└──────────────────┴──────────────┘
         ↓
    물리 주소 = PFN × 4096 + Offset

예시:
  가상 주소 = 0x00003A2F
  VPN = 0x00003 (페이지 3번)
  Offset = 0xA2F (페이지 내 위치)

  페이지 테이블: 가상 페이지 3 → 물리 프레임 9
  물리 주소 = 9 × 4096 + 0xA2F = 0x0009A2F

TLB – 주소 변환 캐시

페이지 테이블은 메모리(RAM)에 저장됩니다. 즉, 모든 메모리 접근마다 페이지 테이블 조회(메모리 접근) + 실제 데이터 접근(메모리 접근) 으로 총 2번의 메모리 접근이 필요합니다. 이 오버헤드를 줄이기 위해 **TLB(Translation Lookaside Buffer)**가 도입됐습니다.

TLB는 최근에 사용한 가상→물리 주소 변환 결과를 저장하는 CPU 내부의 고속 캐시입니다. TLB에 원하는 변환 정보가 있으면(TLB Hit) 페이지 테이블 조회 없이 즉시 물리 주소를 얻습니다.

[TLB 동작 흐름]

CPU 가상 주소 요청
        │
        ▼
  TLB 조회 (CPU 내부, 수 나노초)
        │
   ┌────┴────┐
   │TLB Hit  │ TLB Miss
   │ (~1ns)  │ (~100ns)
   ▼         ▼
물리 주소  페이지 테이블 조회 (RAM)
즉시 반환       │
            물리 주소 계산
                │
            TLB 업데이트 (다음 접근을 위해)
                │
            물리 주소 반환

[TLB 적중률과 성능]
TLB 적중률 99%:
  평균 접근 시간 ≈ 0.99 × 1ns + 0.01 × 100ns = ~2ns
  → 페이지 테이블만 사용(100ns) 대비 50배 빠름

TLB 항목 수: 보통 64~1024개 (매우 한정적)
→ 지역성(Locality)이 좋은 프로그램일수록 TLB 적중률 높음

다단계 페이지 테이블

64비트 시스템에서 단일 페이지 테이블은 수 TB에 달하는 크기가 필요합니다. 이를 해결하기 위해 **다단계 페이지 테이블(Multi-level Page Table)**을 사용합니다.

[x86-64 4단계 페이지 테이블 구조]

가상 주소 (48비트 사용):
┌────┬────┬────┬────┬────────────┐
│ L4 │ L3 │ L2 │ L1 │  오프셋   │
│ 9b │ 9b │ 9b │ 9b │   12b     │
└────┴────┴────┴────┴────────────┘
  ↓    ↓    ↓    ↓
PML4 → PDPT → PD → PT → 물리 프레임

장점: 사용하지 않는 가상 주소 공간에 대해
      하위 레벨 테이블을 생성하지 않음
      → 메모리 절약

3. 페이지 폴트 처리 과정 – 느린 메모리 접근의 전 과정

페이지 폴트란

프로세스가 접근하려는 가상 페이지가 현재 물리 메모리(RAM)에 없을 때 발생하는 예외(Exception)입니다. OS는 이 예외를 처리하여 필요한 페이지를 디스크에서 RAM으로 불러옵니다. 페이지 폴트 자체는 설계된 정상 동작이지만, 너무 자주 발생하면 성능이 급격히 저하됩니다.

[페이지 폴트 처리 전체 흐름]

① CPU: 가상 주소 0x3A2F 접근 시도
        │
② MMU: 페이지 테이블 확인
        │   Present Bit = 0 (RAM에 없음)
        ▼
③ 하드웨어: 페이지 폴트 인터럽트 발생
        │
④ OS: 현재 프로세스 실행 중단, 컨텍스트 저장
        │
⑤ OS: 해당 페이지가 디스크 어디에 있는지 확인
        │
⑥ OS: 빈 물리 프레임 확보
        │   빈 프레임 없으면?
        │   → 페이지 교체 알고리즘 실행
        │     (희생 페이지 선택 → 디스크로 스왑 아웃)
        ▼
⑦ OS: 디스크에서 필요한 페이지를 RAM으로 로드 (스왑 인)
        │   ← 이 단계가 수 ms 소요 (RAM 접근 수십 ns 대비 10만 배 느림)
        ▼
⑧ OS: 페이지 테이블 업데이트 (Present Bit = 1, 프레임 번호 기록)
        │
⑨ OS: TLB 갱신
        │
⑩ OS: 중단된 프로세스 재개 → 원래 명령어부터 재실행

페이지 폴트의 종류

종류설명비용
Minor Fault페이지가 RAM에 있지만 페이지 테이블에 미등록 (공유 메모리, fork() 후 COW)낮음
Major Fault페이지가 디스크에 있어 I/O 로드 필요매우 높음 (~수 ms)
Invalid Fault잘못된 주소 접근 (Segmentation Fault로 이어짐)프로세스 종료

c

// Linux에서 페이지 폴트 통계 확인
// /proc/[pid]/stat 또는 getrusage() 사용

#include <sys/resource.h>
#include <stdio.h>

int main() {
    struct rusage usage;
    getrusage(RUSAGE_SELF, &usage);

    // ru_minflt: Minor Page Fault 횟수
    // ru_majflt: Major Page Fault 횟수 (디스크 I/O 동반)
    printf("Minor Faults: %ld\n", usage.ru_minflt);
    printf("Major Faults: %ld\n", usage.ru_majflt);
    return 0;
}

4. 페이지 교체 알고리즘 완전 비교 – FIFO·OPT·LRU·Clock

왜 페이지 교체 알고리즘이 필요한가

RAM이 가득 찬 상태에서 새 페이지를 올려야 할 때, 어떤 기존 페이지를 디스크로 내보낼지(Victim Page 선택) 결정하는 것이 페이지 교체 알고리즘입니다. 잘못된 선택은 곧 재사용으로 이어져 페이지 폴트를 반복 유발합니다.

아래 예제에서는 물리 프레임 3개, 페이지 참조열 [1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5]로 알고리즘을 비교합니다.

FIFO (First In First Out)

가장 먼저 들어온 페이지를 가장 먼저 교체합니다. 구현이 단순하지만 자주 사용하는 오래된 페이지를 내보낼 수 있습니다.

참조열: 1  2  3  4  1  2  5  1  2  3  4  5
프레임: (크기 3)

참조  프레임 상태         교체 여부
 1   [1, -, -]           Miss (폴트)
 2   [1, 2, -]           Miss
 3   [1, 2, 3]           Miss
 4   [4, 2, 3]  → 1 교체  Miss ← 1이 곧 다시 필요한데 내보냄
 1   [4, 1, 3]  → 2 교체  Miss
 2   [4, 1, 2]  → 3 교체  Miss
 5   [5, 1, 2]  → 4 교체  Miss
 1   [5, 1, 2]            Hit  ✅
 2   [5, 1, 2]            Hit  ✅
 3   [5, 3, 2]  → 1 교체  Miss
 4   [5, 3, 4]  → 2 교체  Miss
 5   [5, 3, 4]            Hit  ✅

총 페이지 폴트: 9회

⚠️ Belady의 역설(Belady's Anomaly):
   FIFO는 프레임 수를 늘려도 페이지 폴트가 오히려 증가하는
   비직관적 현상이 발생할 수 있음

OPT (Optimal Page Replacement)

미래에 가장 오랫동안 사용되지 않을 페이지를 교체합니다. 이론적으로 최적이지만 미래 참조열을 알아야 하므로 실제 구현 불가능합니다. 다른 알고리즘의 성능 기준선(Benchmark)으로만 사용됩니다.

참조열: 1  2  3  4  1  2  5  1  2  3  4  5
프레임: (크기 3)

참조  프레임 상태         결정 근거
 1   [1, -, -]           Miss
 2   [1, 2, -]           Miss
 3   [1, 2, 3]           Miss
 4   [1, 2, 4]  → 3 교체  Miss ← 3은 6칸 후 참조, 4는 0칸 후 → 3 교체
 1   [1, 2, 4]            Hit  ✅
 2   [1, 2, 4]            Hit  ✅
 5   [1, 2, 5]  → 4 교체  Miss ← 4는 4칸 후, 5는 5칸 후 → 4 교체
 1   [1, 2, 5]            Hit  ✅
 2   [1, 2, 5]            Hit  ✅
 3   [3, 2, 5]  → 1 교체  Miss ← 1은 더 이상 참조 없음 → 1 교체
 4   [3, 4, 5]  → 2 교체  Miss ← 2는 더 이상 참조 없음 → 2 교체
 5   [3, 4, 5]            Hit  ✅

총 페이지 폴트: 6회 ← 이론적 최솟값

LRU (Least Recently Used)

가장 오랫동안 사용되지 않은 페이지를 교체합니다. “최근에 사용된 페이지는 곧 다시 사용될 것”이라는 시간적 지역성(Temporal Locality) 원리에 기반합니다. OPT에 가장 근접한 성능을 보이며 실용적입니다.

참조열: 1  2  3  4  1  2  5  1  2  3  4  5
프레임: (크기 3)

참조  프레임 상태 (왼쪽=최근)    교체 여부
 1   [1, -, -]                  Miss
 2   [2, 1, -]                  Miss
 3   [3, 2, 1]                  Miss
 4   [4, 3, 2]  → 1 교체        Miss ← 1이 가장 오래됨
 1   [1, 4, 3]  → 2 교체        Miss ← 2가 가장 오래됨
 2   [2, 1, 4]  → 3 교체        Miss ← 3이 가장 오래됨
 5   [5, 2, 1]  → 4 교체        Miss ← 4가 가장 오래됨
 1   [1, 5, 2]                  Hit  ✅
 2   [2, 1, 5]                  Hit  ✅
 3   [3, 2, 1]  → 5 교체        Miss ← 5가 가장 오래됨
 4   [4, 3, 2]  → 1 교체        Miss ← 1이 가장 오래됨
 5   [5, 4, 3]  → 2 교체        Miss ← 2가 가장 오래됨

총 페이지 폴트: 8회

LRU 구현 방법:
① 카운터 방식: 각 페이지마다 마지막 접근 시각 기록
   → 모든 접근 시 타임스탬프 갱신 → 교체 시 최솟값 탐색
   → 오버헤드 높음

② 스택(더블 링크드 리스트) 방식:
   → 접근 시 해당 페이지를 스택 최상단으로 이동
   → 스택 최하단 = LRU 페이지
   → O(1) 교체 결정 가능

LRU 스택 방식 Java 구현

java

import java.util.*;

/**
 * LRU 페이지 교체 시뮬레이터
 * LinkedHashMap의 accessOrder=true 활용 → LRU 순서 자동 유지
 */
public class LRUPageReplacement {

    private final int frameSize;
    // accessOrder=true: 가장 최근 접근 순으로 순서 유지
    private final LinkedHashMap<Integer, Boolean> frames;
    private int pageFaults = 0;

    public LRUPageReplacement(int frameSize) {
        this.frameSize = frameSize;
        this.frames = new LinkedHashMap<>(frameSize, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<Integer, Boolean> eldest) {
                // 프레임이 가득 찼을 때 가장 오래된 항목(LRU) 자동 제거
                return size() > frameSize;
            }
        };
    }

    public void accessPage(int pageNumber) {
        if (!frames.containsKey(pageNumber)) {
            // 페이지 폴트 발생
            pageFaults++;
            System.out.printf("Page %2d | Miss | Frames: %s%n",
                    pageNumber, frames.keySet());
        } else {
            System.out.printf("Page %2d | Hit  | Frames: %s%n",
                    pageNumber, frames.keySet());
        }
        // 접근 기록 (존재하면 최근 접근으로 갱신, 없으면 추가 후 LRU 제거)
        frames.put(pageNumber, true);
    }

    public int getPageFaults() { return pageFaults; }

    public static void main(String[] args) {
        int[] referenceString = {1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5};
        LRUPageReplacement lru = new LRUPageReplacement(3);

        System.out.println("=== LRU 페이지 교체 시뮬레이션 ===");
        for (int page : referenceString) {
            lru.accessPage(page);
        }
        System.out.println("총 페이지 폴트: " + lru.getPageFaults());
    }
}

Clock (Second Chance) 알고리즘

LRU의 성능을 유지하면서 구현 오버헤드를 줄인 실용적 근사 알고리즘입니다. 실제 Linux OS의 페이지 교체 구현 기반이 되는 알고리즘입니다. 각 페이지에 **참조 비트(Reference Bit)**를 두고, 시계 바늘이 원형으로 돌면서 교체 대상을 선택합니다.

[Clock 알고리즘 동작 원리]

각 프레임에 참조 비트(R) 부여:
  R = 1: 최근에 접근됨
  R = 0: 최근에 접근되지 않음

교체 필요 시 시계 바늘 순서로 순회:
  R = 1 → R을 0으로 낮추고 다음 프레임으로 (두 번째 기회 부여)
  R = 0 → 이 프레임 교체! (한 번도 기회를 못 살린 페이지)

예시:
  프레임 상태: [P1(R=1)] → [P2(R=0)] → [P3(R=1)] → [P4(R=1)]
                                 ↑ 시계 바늘 현재 위치

  교체 필요:
  P1: R=1 → 0으로 변경, 통과
  P2: R=0 → 교체 대상 선택! ✅

알고리즘 성능 최종 비교

알고리즘폴트 수구현 복잡도실제 사용특이점
FIFO9회낮음드묾Belady 역설 가능
OPT6회불가벤치마크용이론적 최적
LRU8회중간일부 사용성능 우수, 오버헤드 있음
Clock~8회낮음OS 주력LRU 근사, 효율적
LFU가변중간특수 목적접근 빈도 기반
NRU가변낮음OS 일부R·M 비트 조합

5. 스래싱(Thrashing) – 메모리 관리 실패의 끝

스래싱이란 무엇인가

**스래싱(Thrashing)**은 프로세스가 실제 작업을 수행하는 시간보다 페이지 교체(스왑 인/아웃)에 소비하는 시간이 더 많아지는 상태입니다. 메모리 관리가 실패했을 때 나타나는 최악의 상황으로, 시스템 전체 성능이 극도로 저하됩니다.

[스래싱 발생 메커니즘]

① 물리 메모리 부족 상태에서 프로세스 수 증가
        ↓
② 각 프로세스에 할당되는 프레임 수 감소
        ↓
③ 프로세스의 워킹셋(Working Set)을 담기에 프레임 부족
        ↓
④ 페이지 폴트 빈도 폭증
        ↓
⑤ OS가 페이지 교체 작업에 CPU 대부분 소비
        ↓
⑥ CPU 이용률 급락 → OS는 이용률 회복을 위해 프로세스 추가 실행
        ↓
⑦ 메모리 경쟁 더욱 심화 → 스래싱 악순환 ♻️

[CPU 이용률 vs 다중프로그래밍 정도 그래프]

CPU
이용률
  │       ★ 최적점
  │      /  \
  │     /    \←── 스래싱 구간
  │    /      \
  │   /        \
  └────────────────→ 동시 실행 프로세스 수

스래싱 방지 전략

① 워킹셋(Working Set) 모델

프로세스가 최근 Δ번의 메모리 참조에서 사용한 페이지 집합을 워킹셋이라 합니다. 각 프로세스의 워킹셋 크기 합이 가용 프레임 수를 초과하면 일부 프로세스를 일시 중단(Suspend)해 스래싱을 예방합니다.

[워킹셋 모델 예시]

프로세스 A 워킹셋 (Δ=10): {1, 2, 3, 4, 5} → 5 프레임 필요
프로세스 B 워킹셋 (Δ=10): {6, 7, 8}       → 3 프레임 필요
프로세스 C 워킹셋 (Δ=10): {1, 9, 10, 11}  → 4 프레임 필요

총 워킹셋 크기: 5 + 3 + 4 = 12 프레임

가용 프레임: 10개 → 부족! (12 > 10)
→ 프로세스 C를 일시 중단 → A, B만 실행 (8 프레임 필요)
→ C의 페이지를 디스크로 → A, B 안정적 실행

② PFF(Page Fault Frequency) 기반 조절

페이지 폴트 발생 빈도를 측정해 프레임 수를 동적으로 조절합니다.

페이지 폴트율 > 상한선 → 프레임 수 증가
페이지 폴트율 < 하한선 → 프레임 수 감소 (여유분 반환)
프레임 추가 불가 상황 → 해당 프로세스 일시 중단

[PFF 제어 다이어그램]

폴트율
  │  상한선 ──────────────────
  │          프레임 추가 구간
  │  하한선 ──────────────────
  │          정상 운영 구간
  │          프레임 반환 구간
  └────────────────────────→ 시간

③ 메모리 압축과 OOM Killer (Linux)

bash

# Linux 스왑 사용량 및 스래싱 징후 확인
vmstat 1 5
# 출력 항목:
# si (swap in):  디스크 → RAM 페이지 수/초  (높으면 스래싱 의심)
# so (swap out): RAM → 디스크 페이지 수/초  (높으면 스래싱 의심)
# wa (I/O wait): I/O 대기 CPU 비율          (높으면 스왑 I/O 병목)

# 페이지 폴트 통계 실시간 확인
sar -B 1 5
# pgfault/s: 초당 페이지 폴트 수

# OOM Killer 로그 확인 (메모리 고갈 시 OS가 프로세스 강제 종료)
dmesg | grep -i "oom\|killed"

6. 실전 관점 – 현대 OS와 언어별 메모리 관리 전략

Linux 메모리 관리 아키텍처

Linux는 **Buddy System + Slab Allocator + Page Frame Reclaim Algorithm(PFRA)**을 조합해 메모리를 관리합니다.

[Linux 메모리 관리 계층]

애플리케이션 (malloc, new)
        ↓
glibc (ptmalloc) / tcmalloc / jemalloc
        ↓
커널 slab/slub 할당자 (소형 객체 효율화)
        ↓
Buddy System (연속 페이지 프레임 할당, 외부 단편화 최소화)
        ↓
물리 페이지 프레임 관리
        ↓
스왑 서브시스템 (kswapd 데몬)

[kswapd 동작]
  → 백그라운드에서 메모리 여유 공간 모니터링
  → 여유 공간이 low watermark 이하로 떨어지면 페이지 회수 시작
  → LRU 변형 알고리즘 (Active/Inactive 리스트 분리) 사용

Java JVM의 가상 메모리 활용

java

// JVM 메모리 영역과 가상 메모리 관계

/**
 * JVM은 OS의 가상 메모리 위에서 동작
 * JVM 자체도 가상 주소 공간을 사용하며,
 * GC가 메모리 정리를 담당 (OS의 페이지 교체와 별개)
 */
public class JvmMemoryInfo {

    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();

        // JVM 힙 메모리 정보
        long maxMemory  = runtime.maxMemory();    // -Xmx 설정값
        long totalMemory = runtime.totalMemory(); // 현재 JVM이 OS에서 확보한 메모리
        long freeMemory = runtime.freeMemory();   // JVM 힙 내 여유 공간
        long usedMemory = totalMemory - freeMemory;

        System.out.printf("JVM 최대 힙: %,d MB%n", maxMemory / 1024 / 1024);
        System.out.printf("JVM 확보 힙: %,d MB%n", totalMemory / 1024 / 1024);
        System.out.printf("JVM 사용 힙: %,d MB%n", usedMemory / 1024 / 1024);
        System.out.printf("JVM 여유 힙: %,d MB%n", freeMemory / 1024 / 1024);

        // JVM 힙이 커질수록 OS 페이지 교체 압박 증가
        // → GC 튜닝과 OS 메모리 관리를 함께 고려해야 함
    }
}

/*
 * JVM 메모리 튜닝 핵심 옵션:
 * -Xms2g        : 초기 힙 크기 2GB (OS에서 즉시 예약)
 * -Xmx4g        : 최대 힙 크기 4GB
 * -XX:+UseG1GC  : G1 GC 사용 (대용량 힙 권장)
 *
 * ⚠️ Xmx를 물리 RAM 크기 이상으로 설정하면
 *    JVM 힙이 스왑 공간으로 밀려나 GC 성능 급락
 *    → 물리 RAM의 75~80% 이하로 설정 권장
 */

언어별 메모리 관리 방식 비교

언어메모리 관리 방식페이지 교체 관여도주의사항
C/C++수동 (malloc/free)직접 관여 가능메모리 누수, 댕글링 포인터
JavaGC (자동)JVM이 추상화힙 크기 > RAM 시 스왑 성능 급락
Python레퍼런스 카운팅 + GCOS에 위임CPython 메모리 반환 지연
GoGC (자동, 저지연)OS에 위임GOGC 튜닝으로 GC 빈도 조절
Rust소유권 시스템 (컴파일 타임)직접 관여 가능GC 없음, 가장 예측 가능

메모리 성능 진단 명령어 모음

bash

# 전체 메모리 사용 현황
free -h
# total: 총 RAM / used: 사용 중 / buff/cache: 캐시 / available: 실제 가용

# 프로세스별 메모리 상세 (가상/물리 메모리 구분)
ps aux --sort=-%mem | head -10
# VSZ: 가상 메모리 크기 / RSS: 실제 물리 메모리 사용량

# 페이지 활동 실시간 모니터링
vmstat -SM 1
# swpd: 스왑 사용량 / si/so: 스왑 인/아웃 속도

# 특정 프로세스 메모리 맵 상세 확인
pmap -x [PID]

# 메모리 압박 지표 (PSI: Pressure Stall Information, Linux 4.20+)
cat /proc/pressure/memory
# some: 하나 이상의 프로세스가 메모리 대기 중인 시간 비율
# full: 모든 프로세스가 메모리 대기 중인 시간 비율 (스래싱 지표)

결론

가상 메모리와 페이지 교체 알고리즘의 핵심은 세 가지로 요약됩니다. 첫째, 가상 메모리는 프로세스 격리·용량 초과 실행·단편화 해소라는 세 문제를 페이지 테이블과 MMU로 동시에 해결한 운영체제의 핵심 설계입니다. 둘째, LRU와 Clock 알고리즘은 시간적 지역성을 활용해 페이지 폴트를 최소화하며, 실제 OS는 구현 효율을 고려해 Clock 계열 알고리즘을 채택합니다. 셋째, 스래싱은 메모리 관리 실패의 끝점이며 워킹셋 모델과 PFF 기반 동적 프레임 조절로 예방해야 합니다. 오늘 배운 개념을 바탕으로 vmstatpmap으로 직접 자신의 시스템 메모리 상태를 분석해 보세요.


⚠️ 면책 고지: 본 글은 기술 학습 및 CS 면접 준비 목적으로 작성된 참고 자료입니다. 설명된 알고리즘 동작 및 수치는 일반적인 교육 환경 기준이며, 실제 운영체제 구현은 버전·아키텍처·커널 설정에 따라 상이할 수 있습니다. 시스템 레벨 설정 변경 시 반드시 공식 문서와 전문가 검토를 거치시기 바랍니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다