JVM이 프로그램 실행 중에 사용하는 메모리 구획들. 어떤 것이 스레드마다 따로이고 어떤 것이 공유되는지가 핵심이다.
전체 구조
┌──────────── 모든 스레드가 공유 ────────────────┐
│ 힙 (Heap) │
│ Young(Eden, Survivor 0/1) Old │
│ 메서드 영역 (Metaspace) │
│ 클래스 메타데이터 · static · 런타임 상수 풀 │
└────────────────────────────────────────────────┘
┌──────────── 스레드마다 하나씩 ─────────────────┐
│ JVM 스택 — 스택 프레임(지역변수·피연산자) │
│ PC 레지스터 — 현재 실행 중인 바이트코드 주소 │
│ 네이티브 메서드 스택 — JNI 호출용 │
└────────────────────────────────────────────────┘
힙 — 객체가 사는 곳
new로 만든 모든 객체와 배열이 여기 있다. GC의 대상이다.
Young Generation Old Generation
┌─────┬────────┬────────┐ ┌────────────────┐
│Eden │Survivor│Survivor│ ───► │ 오래 산 객체 │
│ │ 0 │ 1 │ │ │
└─────┴────────┴────────┘ └────────────────┘
↑ 새 객체는 여기서 태어난다
왜 나누나 — 약한 세대 가설
- "대부분의 객체는 금방 죽는다"
- 갓 만든 객체만 모아 자주 청소하면 적은 비용으로 많이 회수된다 실제로 대부분의 객체(요청 처리용 임시 객체 등)는 몇 밀리초 만에 쓰레기가 된다.
Survivor를 거칠 때마다 age가 오르고, 임계치를 넘으면 Old로 승격된다.
스택 — 실행 흐름이 사는 곳
메서드를 호출할 때마다 스택 프레임이 쌓인다.
프레임에 담기는 것
- 지역 변수 배열 (매개변수 포함)
- 피연산자 스택 (계산 중간값)
- 프레임 데이터 (상수 풀 참조, 반환 주소)
스레드마다 따로이므로 지역 변수는 스레드 안전하다.
-Xss 로 크기를 조절하며, 초과하면 StackOverflowError다.
힙과 스택 — 참조와 실체
void method() {
Person p = new Person("김철수");
}
스택 (이 스레드만) 힙 (모두 공유)
┌──────────┐ ┌─────────────────┐
│ p: 0x1A3F│ ───────────► │ Person 객체 │
└──────────┘ │ name → "김철수" │
참조 변수는 스택 └─────────────────┘
실제 객체는 힙
변수 p는 스택에, 객체는 힙에 있다. 그래서 다른 스레드에 p를
넘기면 같은 객체를 공유하게 된다 — 여기서 동시성 문제가 시작된다.
메서드 영역 (Metaspace)
클래스 정보, static 변수, 상수 풀이 있다.
자바 8에서 큰 변화가 있었다. ~ 자바 7 : PermGen (힙 안. 고정 크기)
- 클래스를 많이 로드하면 OutOfMemoryError: PermGen space
- 크기 예측이 어려워 악명 높았다
자바 8 ~ : Metaspace (네이티브 메모리. 기본 무제한)
- OOM은 줄었지만, 방치하면 시스템 메모리를 다 먹는다
- -XX:MaxMetaspaceSize 로 상한을 두는 것이 권장
Metaspace가 계속 늘어난다면 클래스로더 누수를 의심한다 — 동적 프록시·리플렉션·잦은 재배포에서 발생한다.
OOM 메시지로 원인 찾기 — 실무 진단
| 메시지 | 원인 |
|---|---|
Java heap space | 힙 부족. 누수 또는 -Xmx 부족 |
Metaspace | 클래스 과다 로드·클래스로더 누수 |
unable to create new native thread | 스레드 과다(OS 한계) |
GC overhead limit exceeded | GC에 98% 시간을 쓰는데 2% 미만 회수 |
Requested array size exceeds VM limit | 배열 크기가 비정상 |
메시지가 어느 영역이 터졌는지 알려 준다. 이 표를 아는 것이 장애 대응의 출발점이다.