STUDY NOTE · 개념 정리
JVM 메모리 구조와 GC 정리: Heap, Stack, Young/Old, G1
Java
JVM 메모리는 스레드마다 따로 있는 영역(Stack, PC 레지스터, 네이티브 메서드 스택)과 모든 스레드가 같이 쓰는 영역(Heap, Method Area)으로 나뉜다. GC는 Heap에서 더 이상 닿을 수 없는 객체를 찾아 치우는데, '대부분의 객체는 금방 죽는다'는 가정으로 Young과 Old를 나눠 관리한다.
1. 런타임 데이터 영역
| 영역 | 공유 | 저장하는 것 | 부족하면 |
| PC 레지스터 | 스레드별 | 지금 실행 중인 명령 위치 | - |
| JVM 스택 | 스레드별 | 메서드 호출마다 프레임(지역 변수, 매개변수, 연산 스택) | StackOverflowError |
| 네이티브 메서드 스택 | 스레드별 | JNI 등 네이티브 코드 호출 | StackOverflowError |
| Heap | 전체 공유 | new로 만든 객체, 배열 (GC 대상) | OutOfMemoryError: Java heap space |
| Method Area (Metaspace) | 전체 공유 | 클래스 정보, 메서드 코드, static 변수, 런타임 상수 풀 | OutOfMemoryError: Metaspace |
void run() {
int count = 3; // 스택 프레임의 지역 변수 (값 자체)
User u = new User("kim"); // u(참조)는 스택, User 객체는 힙
} // 메서드가 끝나면 프레임째 사라짐 → 객체는 참조가 없어져 GC 대상
- JDK 8부터 클래스 메타데이터는 힙의 PermGen이 아니라 네이티브 메모리의 Metaspace에 둔다. 크기를 자동으로 늘릴 수 있어 'PermGen space' 에러가 사라졌다.
- 문자열 풀은 JDK 7부터 힙에 있다.
2. GC의 기본: 누가 쓰레기인가
Reachability: GC Root에서 참조를 따라가서 닿지 않는 객체가 쓰레기다. 참조 횟수를 세는 방식이 아니라서, 서로만 가리키는 순환 참조도 치울 수 있다.
GC Root가 되는 것: 실행 중인 스레드의 스택 지역 변수, static 변수, JNI 참조 등.
| 단계 | 하는 일 |
| Mark | Root부터 따라가며 살아 있는 객체 표시 |
| Sweep | 표시 안 된 객체 메모리 해제 |
| Compact | 살아남은 객체를 한쪽으로 모아 조각난 빈 공간 정리 (선택) |
Stop-The-World(STW): GC 작업 중 일부 또는 전부 동안 애플리케이션 스레드가 멈추는 것. GC 튜닝은 결국 이 멈춤을 얼마나 짧게, 얼마나 드물게 하느냐다.
3. 세대별 구조 (Generational)
'약한 세대 가설': 대부분의 객체는 만들어지고 금방 쓸모없어진다. 그래서 새 객체만 자주 빠르게 훑고, 오래 산 객체는 가끔 훑는다.
| 영역 | 설명 |
| Eden | 새 객체가 처음 생기는 곳 |
| Survivor 0 / 1 | Minor GC에서 살아남은 객체가 둘 중 한쪽으로 복사됨. 항상 하나는 비어 있음 |
| Old | Survivor를 여러 번(age 임계값, 기본 최대 15) 살아남거나 너무 큰 객체가 옮겨 오는 곳 |
| GC 종류 | 대상 | 특징 |
| Minor GC | Young (Eden + Survivor) | 자주, 빠르게. 살아 있는 것만 복사하니 죽은 객체가 많을수록 쌈 |
| Major / Full GC | Old (Full은 전체 힙) | 드물지만 오래 걸림. 긴 STW의 주범 |
Old의 객체가 Young의 객체를 가리키면 Minor GC 때 Old 전체를 훑어야 할까? 아니다. 카드 테이블에 'Old에서 Young을 가리키는 곳'을 기록해 두고 그 부분만 본다.
4. GC 알고리즘 비교
| GC | 방식 | 어울리는 곳 |
| Serial | 한 스레드로 전부 수행 | 작은 힙, 클라이언트 |
| Parallel | 여러 스레드로 수행, 처리량 우선 | 배치 작업 (JDK 8 기본) |
| CMS | Old를 앱과 동시에 표시·정리, 압축 안 함 | 응답 시간 중시였으나 JDK 14에서 제거 |
| G1 | 힙을 같은 크기 Region으로 나눠 쓰레기가 많은 Region부터 수거, 목표 멈춤 시간 설정 | 범용 서버 (JDK 9부터 기본) |
| ZGC / Shenandoah | 대부분 작업을 앱과 동시에, 멈춤 수 ms 이하 | 큰 힙 + 짧은 지연이 중요한 서비스 |
G1의 핵심: Young/Old가 고정된 연속 공간이 아니라 Region에 붙는 '역할'이다. -XX:MaxGCPauseMillis(기본 200ms)를 넘지 않을 만큼만 쓰레기가 많은 Region(Garbage First)부터 골라 치운다.
5. 실무에서 보는 것
- 메모리 누수의 흔한 원인: static 컬렉션에 계속 넣기, 지우지 않는 캐시, 등록만 하고 해제 안 하는 리스너, ThreadLocal을 스레드 풀에서 쓰고
remove()안 하기. - 확인 도구: GC 로그(
-Xlog:gc*),jstat -gcutil, 힙 덤프(jmap, OOM 때-XX:+HeapDumpOnOutOfMemoryError) → MAT·VisualVM으로 분석. - 자주 쓰는 옵션:
-Xms/-Xmx(힙 시작·최대, 같게 두면 늘리는 비용이 없음),-Xss(스레드 스택 크기),-XX:+UseG1GC. - 컨테이너에서는 JVM이 컨테이너 메모리 한도를 보고 힙을 정하므로
-XX:MaxRAMPercentage로 비율을 지정하는 편이 편하다.
6. 정리
- 스레드별: Stack(프레임), PC / 공유: Heap(객체), Metaspace(클래스 정보).
- GC 대상은 Root에서 닿지 않는 객체. 순환 참조도 수거된다.
- Young은 자주 짧게(Minor), Old는 드물게 길게(Major/Full).
- 기본 GC는 G1, 아주 짧은 멈춤이 필요하면 ZGC.