Java 가비지 컬렉터(GC) 완벽 가이드
2025. 10. 6. 18:41

들어가며

Java의 가장 큰 장점 중 하나는 개발자가 메모리 관리를 직접 하지 않아도 된다는 점입니다. 이것이 가능한 이유는 바로 가비지 컬렉터(Garbage Collector, GC) 덕분입니다. 이 글에서는 JVM의 메모리 구조부터 GC의 동작 원리까지 상세히 알아보겠습니다.

 

1. JVM 전체 구조

JVM(Java Virtual Machine)은 Java 애플리케이션을 실행하기 위한 가상 머신으로, 다음과 같은 주요 컴포넌트로 구성되어 있습니다.

 

주요 구성 요소

Class Loader Subsystem

  • Java 클래스 파일(.class)을 메모리에 로드
  • Loading → Linking → Initialization 단계를 거침

Runtime Data Areas

  • Method Area: 클래스 메타데이터, static 변수 저장
  • Heap: 객체 인스턴스가 생성되는 영역 (GC의 주요 대상)
  • Stack: 각 스레드별로 생성되며, 지역 변수와 메서드 호출 정보 저장
  • PC Register: 현재 실행 중인 명령어 주소
  • Native Method Stack: 네이티브 메서드 호출을 위한 스택

Execution Engine

  • Interpreter: 바이트코드를 한 줄씩 해석하여 실행
  • JIT Compiler: 자주 사용되는 코드를 기계어로 컴파일
  • Garbage Collector: 사용하지 않는 객체를 자동으로 메모리에서 제거

 

2. Heap 메모리 구조

Heap은 모든 객체 인스턴스가 할당되는 영역으로, GC의 주요 작업 공간입니다. Heap은 크게 Young GenerationOld Generation으로 나뉩니다.

Young Generation

새로 생성된 객체들이 위치하는 영역으로, 다시 세 부분으로 나뉩니다:

Eden Space

  • 새로운 객체가 최초로 생성되는 공간
  • 대부분의 객체는 Eden에서 생성된 후 금방 사라짐 (Short-lived objects)

Survivor Space 0 (S0)

  • Minor GC에서 살아남은 객체가 이동하는 공간
  • From 영역이라고도 불림

Survivor Space 1 (S1)

  • Minor GC에서 살아남은 객체가 이동하는 공간
  • To 영역이라고도 불림

중요한 규칙: S0과 S1 중 하나는 항상 비어있어야 합니다. 두 영역은 Minor GC마다 From과 To 역할을 교대로 수행합니다.

Old Generation (Tenured)

  • Young Generation에서 오래 살아남은 객체가 승격(Promotion)되는 영역
  • 일반적으로 age threshold가 15 이상일 때 승격
  • Young Generation보다 크기가 크며, GC 빈도가 낮음
  • Major GC가 발생하는 영역

 

3. GC 알고리즘: Mark-Sweep-Compact

가비지 컬렉터는 기본적으로 세 단계의 프로세스를 거쳐 메모리를 정리합니다.

1단계: Mark (마킹)

GC Root부터 시작하여 참조되는 객체를 마킹합니다.

GC Root는 다음을 포함합니다:

  • Stack의 지역 변수
  • Static 변수
  • JNI(Java Native Interface) 참조
  • 실행 중인 스레드

GC는 이러한 Root로부터 시작하여 참조 그래프를 따라가며 도달 가능한(reachable) 모든 객체를 마킹합니다.

2단계: Sweep (제거)

마킹되지 않은 객체(가비지)를 메모리에서 제거합니다.

Mark 단계에서 마킹되지 않은 객체는 더 이상 참조되지 않는 객체이므로, 안전하게 메모리에서 해제할 수 있습니다.

3단계: Compact (압축)

남은 객체들을 메모리의 한쪽으로 이동시켜 단편화를 방지합니다.

메모리 단편화를 줄이고 연속된 빈 공간을 확보하여, 새로운 객체 할당을 효율적으로 만듭니다.

참고: Compact 단계는 모든 GC 알고리즘에서 수행되는 것은 아니며, 알고리즘에 따라 선택적으로 적용됩니다.

 

4. Minor GC 동작 과정

 

Minor GC는 Young Generation에서 발생하는 GC로, 매우 빈번하게 발생하지만 빠르게 완료됩니다.

발생 조건

Eden 영역이 가득 찼을 때 자동으로 발생합니다.

동작 과정

1. GC 발생 전

  • Eden: 객체들로 가득 참
  • S0 (From): 이전 Minor GC에서 살아남은 객체들 (age: 1, 2, ...)
  • S1 (To): 비어있음

