JVM이 관리하는 메모리는 공유하는 곳과 스레드마다 갖는 곳으로 나뉜다.
[모든 스레드가 공유]
힙(Heap) 객체와 배열. GC 의 대상
메타스페이스 클래스 메타데이터 (Java 8+ · 네이티브 메모리, 기본 무제한)
[스레드마다 하나씩]
스택 메서드 호출마다 프레임을 쌓는다 (지역 변수 · 부분 결과)
PC 레지스터 지금 실행 중인 바이트코드 주소
네이티브 스택 JNI 호출용
객체 하나가 놓이는 모습
void f() {
Member m = new Member("철수", 20);
}
[스택 — f() 의 프레임] [힙]
m │ 0x7A3 │ ─────────────→ ┌────────────────────────┐
│ 헤더 (mark word · klass)│
│ name → 0x9C1 │
│ age = 20 │
└─────────────────────────┘
↓
┌────────────────────────┐
│ String "철수" │
└────────────────────────┘
참조 변수는 스택, 객체 실체는 힙이라는 게 기본 그림이다.
OOM 메시지가 어디를 가리키나
java.lang.OutOfMemoryError: Java heap space
→ 힙 부족. 누수이거나 -Xmx 가 작다
java.lang.OutOfMemoryError: Metaspace
→ 클래스가 계속 로드된다 (동적 프록시 · 리플렉션 · 잦은 재배포)
java.lang.OutOfMemoryError: unable to create new native thread
→ 힙과 무관. OS 스레드 한계다 (스레드 풀 상한이 없는 경우)
java.lang.StackOverflowError
→ 스택 부족. 재귀가 너무 깊다 (-Xss 로 조정 가능하나 대개 코드 문제)
java.lang.OutOfMemoryError: Direct buffer memory
→ 힙 밖(오프힙). DirectByteBuffer 누수
메시지 뒷부분이 어느 영역인지 말해 준다 — 무작정 -Xmx를 올리기 전에 이걸 먼저 읽는다.
Java 8에서 바뀐 것
- ~7 — PermGen — 힙 안. 크기 고정 → "PermGen space" OOM 이 흔했다
- 8~ — Metaspace — 네이티브 메모리. 기본 무제한이라 잘 안 터진다
- 대신 방치하면 OS 메모리를 다 먹는다 → -XX:MaxMetaspaceSize 로 상한을 둔다
확인하는 명령
jcmd <pid> VM.native_memory summary # 영역별 사용량 (NMT 켜야 함)
jmap -histo <pid> | head -20 # 어떤 클래스가 메모리를 먹나
jcmd <pid> GC.heap_info
jstack <pid> # 스레드 수 · 데드락
문자열 상수 풀
String a = "hi", b = "hi";
a == b; // true — 상수 풀에서 공유된다
new String("hi") == a; // false — 힙에 새 객체
"hi".intern(); // 풀의 인스턴스를 얻는다
풀은 Java 7부터 힙에 있다(그전엔 PermGen). 그래서 GC 대상이 된다.
면접 함정
- ❌ "원시 타입은 항상 스택" → 객체의 필드면 힙에 있다.
- ❌ "OOM은 전부 힙 문제" → 메타스페이스·네이티브 스레드·다이렉트 버퍼는 힙과 무관하다.