malloc을 호출하면 실제로 어떤 일이 일어날까? OS한테 바로 메모리를 달라고 하는 걸까, 아니면 뭔가 중간 단계가 있을까?

Java에서 GC가 알아서 메모리를 정리해준다는데, 그럼 왜 OutOfMemoryError가 터지는 걸까요? 가상 메모리 위에서 실제로 메모리가 할당되고 해제되는 과정을, C의 malloc부터 JVM의 G1 GC까지 한 단계씩 내려가 보겠습니다.

프로세스 메모리 구조 복습

프로세스와 스레드 글에서도 다뤘지만, 메모리 관리를 이야기하려면 이 구조를 다시 짚고 가야 해요.

PLAINTEXT
[높은 주소]
┌─────────────────┐
│     Stack       │  ← 지역 변수, 함수 호출 정보
│       ↓         │
│                 │
│       ↑         │
│     Heap        │  ← malloc, new로 동적 할당
├─────────────────┤
│     BSS         │  ← 초기화 안 된 전역/정적 변수
├─────────────────┤
│     Data        │  ← 초기화된 전역/정적 변수
├─────────────────┤
│     Code(Text)  │  ← 기계어 명령어
└─────────────────┘
[낮은 주소]

각 영역의 특성을 간단히 정리하면 이렇습니다.

영역저장 대상생명주기크기 결정
Code컴파일된 기계어프로세스 시작~종료컴파일 타임
Data초기화된 전역/정적 변수프로세스 시작~종료컴파일 타임
BSS초기화 안 된 전역/정적 변수프로세스 시작~종료컴파일 타임
Heap동적 할당 데이터malloc~free런타임
Stack지역 변수, 매개변수, 리턴 주소함수 호출~리턴런타임 (보통 고정 크기)

Stack vs Heap

"스택과 힙의 차이"를 이해하려면, 단순히 "스택은 LIFO, 힙은 동적 할당"으로 끝내면 안 됩니다. 차이가 발생하는 이유까지 설명할 수 있어야 해요.

할당/해제 속도

스택은 ** 스택 포인터(SP)를 이동시키는 것만으로** 할당과 해제가 끝납니다. 어셈블리로 보면 sub rsp, 16 한 줄이에요. O(1)이고 극도로 빠릅니다.

힙은 그렇지 않아요. malloc을 호출하면 내부적으로 프리 리스트(free list)를 순회하면서 적절한 크기의 빈 블록을 찾아야 합니다. 찾은 블록을 쪼개고, 메타데이터를 기록하고, 포인터를 반환해요. free할 때도 인접한 빈 블록을 병합하는 작업(coalescing)이 필요할 수 있습니다.

생명주기

스택에 할당된 변수는 함수가 리턴되면 자동으로 사라집니다. 별도의 해제 코드가 필요 없어요. 힙에 할당된 메모리는 명시적으로 해제하지 않으면 프로세스가 종료될 때까지 살아 있습니다. 이게 메모리 릭의 근본 원인이에요.

스레드 공유

스택은 ** 스레드마다 독립적 **입니다. 각 스레드가 자신만의 스택을 가지고 있어서, 다른 스레드의 스택을 건드리지 않아요(물론 포인터로 접근하면 물리적으로 가능하긴 한데, 그건 버그입니다). 반면 힙은 같은 프로세스 내의 ** 모든 스레드가 공유 **해요. 그래서 힙에 할당된 데이터를 여러 스레드가 동시에 읽고 쓰면 동기화 문제가 발생합니다.

크기 제한

스택 크기는 보통 OS가 고정합니다. 리눅스 기본값이 8MB 정도예요. 재귀가 너무 깊어지면 ** 스택 오버플로우 **가 터집니다. 힙은 가상 메모리 공간이 허용하는 한 계속 커질 수 있어요. 물론 물리 메모리와 스왑이 부족해지면 mallocNULL을 반환하거나 OOM Killer가 프로세스를 죽입니다.


동적 메모리 할당

malloc / calloc / realloc / free

C에서 동적 메모리를 다루는 기본 함수들입니다.

C
// malloc: 초기화 없이 할당. 내용은 쓰레기 값
int *arr = (int *)malloc(sizeof(int) * 10);

// calloc: 0으로 초기화하면서 할당
int *arr2 = (int *)calloc(10, sizeof(int));