2. GC 진행

  • Eden과 S0의 모든 객체를 검사
  • Live 객체는 S1으로 이동하며 age가 1 증가
  • Garbage 객체는 메모리에서 제거

3. GC 완료 후

  • Eden: 완전히 비워짐 (새 객체 생성 준비)
  • S0 (From): 비워짐
  • S1 (To): 살아남은 모든 객체 보관 (age 증가됨)

4. 다음 Minor GC

  • S0과 S1의 역할이 교대됨
  • 이전 To(S1) → 다음 From
  • 이전 From(S0) → 다음 To

Minor GC의 특징

빠른 속도

  • Young Generation 영역만 검사하므로 속도가 빠름
  • STW(Stop-The-World) 시간이 짧음 (보통 수 밀리초)

높은 빈도

  • Eden이 금방 가득 차므로 자주 발생
  • 애플리케이션 실행 중 지속적으로 발생

Weak Generational Hypothesis

  • 대부분의 객체는 생성 후 금방 가비지가 됨
  • Eden에서 생성된 객체의 약 90%가 첫 Minor GC에서 제거
  • 이런 특성 때문에 Young GC가 효율적

 

5. 프로모션(Promotion) 과정

 

프로모션은 Young Generation에서 오래 살아남은 객체를 Old Generation으로 이동시키는 과정입니다.

객체의 생애 주기

1단계: Eden에서 탄생

  • 모든 객체는 Eden에서 생성 (age = 0)

2단계: 첫 생존

  • Minor GC에서 살아남으면 Survivor 0로 이동 (age = 1)

3단계: 반복 생존

  • Minor GC마다 Survivor 영역을 오가며 age 증가 (age = 2, 3, 4, ...)

4단계: Old Generation 승격

  • age가 threshold에 도달하면 Old Generation으로 이동

프로모션 발생 조건

조건 1: Age Threshold 도달

  • 객체의 age가 설정된 threshold에 도달
  • 기본값: age = 15
  • JVM 옵션으로 변경 가능: -XX:MaxTenuringThreshold=N (N은 1~15)

조건 2: Survivor 공간 부족

  • Survivor 영역이 가득 찬 경우
  • age와 관계없이 조기 승격 발생
  • 이를 방지하려면 Survivor 크기 조정: -XX:SurvivorRatio=N

조건 3: 대용량 객체

  • 큰 객체는 Eden을 건너뛰고 Old Generation에 직접 할당
  • 임계값 설정: -XX:PretenureSizeThreshold=N (바이트 단위)
  • Young Generation을 거치지 않고 바로 Old로 할당

조건 4: Dynamic Age Calculation (동적 age 계산)

  • Survivor 영역의 50% 이상을 차지하는 동일 age의 객체들이 있을 경우
  • 해당 age 이상의 모든 객체를 threshold 이전에도 승격
  • 메모리 압박 상황을 고려한 적응형 계산

프로모션의 특징

  • Young → Old로의 단방향 이동
  • Old → Young 역방향 이동은 없음
  • 프로모션은 Minor GC 중에 발생
  • 잘못된 프로모션 설정은 Old Generation을 빠르게 채워 Major GC를 유발

 

6. Major GC (Full GC) 동작 과정

 

Major GC는 Old Generation에서 발생하는 GC로, Minor GC보다 훨씬 무겁고 시간이 오래 걸립니다.

발생 조건

Old Generation 메모리 부족

  • Old Generation이 거의 가득 찼을 때 발생
  • Minor GC로 해결되지 않는 메모리 압박 상황

System.gc() 호출

  • 명시적으로 GC를 호출한 경우 (권장하지 않음)
  • JVM이 적절한 타이밍에 GC를 수행하도록 맡기는 것이 좋음

Metaspace 부족

  • 클래스 메타데이터 영역이 부족한 경우

동작 과정

1단계: GC 발생 전

  • Old Generation이 Live 객체와 Garbage 객체로 혼재
  • 메모리 단편화 발생 가능
  • 메모리 부족 임박 상태

2단계: Mark Phase (마킹)

  • GC Root부터 시작하여 참조 그래프 탐색
  • Stack, Static 변수, JNI 참조 등에서 시작
  • 도달 가능한 모든 객체를 마킹
  • Old Generation 전체를 스캔하므로 시간 소요

3단계: Sweep Phase (제거)

  • 마킹되지 않은 객체(가비지)를 메모리에서 제거
  • 메모리 공간 해제
  • 이 시점에서 메모리 단편화 발생 가능

4단계: Compact Phase (압축)

  • 살아남은 객체들을 메모리의 한쪽으로 이동
  • 연속된 빈 공간 확보
  • 메모리 단편화 방지
  • 새로운 객체 할당을 위한 공간 확보

Major GC의 특징

