JavaHard#52
Java의 가비지 컬렉션(GC) 동작 원리와 주요 GC 알고리즘을 설명해주세요.
#Java#GC#JVM#성능
힌트
Minor GC / Major GC, Stop-the-World, G1 GC를 생각해보세요.
정답 및 해설
Java의 가비지 컬렉션(GC) 동작 원리와 주요 GC 알고리즘을 설명해주세요.
가비지 컬렉션(GC)은 JVM이 더 이상 사용되지 않는 객체를 자동으로 메모리에서 해제하는 메커니즘입니다. 개발자가 직접 메모리를 관리하지 않아도 되어 메모리 누수 위험이 줄어들지만, GC가 실행되는 동안 애플리케이션이 일시적으로 멈추는 "Stop-the-World" 현상이 발생할 수 있습니다. GC의 동작 원리를 이해하면 성능 튜닝과 메모리 이슈 해결에 크게 도움이 됩니다.
GC의 기본 원리
객체 생존 여부 판단
// GC는 "도달 가능성(Reachability)"으로 객체 생존 여부 판단
// GC Root에서 시작하여 참조를 따라갈 수 있으면 생존(Reachable)
// 참조를 따라갈 수 없으면 수거 대상(Unreachable)
// GC Root가 되는 것들:
// 1. JVM Stack의 지역 변수
// 2. Method Area의 static 변수
// 3. JNI에 의해 생성된 참조
void example() {
// GC Root: 지역 변수
User user1 = new User("김철수") // user1 → User 객체 (Reachable)
User user2 = new User("이영희") // user2 → User 객체 (Reachable)
user2 = user1 // 이영희 User 객체 → 참조 없음 → Unreachable → GC 대상
user1 = null // 김철수 User 객체 → 참조 없음 → GC 대상
// 두 객체 모두 GC 대상이 됨
}
// 순환 참조도 GC가 수거 가능 (Reference Counting 방식은 불가능하지만
// Java의 Mark-and-Sweep 방식은 순환 참조도 처리 가능)
class Node {
Node next
}
Node a = new Node()
Node b = new Node()
a.next = b // a → b
b.next = a // b → a (순환 참조)
a = null
b = null // GC Root에서 도달 불가 → 순환 참조임에도 GC 대상
GC 동작 흐름 (Generational GC)
세대(Generation) 가설
대부분의 객체는 금방 죽는다(Weak Generational Hypothesis)
→ 오래 살아남을수록 더 오래 생존할 가능성이 높다
→ 세대 구분으로 GC 효율 향상
단계별 동작
1. 객체 생성 → Eden Space에 배치
┌──────────────────────────────┐
│ Eden: [A][B][C][D][E][F][G] │ (거의 가득 참)
│ S0: [] │
│ S1: [] │
└──────────────────────────────┘
2. Eden이 가득 차면 → Minor GC(Young GC) 실행
- Mark: 살아있는 객체 표시 (A, C, E, G 살아있음)
- Copy: 살아있는 객체를 S0으로 복사, age = 1
┌──────────────────────────────┐
│ Eden: [] │ (비워짐)
│ S0: [A(1)][C(1)][E(1)][G(1)] │
│ S1: [] │
└──────────────────────────────┘
3. 다시 Eden 가득 참 → Minor GC 재실행
- Eden과 S0의 살아있는 객체를 S1으로 복사, age 증가
┌───────────────────────────────────┐
│ Eden: [] │
│ S0: [] │
│ S1: [A(2)][E(2)][H(1)][I(1)] │ (age 임계값 미만)
└───────────────────────────────────┘
4. age가 임계값(기본 15) 도달 → Old Generation으로 승격(Promotion)
- A가 age=15 도달 → Old Gen으로 이동
5. Old Gen이 가득 차면 → Major GC(Full GC) 실행
- 더 오래, Stop-the-World 시간 더 길어짐
Minor GC vs Major GC
Minor GC (Young GC):
- Young Generation만 수거
- 자주 실행 (수 초~수십 초 간격)
- Stop-the-World: 짧음 (수 ms ~ 수십 ms)
- 대부분의 객체를 수거하므로 효율적
Major GC (Full GC):
- Old Generation + 전체 Heap 수거
- 드물게 실행
- Stop-the-World: 길 수 있음 (수십 ms ~ 수 초)
- 성능에 큰 영향 → 최소화가 GC 튜닝의 목표
GC 알고리즘
Serial GC
# 사용 설정
-XX:+UseSerialGC
특징:
- 단일 스레드로 GC 수행
- GC 중 나머지 모든 스레드 정지 (Stop-the-World)
- Young: Mark-Copy, Old: Mark-Sweep-Compact
# 사용 적합한 경우:
# - 단일 코어 CPU 환경
# - 메모리가 적은 임베디드 환경
# - 클라이언트 사이드 애플리케이션
Serial GC 동작:
Single Thread GC
┌──────────────────────┐
│ GC Thread: ████████ │ (GC 수행 중)
│ App Thread: ─ ─ ─ ─ │ (모두 정지)
└──────────────────────┘
Parallel GC
# 사용 설정 (Java 8 기본값)
-XX:+UseParallelGC
-XX:ParallelGCThreads=4 # GC 스레드 수
특징:
- 여러 스레드가 병렬로 GC 수행
- Young Generation에서 특히 효과적
- Throughput 우선 (처리량 극대화)
- Stop-the-World는 여전히 발생하지만 시간은 단축
# 사용 적합한 경우:
# - 배치 처리 애플리케이션
# - 응답 시간보다 처리량이 중요한 경우
Parallel GC 동작:
Multi-Thread GC
┌───────────────────────────┐
│ GC Thread1: ████ │
│ GC Thread2: ████ │ (병렬 GC)
│ GC Thread3: ████ │
│ App Thread: ─ ─ ─ │ (정지)
└───────────────────────────┘
CMS GC (Concurrent Mark Sweep)
# 사용 설정 (Java 9부터 deprecated, Java 14에서 제거됨)
-XX:+UseConcMarkSweepGC
특징:
- Old Generation GC의 대부분을 애플리케이션과 동시(Concurrent) 수행
- Stop-the-World 시간 최소화
- 단점: 메모리 단편화 발생 (Compact 없음), CPU 사용량 높음
동작 단계:
1. Initial Mark (STW): GC Root에서 직접 참조된 객체만 표시 (매우 짧음)
2. Concurrent Mark: 나머지 객체를 앱 실행 중에 표시 (STW 없음)
3. Remark (STW): 2단계 중 변경된 객체 재표시 (짧음)
4. Concurrent Sweep: 앱 실행 중에 수거 (STW 없음)
CMS GC 동작:
┌──────────────────────────────────────────────┐
│ GC Thread: ██ ──────── ██ ────────── │
│ ↑ ↑ │
│ Initial Remark │
│ Mark (STW) │
│ (STW) │
│ App Thread: ─ ████████ ─ ████████ │
└──────────────────────────────────────────────┘
G1 GC (Garbage-First GC)
# 사용 설정 (Java 9+ 기본값)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 최대 STW 시간 목표 (기본 200ms)
-XX:G1HeapRegionSize=16m # 리전 크기 (1~32MB, 2의 거듭제곱)
특징:
- Heap을 동일 크기의 Region으로 분할
- Young/Old 구분이 유동적 (Region이 역할 변경)
- Garbage가 많은 Region부터 수거 (Garbage-First!)
- 예측 가능한 STW 시간 (목표 pause time 설정 가능)
- Java 9+에서 기본 GC
G1 GC Heap 구조:
┌────────────────────────────────────────────────────┐
│ [E][E][E][O][O][ ][ ][ ][H][E][O][ ][E][E][E][O] │
│ [O][ ][E][E][O][O][ ][H][E][ ][O][E][ ][E][O][ ] │
│ │
│ E = Eden Region │
│ O = Old Region │
│ H = Humongous Region (큰 객체) │
│ [ ] = Free Region │
└────────────────────────────────────────────────────┘
동작 단계:
1. Young GC: Eden Region을 Survivor/Old로 이동 (병렬, STW)
2. Concurrent Marking: 전체 Heap을 앱 실행 중에 마킹
3. Mixed GC: Old Region 중 Garbage 많은 것부터 수거
4. Full GC: 공간 부족 시 전체 수거 (피해야 함)
ZGC (Z Garbage Collector)
# 사용 설정 (Java 15+ 정식 출시)
-XX:+UseZGC
-XX:SoftMaxHeapSize=8g # 선호하는 최대 Heap 크기
특징:
- 거의 모든 GC 작업을 앱 실행 중에 동시(Concurrent) 수행
- STW 시간: 항상 10ms 미만 (목표)
- Heap 크기에 관계없이 일정한 응답 시간
- 수 TB Heap도 처리 가능
- 컬러 포인터(Colored Pointer)로 객체 상태 추적
# 사용 적합한 경우:
# - 초저지연(Ultra-low latency) 요구 사항
# - 대용량 Heap (수십 GB ~ 수 TB)
# - 실시간 처리 시스템
ZGC 동작 (STW 최소화):
┌─────────────────────────────────────────────────────┐
│ GC Thread: ─ ██████████████████████████ ─ ████ │
│ ↑ ↑ │
│ Concurrent STW(짧음) │
│ Mark/Relocate │
│ App Thread: █████ ─────────────────────── ─ █████ │
└─────────────────────────────────────────────────────┘
Shenandoah GC
# 사용 설정 (Java 15+ 정식, OpenJDK)
-XX:+UseShenandoahGC
특징:
- ZGC와 유사한 저지연 GC
- Concurrent Evacuation (앱 실행 중에 객체 이동)
- Heap 크기에 관계없이 일정한 pause 시간 목표
- ZGC와의 차이: Region 기반 구조 (ZGC는 Page 기반)
GC 성능 지표와 트레이드오프
GC 성능의 3가지 목표 (동시에 모두 달성 불가):
1. Throughput (처리량): GC 시간을 제외한 애플리케이션 실행 시간 비율
2. Latency (지연): GC로 인한 일시 정지 시간
3. Footprint (공간): GC가 사용하는 메모리 양
GC 선택 기준:
┌─────────────────────────────────────────────────┐
│ 처리량 우선 → Parallel GC │
│ 짧은 지연 우선 → G1 GC (권장) │
│ 초저지연 (< 10ms) → ZGC, Shenandoah │
│ 소규모/단일코어 → Serial GC │
└─────────────────────────────────────────────────┘
GC 로그 분석
# GC 로그 설정 (Java 9+)
java -Xlog:gc*:file=gc.log:time,uptime,level,tags \
-Xlog:gc+heap=debug \
-jar myapp.jar
# GC 로그 예시 (G1 GC)
[2024-01-01T10:00:01.234+0900][0.234s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 256M->128M(1024M) 12.345ms
# 의미: Young GC, 256MB → 128MB로 줄어듦, 전체 Heap 1024MB, 12.345ms 정지
[2024-01-01T10:00:10.567+0900][9.567s][info][gc] GC(5) Pause Full (G1 Evacuation Pause) 900M->200M(1024M) 245.678ms
# Full GC 발생 - 245ms 정지 → 튜닝 필요
# Java 8에서 GC 로그
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -jar myapp.jar
GC 튜닝 전략
# 1. Heap 크기 적절히 설정
-Xms4g -Xmx4g # 초기=최대 (GC 오버헤드 감소, 재조정 비용 제거)
# 2. Young Generation 크기 조정 (Minor GC 빈도 조절)
-XX:NewRatio=2 # Young:Old = 1:2
-XX:NewSize=1g # Young Gen 최소 크기
-XX:MaxNewSize=2g # Young Gen 최대 크기
# 3. G1 GC Pause 목표 설정
-XX:MaxGCPauseMillis=100 # 100ms 이하 목표 (기본 200ms)
# 4. GC 스레드 수 설정
-XX:ParallelGCThreads=8 # STW GC 스레드 수
-XX:ConcGCThreads=4 # Concurrent GC 스레드 수 (CPU 코어의 1/4 권장)
# 5. 조기 승격(Premature Promotion) 방지
-XX:MaxTenuringThreshold=15 # Old Gen 승격 임계값 (기본 15)
-XX:SurvivorRatio=8 # Eden:Survivor = 8:1:1 (기본)
# 6. Large Object (Humongous Object) 처리
-XX:G1HeapRegionSize=16m # Region 크기의 50% 이상 객체는 Humongous
# Humongous 객체는 Young GC에서 수거 안 됨 → 큰 객체 생성 최소화
메모리 누수 감지
// 흔한 메모리 누수 패턴들
// 1. static 컬렉션에 계속 추가
class CacheManager {
private static Map<String, Object> cache = new HashMap<>()
// 제거 없이 계속 추가 → 메모리 누수
public static void add(String key, Object value) {
cache.put(key, value) // ❌ 크기 제한 없음
}
// 해결: WeakHashMap 또는 크기 제한
private static Map<String, Object> cache = new LinkedHashMap<>() {
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 1000 // 1000개 초과 시 가장 오래된 항목 제거
}
}
}
// 2. 리스너/콜백 해제 안 함
class EventBus {
private List<EventListener> listeners = new ArrayList<>()
void register(EventListener listener) {
listeners.add(listener) // 등록은 하지만
}
// unregister가 없으면 listeners가 계속 쌓임
void unregister(EventListener listener) {
listeners.remove(listener) // 반드시 해제 필요
}
}
// 3. try-with-resources로 자원 해제
try (Connection conn = dataSource.getConnection()
PreparedStatement ps = conn.prepareStatement(sql)) {
// 자동으로 close() 호출 → 메모리/연결 해제
}
정리 표
| GC 알고리즘 | Java 버전 | STW 시간 | 처리량 | 주요 용도 | 특징 |
|---|---|---|---|---|---|
| Serial GC | 모든 버전 | 긺 | 낮음 | 단일 코어, 소규모 | 단일 스레드 |
| Parallel GC | Java 8 기본 | 중간 | 높음 | 배치 처리 | 멀티 스레드 |
| CMS GC | Deprecated | 짧음 | 중간 | 저지연 (구) | 단편화 문제 |
| G1 GC | Java 9+ 기본 | 예측 가능 | 높음 | 범용 (권장) | Region 기반 |
| ZGC | Java 15+ | < 10ms | 높음 | 초저지연 | 대용량 Heap |
| Shenandoah | Java 15+ | < 10ms | 높음 | 초저지연 | OpenJDK |