// realloc: 기존 블록의 크기 변경. 내부적으로 새 블록에 복사할 수도 있음
arr = (int *)realloc(arr, sizeof(int) * 20);

// free: 해제
free(arr);
free(arr2);

핵심 포인트 몇 가지:

  • malloc(0)은 구현에 따라 NULL 또는 유효한 포인터를 반환합니다. 표준이 정하지 않은 부분이에요.
  • free 후에 포인터를 NULL로 설정하지 않으면 dangling pointer 문제가 생깁니다. free(ptr); ptr = NULL; 습관을 들여야 해요.
  • reallocNULL을 넘기면 malloc처럼 동작합니다. 크기를 0으로 넘기면 free처럼 동작해요(구현 의존).

내부 구현: sbrk와 mmap

malloc이 실제로 OS한테 메모리를 달라고 하는 방법은 크게 두 가지입니다.

sbrk (brk)

프로세스의 힙 영역 끝(program break)을 늘리는 시스템 콜입니다. sbrk(4096)을 하면 힙이 4KB만큼 커져요. 간단하지만, 힙의 끝부분에서만 확장/축소가 가능하다는 제약이 있습니다. 중간에 빈 공간이 있어도 program break를 줄일 수 없으면 OS에 메모리를 돌려줄 수 없어요.

mmap

가상 메모리에 새로운 매핑을 만듭니다. 힙 영역이 아닌 별도의 가상 주소 공간에 할당하므로, 해제할 때 munmap으로 바로 OS에 돌려줄 수 있어요. 보통 큰 블록(glibc 기준 128KB 이상)은 mmap으로, 작은 블록은 sbrk로 할당합니다.

glibc의 malloc 구현(ptmalloc2)은 이 두 가지를 상황에 따라 섞어 씁니다. 아레나(arena), 빈(bin) 같은 자료구조로 프리 리스트를 관리하고, 멀티스레드 환경에서는 아레나를 여러 개 두어 락 경합을 줄여요.


메모리 할당 전략

OS나 메모리 할당자가 빈 블록 중 어떤 걸 고를 것인가의 문제입니다. 공부하다 보니 여기서 많이 헷갈렸어요.

First Fit

프리 리스트를 앞에서부터 순회하면서 요청 크기 이상인 ** 첫 번째 블록 **을 선택합니다. 빠르지만, 리스트 앞쪽에 작은 조각들이 쌓이는 경향이 있어요. 다음 검색을 마지막으로 할당한 위치부터 시작하는 Next Fit 변형도 있습니다.

Best Fit

요청 크기에 ** 가장 가까운(가장 작은 차이)** 블록을 선택합니다. 남는 조각이 최소화될 것 같지만, 실제로는 아주 작은 조각들이 많이 생겨서 오히려 외부 단편화가 심해질 수 있어요. 게다가 전체 리스트를 순회해야 해서 느립니다.

Worst Fit

** 가장 큰 블록 **을 선택합니다. 남은 조각이 충분히 커서 다른 요청에 쓸 수 있을 거라는 아이디어인데, 실험적으로 성능이 좋지 않아요.

Buddy System

블록 크기를 항상 2의 거듭제곱으로 관리합니다. 128KB를 요청하면 128KB 블록을 줘요. 없으면 256KB를 반으로 쪼개서(buddy) 하나를 줍니다. 해제할 때는 buddy가 둘 다 비어 있으면 다시 합쳐요. 리눅스 커널의 물리 메모리 할당이 이 방식을 씁니다.

장점은 병합이 쉽고 빠르다는 것이고, 단점은 ** 내부 단편화 **가 발생한다는 점이에요. 65KB를 요청하면 128KB 블록을 할당해야 하니까 63KB가 낭비됩니다.


메모리 단편화

외부 단편화 (External Fragmentation)

전체 빈 공간은 충분한데, 연속된 빈 공간이 없어서 할당에 실패하는 상황입니다.

PLAINTEXT
| 사용 | 빈(30KB) | 사용 | 빈(20KB) | 사용 | 빈(40KB) |

총 90KB가 비어 있지만 50KB짜리 연속 블록이 필요하면 할당할 수 없어요.

내부 단편화 (Internal Fragmentation)

할당된 블록 내부에서 실제로 사용하지 않는 공간이 낭비되는 것입니다. Buddy System에서 65KB를 요청했는데 128KB를 줬으면, 63KB가 내부 단편화예요. 페이징에서도 마지막 페이지가 꽉 차지 않으면 내부 단편화가 발생합니다.