Stop-The-World (STW)

  • 모든 애플리케이션 스레드가 일시 정지
  • Minor GC보다 훨씬 긴 중단 시간 (수십 밀리초 ~ 수 초)
  • 사용자 경험에 직접적인 영향
  • 성능 저하의 주요 원인

낮은 빈도

  • Minor GC보다 덜 빈번하게 발생
  • Old Generation 크기가 크므로 채워지는 데 시간이 오래 걸림
  • 하지만 한 번 발생하면 오래 걸림

전체 Heap 영향

  • Full GC의 경우 Young + Old + Metaspace 전체 정리
  • Major GC는 주로 Old Generation만 정리

Major GC vs Full GC

Major GC

  • Old Generation만 정리
  • Young Generation은 영향 없음

Full GC

  • Heap 전체 (Young + Old) 정리
  • Metaspace (또는 Permanent Generation)도 함께 정리
  • 일반적으로 Full GC가 더 무거움

 

7. GC 최적화 팁

힙 크기 설정

# 초기 힙 크기
-Xms2g

# 최대 힙 크기
-Xmx4g

# 초기값과 최대값을 같게 설정하면 동적 조정 오버헤드 제거
-Xms4g -Xmx4g

Young Generation 크기 조정

# Young Generation 크기 설정
-Xmn1g

# 또는 비율로 설정 (전체 힙의 1/3)
-XX:NewRatio=2

Survivor 영역 크기 조정

# Eden : Survivor 비율 설정 (8:1:1)
-XX:SurvivorRatio=8

GC 알고리즘 선택

# G1 GC (Java 9+ 기본값)
-XX:+UseG1GC

# Parallel GC (처리량 중시)
-XX:+UseParallelGC

# CMS GC (응답 시간 중시, deprecated)
-XX:+UseConcMarkSweepGC

# ZGC (초저지연, Java 11+)
-XX:+UseZGC

# Shenandoah GC (초저지연)
-XX:+UseShenandoahGC

GC 로그 활성화

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

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

애플리케이션 레벨 최적화

불필요한 객체 생성 최소화

  • 객체 풀 사용 (필요한 경우에만)
  • String concatenation 시 StringBuilder 사용
  • 불변 객체 재사용

적절한 자료구조 사용

  • 예상 크기를 알 경우 초기 용량 설정
  • ArrayList<>(expectedSize), HashMap<>(expectedSize)

메모리 누수 방지

  • 사용하지 않는 참조 제거
  • 리스너, 콜백 등록 후 해제
  • 캐시 크기 제한

대용량 객체 주의

  • 큰 객체는 Old Generation으로 직접 할당될 수 있음
  • 필요한 경우에만 생성하고, 빠르게 해제

 

8. GC 모니터링 및 분석

JVM 모니터링 도구

JConsole

  • JDK 기본 제공 GUI 모니터링 도구
  • 실시간 힙 메모리, 스레드, 클래스 현황 확인

VisualVM

  • 강력한 프로파일링 도구
  • 힙 덤프 분석, CPU/메모리 프로파일링

Java Mission Control (JMC)

  • Oracle에서 제공하는 고급 모니터링 도구
  • Flight Recorder와 함께 사용

GC 로그 분석 도구

  • GCeasy (https://gceasy.io) - 온라인 GC 로그 분석
  • GCViewer - 오픈소스 GC 로그 시각화 도구

주요 모니터링 지표

GC 빈도

  • Minor GC 발생 주기
  • Major GC 발생 주기

GC 소요 시간

  • 각 GC의 pause time
  • 전체 실행 시간 대비 GC 시간 비율

힙 사용률

  • Young/Old Generation 각각의 사용률
  • GC 후 힙 메모리 회수율

프로모션 비율

  • Young에서 Old로 승격되는 객체 비율
  • 높은 프로모션 비율은 문제의 신호

 

마치며

가비지 컬렉터는 Java의 핵심 기능이자, 애플리케이션 성능에 직접적인 영향을 미치는 중요한 요소입니다.

핵심 요약:

  1. JVM Heap은 Young과 Old Generation으로 구성
  2. Minor GC는 Young Generation에서 자주, 빠르게 발생
  3. 객체는 age가 증가하며 Old Generation으로 승격
  4. Major GC는 Old Generation에서 드물게, 느리게 발생
  5. GC 튜닝은 애플리케이션 특성에 맞춰 진행

적절한 GC 설정과 모니터링을 통해 안정적이고 고성능의 Java 애플리케이션을 구축할 수 있습니다. GC 로그를 주기적으로 확인하고, 병목 지점을 파악하여 최적화하는 것이 중요합니다.

 

 

참고 자료:

  • Oracle Java Documentation
  • Java Performance: The Definitive Guide
  • Java Garbage Collection Handbook