← 개인 공부 · Java
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 대상
  1. JDK 8부터 클래스 메타데이터는 힙의 PermGen이 아니라 네이티브 메모리의 Metaspace에 둔다. 크기를 자동으로 늘릴 수 있어 'PermGen space' 에러가 사라졌다.
  2. 문자열 풀은 JDK 7부터 힙에 있다.

2. GC의 기본: 누가 쓰레기인가

Reachability: GC Root에서 참조를 따라가서 닿지 않는 객체가 쓰레기다. 참조 횟수를 세는 방식이 아니라서, 서로만 가리키는 순환 참조도 치울 수 있다.

GC Root가 되는 것: 실행 중인 스레드의 스택 지역 변수, static 변수, JNI 참조 등.

단계하는 일
MarkRoot부터 따라가며 살아 있는 객체 표시
Sweep표시 안 된 객체 메모리 해제
Compact살아남은 객체를 한쪽으로 모아 조각난 빈 공간 정리 (선택)

Stop-The-World(STW): GC 작업 중 일부 또는 전부 동안 애플리케이션 스레드가 멈추는 것. GC 튜닝은 결국 이 멈춤을 얼마나 짧게, 얼마나 드물게 하느냐다.

3. 세대별 구조 (Generational)

'약한 세대 가설': 대부분의 객체는 만들어지고 금방 쓸모없어진다. 그래서 새 객체만 자주 빠르게 훑고, 오래 산 객체는 가끔 훑는다.

영역설명
Eden새 객체가 처음 생기는 곳
Survivor 0 / 1Minor GC에서 살아남은 객체가 둘 중 한쪽으로 복사됨. 항상 하나는 비어 있음
OldSurvivor를 여러 번(age 임계값, 기본 최대 15) 살아남거나 너무 큰 객체가 옮겨 오는 곳
GC 종류대상특징
Minor GCYoung (Eden + Survivor)자주, 빠르게. 살아 있는 것만 복사하니 죽은 객체가 많을수록 쌈
Major / Full GCOld (Full은 전체 힙)드물지만 오래 걸림. 긴 STW의 주범

Old의 객체가 Young의 객체를 가리키면 Minor GC 때 Old 전체를 훑어야 할까? 아니다. 카드 테이블에 'Old에서 Young을 가리키는 곳'을 기록해 두고 그 부분만 본다.

4. GC 알고리즘 비교

GC방식어울리는 곳
Serial한 스레드로 전부 수행작은 힙, 클라이언트
Parallel여러 스레드로 수행, 처리량 우선배치 작업 (JDK 8 기본)
CMSOld를 앱과 동시에 표시·정리, 압축 안 함응답 시간 중시였으나 JDK 14에서 제거
G1힙을 같은 크기 Region으로 나눠 쓰레기가 많은 Region부터 수거, 목표 멈춤 시간 설정범용 서버 (JDK 9부터 기본)
ZGC / Shenandoah대부분 작업을 앱과 동시에, 멈춤 수 ms 이하큰 힙 + 짧은 지연이 중요한 서비스

G1의 핵심: Young/Old가 고정된 연속 공간이 아니라 Region에 붙는 '역할'이다. -XX:MaxGCPauseMillis(기본 200ms)를 넘지 않을 만큼만 쓰레기가 많은 Region(Garbage First)부터 골라 치운다.

5. 실무에서 보는 것

  1. 메모리 누수의 흔한 원인: static 컬렉션에 계속 넣기, 지우지 않는 캐시, 등록만 하고 해제 안 하는 리스너, ThreadLocal을 스레드 풀에서 쓰고 remove() 안 하기.
  2. 확인 도구: GC 로그(-Xlog:gc*), jstat -gcutil, 힙 덤프(jmap, OOM 때 -XX:+HeapDumpOnOutOfMemoryError) → MAT·VisualVM으로 분석.
  3. 자주 쓰는 옵션: -Xms/-Xmx(힙 시작·최대, 같게 두면 늘리는 비용이 없음), -Xss(스레드 스택 크기), -XX:+UseG1GC.
  4. 컨테이너에서는 JVM이 컨테이너 메모리 한도를 보고 힙을 정하므로 -XX:MaxRAMPercentage로 비율을 지정하는 편이 편하다.

6. 정리

  1. 스레드별: Stack(프레임), PC / 공유: Heap(객체), Metaspace(클래스 정보).
  2. GC 대상은 Root에서 닿지 않는 객체. 순환 참조도 수거된다.
  3. Young은 자주 짧게(Minor), Old는 드물게 길게(Major/Full).
  4. 기본 GC는 G1, 아주 짧은 멈춤이 필요하면 ZGC.
Gunmo Lee