해결 방법

** 컴팩션 (Compaction)**

사용 중인 블록들을 한쪽으로 몰아서 빈 공간을 하나로 합치는 방법입니다. 직관적이지만 비용이 커요. 블록을 옮기는 동안 해당 메모리를 참조하는 모든 포인터를 업데이트해야 합니다. C/C++처럼 원시 포인터를 쓰는 언어에서는 사실상 불가능하고, GC가 있는 언어(Java, C# 등)에서는 GC가 컴팩션을 수행하기도 해요.

** 페이징 (Paging)**

메모리를 고정 크기의 페이지 단위로 나누면 외부 단편화가 원천적으로 사라집니다. 연속되지 않은 물리 프레임에 매핑하면 되니까요. 대신 내부 단편화는 발생할 수 있어요(마지막 페이지). 자세한 내용은 가상 메모리 글을 참고해 주세요.

** 세그먼테이션 (Segmentation)**

논리적인 단위(코드, 데이터, 스택)로 메모리를 나누는 방법인데, 가변 크기라서 외부 단편화 문제는 여전합니다. 현대 OS에서는 페이징과 조합해서 쓰거나, 페이징 위주로 운영해요.


메모리 릭 (Memory Leak)

할당했는데 해제하지 않아서, 더 이상 접근할 수 없는데도 메모리를 차지하고 있는 상태입니다. 장시간 실행되는 서버 프로그램에서 특히 치명적이에요.

흔한 원인

C
void leak() {
    char *buf = malloc(1024);
    if (error_condition) {
        return;  // buf를 free하지 않고 리턴 → 릭
    }
    free(buf);
}
  • 에러 경로에서 free 빼먹기
  • 자료구조에서 노드를 제거하면서 메모리 해제를 안 하기
  • 순환 참조 (특히 Reference Counting GC에서)
  • 이벤트 리스너나 콜백을 등록해놓고 해제 안 하기

탐지 도구

Valgrind (Memcheck)

C/C++ 프로그램의 메모리 오류를 찾아주는 대표적인 도구입니다. 할당/해제를 전부 추적해서 프로그램 종료 시 해제되지 않은 블록, use-after-free, 버퍼 오버런 등을 보고해요. 다만 프로그램이 5~20배 느려집니다.

BASH
valgrind --leak-check=full ./my_program

AddressSanitizer (ASan)

컴파일 타임에 계측 코드를 삽입하는 방식입니다. Valgrind보다 훨씬 빠르고(2배 정도 오버헤드), use-after-free, 버퍼 오버플로우, 스택 오버플로우 등을 탐지해요. GCC, Clang에서 -fsanitize=address 플래그로 활성화합니다.

BASH
gcc -fsanitize=address -g my_program.c -o my_program
./my_program

LeakSanitizer (LSan)

ASan에 포함되어 있거나 단독으로 쓸 수 있습니다. 프로그램 종료 시점에 해제되지 않은 메모리를 보고해요.


가비지 컬렉션 (Garbage Collection)

C/C++에서는 프로그래머가 직접 free를 호출해야 하지만, Java, Python, Go, JavaScript 같은 언어에서는 ** 가비지 컬렉터(GC)**가 더 이상 사용되지 않는 객체를 자동으로 해제합니다. 편하지만 공짜는 아니에요. GC가 돌 때 성능 비용이 발생합니다.

Reference Counting

각 객체가 자신을 참조하는 포인터의 개수(참조 카운트)를 관리합니다. 카운트가 0이 되면 즉시 해제해요.

** 장점**

  • 해제 시점이 예측 가능합니다 — 참조가 끊어지는 즉시 해제
  • 구현이 비교적 단순해요

** 단점**

  • ** 순환 참조를 처리 못 합니다.** A→B, B→A로 서로 참조하면 카운트가 영원히 1 이상이라 해제가 안 돼요.
  • 참조를 복사할 때마다 카운트를 갱신해야 해서 오버헤드가 있습니다
  • 멀티스레드 환경에서는 카운트 갱신에 원자적 연산이 필요해요

Python은 Reference Counting을 기본으로 쓰면서, 순환 참조를 잡기 위해 별도의 cycle detector를 돌립니다. Swift의 ARC(Automatic Reference Counting)도 이 방식인데, weak/unowned 참조로 순환 참조를 개발자가 직접 끊어줘야 해요.

Mark-and-Sweep

**Mark 단계 **: GC 루트(스택 변수, 전역 변수, 레지스터)에서 시작해서 도달 가능한 모든 객체를 마킹합니다.

**Sweep 단계 **: 전체 힙을 순회하면서 마킹되지 않은 객체를 해제해요.

PLAINTEXT
GC Roots

   ├──→ A ──→ B ──→ C

   └──→ D

E ──→ F  (루트에서 도달 불가 → Sweep 대상)

순환 참조도 문제없이 수거됩니다. 루트에서 도달 불가능하면 서로 참조하고 있더라도 전부 Sweep 대상이에요. 단점은 GC가 돌 때 Stop-The-World(STW) 가 발생한다는 점입니다. 마킹하는 동안 객체 그래프가 변하면 안 되니까 애플리케이션 스레드를 멈춰야 해요.

Mark-Compact 는 Sweep 대신 살아 있는 객체를 한쪽으로 몰아서 외부 단편화를 해결하는 변형입니다. 컴팩션 비용이 추가되지만 메모리 활용도가 높아져요.


JVM 메모리 구조

Java를 다룬다면 빠질 수 없는 주제입니다. JVM이 메모리를 어떻게 나눠서 관리하는지 알아야 GC 동작이 이해돼요.

전체 구조

PLAINTEXT
┌──────────────────────────────────────────────┐
│                 JVM Memory                   │
├──────────┬───────────────────────────────────┤
│          │  Young Generation                 │
│          │  ┌───────┬──────────┬──────────┐  │
│   Heap   │  │ Eden  │Survivor0│Survivor1 │  │
│          │  └───────┴──────────┴──────────┘  │
│          ├───────────────────────────────────┤
│          │  Old Generation                   │
├──────────┼───────────────────────────────────┤
│Non-Heap  │  Metaspace (Java 8+)              │
│          │  Code Cache                       │
├──────────┼───────────────────────────────────┤
│ Per-     │  Stack (스레드별)                   │
│ Thread   │  PC Register                      │
│          │  Native Method Stack               │
└──────────┴───────────────────────────────────┘

Young Generation

새로 생성된 객체가 처음 들어가는 곳입니다. 대부분의 객체는 금방 쓸모없어진다는 "약한 세대 가설(Weak Generational Hypothesis)" 에 기반해요.

Eden

객체가 new로 생성되면 먼저 Eden에 들어갑니다. Eden이 가득 차면 Minor GC가 발생해요.

Survivor (S0, S1)

Minor GC에서 살아남은 객체가 Survivor 영역으로 이동합니다. S0와 S1은 번갈아 사용돼요. GC가 발생하면 Eden + 현재 사용 중인 Survivor에서 살아 있는 객체를 다른 Survivor로 복사합니다. 이 과정에서 age(GC를 살아남은 횟수)가 증가하고, 임계값(기본 15)을 넘기면 Old Generation으로 승격(promotion)됩니다.

Old Generation

오래 살아남은 객체가 모이는 곳입니다. Young보다 크기가 크고, GC 빈도는 낮지만 한 번 돌 때 시간이 오래 걸려요. Old가 가득 차면 Major GC(Full GC)가 발생합니다.

Metaspace

Java 8부터 PermGen을 대체한 영역입니다. 클래스 메타데이터, 메서드 데이터 등이 저장돼요. PermGen과 달리 네이티브 메모리를 사용하므로 기본적으로 크기 제한이 없습니다(물론 -XX:MaxMetaspaceSize로 제한할 수 있어요). 클래스가 GC에 의해 수거되면 Metaspace도 줄어듭니다.


JVM GC 종류

Minor GC

Young Generation에서 발생합니다. Eden이 가득 차면 트리거되고, Eden + 활성 Survivor의 살아 있는 객체를 비활성 Survivor로 복사하는 방식이에요. 죽은 객체가 대부분이라 보통 수 밀리초 내에 끝납니다. STW가 발생하지만 시간이 짧아서 큰 문제가 안 돼요.

Major GC (Full GC)

Old Generation까지 포함해서 정리합니다. Young + Old 전체를 대상으로 하므로 시간이 오래 걸려요. 실시간성이 중요한 서비스에서는 Full GC가 수백 ms~수 초 동안 STW를 유발할 수 있어서 큰 이슈가 됩니다.

Serial GC

싱글 스레드로 GC를 수행합니다. 클라이언트 VM이나 메모리가 작은 환경에서 사용해요. 요즘 서버 환경에서는 거의 쓰지 않습니다.

Parallel GC (Throughput GC)

여러 스레드로 GC를 병렬 수행합니다. Java 8의 기본 GC였어요. 처리량은 높지만 STW 시간 자체를 줄이는 데는 한계가 있습니다.

CMS (Concurrent Mark-Sweep)

대부분의 마킹 작업을 애플리케이션 스레드와 동시에 수행해서 STW를 줄입니다. 하지만 컴팩션을 하지 않아서 외부 단편화가 쌓이고, 결국 Full GC가 필요해져요. Java 9에서 deprecated, Java 14에서 제거됐습니다.

G1 GC (Garbage First)

Java 9부터 기본 GC입니다. 힙을 고정 크기의 Region(보통 1~32MB)으로 나눠요. 각 Region은 Eden, Survivor, Old 중 하나의 역할을 동적으로 맡습니다.

PLAINTEXT
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│  E  │  E  │  S  │  O  │  O  │  E  │  H  │  O  │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
 E=Eden  S=Survivor  O=Old  H=Humongous(큰 객체)

핵심 아이디어는 ** 가비지가 가장 많은 Region부터 수거한다 **는 것입니다(Garbage First). 전체 힙을 한꺼번에 정리할 필요 없이, 효율이 높은 Region만 골라서 수거하므로 STW를 목표 시간(-XX:MaxGCPauseMillis, 기본 200ms) 이내로 제어할 수 있어요.

동작 흐름:

  1. Young GC: Eden Region이 가득 차면 살아남은 객체를 Survivor Region으로 복사
  2. Concurrent Marking: Old Region에서 살아 있는 객체를 마킹 (애플리케이션과 동시 진행)
  3. Mixed GC: Young + 가비지가 많은 Old Region을 함께 수거

ZGC

Java 11에서 실험적으로 도입, Java 15에서 프로덕션 레디가 된 GC입니다. STW를 10ms 이내로 줄이는 것이 목표이고, 힙 크기가 수 TB까지 커져도 pause 시간이 거의 일정해요.

핵심 기술:

  • Colored Pointers: 포인터의 일부 비트에 GC 메타데이터(마킹, 리매핑 등)를 저장
  • Load Barriers: 객체를 읽을 때 추가 작업을 삽입해서, 대부분의 GC 작업을 애플리케이션 스레드와 동시에 수행
  • **Region 기반 + 컴팩션 **: G1처럼 Region을 쓰지만 동시 컴팩션까지 수행

ZGC는 거의 모든 작업이 concurrent합니다. STW는 루트 스캔 같은 극히 짧은 구간에서만 발생하고, 보통 1ms 이하예요.

Shenandoah GC

Red Hat이 개발한 GC로, ZGC와 비슷한 목표(낮은 pause)를 가집니다. ZGC가 Colored Pointers를 쓰는 반면 Shenandoah는 Brooks Pointer를 써서 객체 이동 중에도 기존 참조가 유효하도록 해요.


GC 튜닝

STW 줄이기

GC 튜닝의 핵심은 STW를 줄이는 것입니다. 접근법은 크게 세 가지예요.

1. 객체 할당률 줄이기

GC 빈도 자체를 줄이는 가장 근본적인 방법입니다. 불필요한 객체 생성을 피하고, 객체 풀링을 적용하고, String 대신 StringBuilder를 쓰는 등의 최적화가 있어요.

2. Young Generation 크기 조정

  • -Xmn 또는 -XX:NewRatio로 조절
  • Young이 너무 작으면 Minor GC가 너무 자주 발생
  • Young이 너무 크면 Minor GC 한 번의 STW가 길어짐
  • 애플리케이션의 객체 생존 패턴을 보고 조정해야 합니다

3. GC 알고리즘 선택

GC언제 쓰나
Parallel GC배치 처리, 처리량 우선
G1 GC범용, 힙 4GB 이상
ZGC초저지연 필요, 대용량 힙
ShenandoahZGC 대안, OpenJDK 환경

GC 로그 분석

GC 동작을 파악하려면 GC 로그를 켜야 합니다.

BASH
# Java 9+
java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar app.jar

# Java 8
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -jar app.jar

로그에서 봐야 할 것들:

  • GC 발생 빈도와 시간
  • Young GC vs Full GC 비율
  • 각 GC의 STW 시간
  • Old Generation으로의 승격 비율
  • 힙 사용량 추이

GCViewer, GCEasy 같은 도구를 쓰면 로그를 시각화해서 분석할 수 있어요. 그라파나 + Prometheus로 실시간 모니터링하는 것도 일반적입니다.


주의할 점

Java에서 GC가 있어도 메모리 릭이 발생하는 경우

GC가 있으니까 메모리 릭이 안 생길 것 같지만, 그렇지 않습니다. GC 루트에서 도달 가능한데 실제로는 쓰지 않는 객체 가 있으면 릭이에요.

대표적인 사례:

  • static 컬렉션에 계속 추가만 하고 제거하지 않는 경우
  • 이벤트 리스너/콜백을 등록만 하고 해제하지 않는 경우
  • ThreadLocal을 사용 후 remove() 하지 않으면 스레드 풀 환경에서 릭 발생
  • 내부 클래스(inner class)가 외부 클래스의 참조를 암묵적으로 들고 있는 경우
  • Connection, InputStream 등 리소스를 닫지 않는 경우 (try-with-resources 쓰자)

WeakReference / SoftReference

SoftReference: 메모리가 부족할 때만 GC가 수거합니다. 캐시에 적합해요. 메모리가 충분하면 계속 살아 있으니까요.

JAVA
SoftReference<byte[]> cache = new SoftReference<>(new byte[1024 * 1024]);
byte[] data = cache.get(); // null일 수 있음 → null이면 다시 로딩

WeakReference: 다음 GC 때 무조건 수거됩니다. WeakHashMap이 대표적인 사용 사례예요. 키가 더 이상 외부에서 강한 참조를 받지 않으면 엔트리 자체가 제거돼요.

JAVA
WeakReference<Object> weak = new WeakReference<>(new Object());
// 다음 GC 이후 weak.get()은 null

PhantomReference: get()이 항상 null을 반환합니다. 객체가 GC에 의해 수거되기 직전에 알림을 받기 위한 용도예요. ReferenceQueue와 함께 써서 네이티브 리소스 정리 같은 finalization 대안으로 씁니다.

참조 강도 순서: Strong > Soft > Weak > Phantom

스택 오버플로우 vs 힙 OOM

** 스택 오버플로우 (StackOverflowError)**

  • 스택 공간이 부족할 때 발생
  • 보통 재귀가 너무 깊거나 무한 재귀일 때
  • 스레드별로 발생하므로 다른 스레드에는 영향이 없어요
  • -Xss 옵션으로 스레드당 스택 크기를 조절할 수 있습니다 (기본 512KB~1MB)

** 힙 OOM (OutOfMemoryError: Java heap space)**

  • 힙에 새 객체를 할당할 공간이 없을 때 발생
  • GC를 해도 충분한 공간을 확보하지 못하면 터져요
  • -Xmx 옵션으로 최대 힙 크기를 조절합니다
  • 메모리 릭이 원인인 경우가 많아요 → 힙 덤프 떠서 분석해야 합니다
BASH
# OOM 발생 시 자동으로 힙 덤프
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -jar app.jar

Metaspace OOM (OutOfMemoryError: Metaspace)

  • 클래스 메타데이터가 너무 많을 때 발생
  • 리플렉션, 동적 프록시, CGLib를 과도하게 쓰는 경우
  • -XX:MaxMetaspaceSize로 제한을 걸어서 조기에 감지하는 게 좋습니다

파생 개념

이 글에서 다룬 내용과 연결되는 주제들을 정리해 두겠습니다.

  • ** 가상 메모리 **: 페이징, 페이지 교체 알고리즘, 스래싱 → 가상 메모리 글에서 다뤘습니다
  • ** 프로세스와 스레드 **: 메모리 구조, 컨텍스트 스위칭, 멀티스레드 → 프로세스와 스레드 글에서 다뤘습니다
  • **JVM 내부 구조 **: ClassLoader, JIT 컴파일러, 바이트코드 실행 → 별도 글에서 다룰 예정
  • **Java 동시성 **: synchronized, volatile, CAS, java.util.concurrent → 스레드 동기화와 연결되는 주제
댓글 로딩 중...