1. GC 소개
C 또는 C++로 코딩해 본 적이 있다면, free()나 delete로 메모리를 수동 해제해야 했던 경험이 있을 것입니다. Java에서는 훨씬 간단합니다. new로 객체를 생성하면 되고, 삭제는 신경 쓰지 않아도 됩니다 — 이를 담당하는 특별한 “청소부”가 바로 가비지 컬렉터(Garbage Collector, GC)입니다.
GC는 JVM의 일부로서 더 이상 참조되지 않는 객체가 점유한 메모리를 자동으로 해제합니다. 덕분에 Java 개발자는 메모리 해제를 깜박해 누수가 생기거나, 반대로 아직 필요한 객체를 실수로 해제해 크래시가 나는 상황을 크게 걱정하지 않아도 됩니다.
하지만 다른 청소부와 마찬가지로 GC도 완벽하진 않습니다. 가장 곤란한 순간에 “대청소”(Stop-the-World)를 일으키거나, 기대만큼 빠르게 동작하지 않을 때가 있죠. 그래서 JVM에는 여러 구현의 가비지 컬렉터가 존재하며 — 올바른 선택이 애플리케이션 성능에 큰 영향을 줄 수 있습니다.
기본 GC 유형
Serial GC
- Serial GC는 가장 단순하고 오래된 가비지 컬렉터입니다.
- 단일 스레드로 동작합니다.
- 수집 동안 다른 모든 스레드를 중단합니다(Stop-the-World).
- 활발한 멀티스레딩이 없는 작은 애플리케이션에 적합합니다.
- 플래그로 활성화: -XX:+UseSerialGC
Parallel GC
- Parallel GC(“Throughput Collector”라고도 함).
- 여러 스레드를 사용해 가비지를 수집합니다.
- 최대 처리량에 중점을 둡니다.
- 여전히 Stop-the-World 일시정지를 동반하지만, Serial보다 빠릅니다.
- 짧은 일시정지가 치명적이지 않은 서버 애플리케이션에 적합합니다.
- 플래그로 활성화: -XX:+UseParallelGC
CMS (Concurrent Mark Sweep)
- CMS는 한때 널리 쓰였던, 일시정지를 최소화하는 구형 GC입니다.
- 애플리케이션과 부분적으로 병렬로 동작하여 중단 시간을 줄입니다.
- 설정이 더 복잡하고 오버헤드가 있습니다.
- Java 9부터는 deprecated로 표시되었습니다.
- 플래그로 활성화: -XX:+UseConcMarkSweepGC
G1 (Garbage First)
- G1 GC는 현대적인 기본 가비지 컬렉터입니다(Java 9부터).
- 최소한의 일시정지와 성능 간 균형을 맞춥니다.
- 힙을 많은 작은 영역으로 나눕니다(영역 기반 모델).
- 전체 힙을 건드리지 않고 영역별로 선별 수집이 가능합니다.
- 목표 최대 일시정지 시간을 지정할 수 있습니다. 예: -XX:MaxGCPauseMillis=200.
- 활성화 플래그: -XX:+UseG1GC (보통 불필요, 기본이 G1).
ZGC와 Shenandoah
- ZGC와 Shenandoah는 현대적인 저지연(low‑latency) 가비지 컬렉터입니다.
- 목표는 거대한 힙(최대 TB 규모)에서도 밀리초 수준의 최소 일시정지입니다.
- 애플리케이션과 거의 완전히 병렬로 동작합니다.
- Java 11+(ZGC) 또는 Java 12+(Shenandoah)가 필요합니다.
- 지연 시간에 민감한 시스템(증권거래소, 핀테크, 실시간 분석)에 적합합니다.
- 활성화 플래그: -XX:+UseZGC 또는 -XX:+UseShenandoahGC
3. 최신 GC의 동작 원리
Young/Old 세대(Generation)
JVM은 힙을 크게 두 부분으로 나눕니다:
Young Generation: 모든 새 객체가 여기에 생성됩니다. 이 영역은 수집이 자주 그리고 빠르게 일어납니다(Minor GC).
Old Generation (Tenured): Young에서 여러 차례의 수집을 “생존”한 객체가 이곳으로 승격됩니다. 이 영역의 수집은 덜 자주 일어나지만 시간이 더 오래 걸릴 수 있습니다(Major/Full GC).
왜 이렇게 나눌까요? Java의 대부분 객체는 매우 짧게 살아갑니다(예: 임시 문자열, 메서드 내부의 컬렉션). 따라서 Young은 자주 빠르게 치우고, Old는 건드리지 않는 편이 효율적입니다.
Minor GC
- Young Generation만 정리합니다.
- 빠르고 일시정지가 짧습니다.
- Old 객체는 건드리지 않습니다.
Major (Full) GC
- 힙 전체(Young과 Old 모두)를 정리합니다.
- 큰 힙에서는 시간이 오래 걸릴 수 있습니다(수 초 이상).
- 보통 애플리케이션의 긴 일시정지를 동반합니다.
GC는 어떤 객체를 삭제할지 어떻게 결정할까?
GC는 루트 참조(root set)에서 “살아있는” 객체를 찾기 시작합니다: 스레드 스택의 지역 변수, 정적 필드, 메서드 매개변수 등. 도달 가능한 것은 모두 살아있는 것으로 간주하고, 나머지는 가비지로 처리합니다.
4. 최신 GC 비교: G1, ZGC, Shenandoah
가장 최신이면서 널리 쓰이는 GC의 차이를 살펴봅시다. 아래 표를 참고하세요:
| GC | 주요 목표 | 메모리 모델 | 최소 일시정지 | 확장성 | 지원 | 사용 시나리오 |
|---|---|---|---|---|---|---|
| G1 | 일시정지/속도의 균형 | 영역(Region) | ~10–200 ms | 수백 GB까지 | Java 9+ (기본값) | 대부분의 서버 애플리케이션 |
| ZGC | 최소 일시정지 | 영역, “컬러 태그(coloring)” | <10 ms | 최대 TB 규모 | Java 11+ | 실시간, 지연 시간 민감 |
| Shenandoah | 최소 일시정지 | 영역, “컬러 태그(coloring)” | <10 ms | 최대 TB 규모 | Java 12+ (Red Hat) | 실시간, 지연 시간 민감 |
G1 GC: Garbage First
- 힙을 많은 영역으로 나눕니다(보통 각 1–32 MB).
- 수집 시 가비지가 가장 많은 영역을 우선적으로 선택합니다(“garbage first”).
- 힙 전체가 아닌 일부만 선택적으로 수집할 수 있습니다.
- 목표 일시정지를 지정할 수 있습니다: -XX:MaxGCPauseMillis=200.
- 속도와 일시정지 간의 균형에 적합하며, Java 9부터 기본값입니다.
활성화 예시(만약 비활성화되어 있다면):
java -XX:+UseG1GC -jar myapp.jar
ZGC: Z Garbage Collector
- Java 11에서는 실험적이고, Java 15부터 안정화되었습니다.
- 애플리케이션을 거의 멈추지 않습니다: 힙이 1–2 TB여도 일시정지는 보통 <10 ms입니다.
- “컬러 태그”(coloring)와 특수 포인터를 사용합니다.
- 64비트 JVM이 필요하며, 32비트 시스템에서는 동작하지 않습니다.
- Linux, macOS, Windows를 지원합니다.
활성화 예시:
java -XX:+UseZGC -jar myapp.jar
Shenandoah
- Red Hat이 개발했으며, 목표는 ZGC와 유사합니다.
- 최소 일시정지, 애플리케이션과 적극적으로 병렬 동작합니다.
- Linux와 Windows를 지원하며, 일부 OpenJDK 빌드에 포함됩니다.
- 유사한 기법을 사용하지만 내부 알고리즘은 다릅니다.
활성화 예시:
java -XX:+UseShenandoahGC -jar myapp.jar
시각적 비교
graph TD
A[Young 세대] -->|Minor GC| B[Old 세대]
B -->|Major GC| C[GC Pause]
D[G1: 영역] --> E[선택적 영역 수집]
F[ZGC/Shenandoah: 영역] --> G[병렬 수집]
5. 실전: GC 확인 및 변경 방법
사용 중인 GC를 어떻게 확인할까?
- JVM 로그: 애플리케이션을 -Xlog:gc*(Java 9+) 또는 -verbose:gc(Java 8 이하)로 실행합니다. 로그에서 사용 중인 GC와 일시정지 발생 빈도를 확인할 수 있습니다.
- jcmd: 다음을 실행하세요:
여기서 <pid>는 Java 프로세스 ID입니다.jcmd <pid> VM.flags - jvisualvm: “모니터링” 섹션에서 GC 유형을 확인할 수 있습니다.
애플리케이션의 GC를 변경하는 방법?
Java 프로그램 실행 시 필요한 플래그를 추가하세요:
G1 GC(기본값이지만 명시 가능):
java -XX:+UseG1GC -jar myapp.jar
ZGC:
java -XX:+UseZGC -jar myapp.jar
Shenandoah:
java -XX:+UseShenandoahGC -jar myapp.jar
힙 크기와 일시정지 목표 설정
- 최대 힙 크기: -Xmx2G
- 최소 힙 크기: -Xms512M
- G1의 경우 원하는 일시정지: -XX:MaxGCPauseMillis=200
전체 실행 예:
java -Xms512M -Xmx2G -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar myapp.jar
6. 용도별 GC 선택 가이드
G1을 선택할 때
- 대부분의 서버 및 데스크톱 애플리케이션에서 훌륭한 기본 선택입니다.
- 수백 MB에서 수백 GB 규모의 힙에서 잘 동작합니다.
- 성능과 일시정지의 균형을 잘 맞춥니다.
ZGC 또는 Shenandoah를 선택할 때
- 애플리케이션이 지연 시간에 민감한 경우(증권거래소, 온라인 게임, 실시간 분석).
- 힙이 매우 큰 경우(수백 GB 이상).
- 밀리초 수준의 최소 일시정지만 허용되는 경우.
- Java 11+(ZGC) 또는 Java 12+(Shenandoah)가 필요합니다.
Parallel GC로 충분한 경우
- 일시정지가 치명적이지 않고 최대 처리량이 중요한 작은 애플리케이션.
- Full GC로 인한 중단을 “감당”할 수 있는 배치 처리.
7. 예시: 간단한 애플리케이션에서 GC 동작 비교
임시 객체를 많이 생성하는 작은 애플리케이션(주문 처리 시뮬레이션):
public class GCSimulator {
public static void main(String[] args) {
while (true) {
// 매 사이클마다 100,000개의 객체 생성
for (int i = 0; i < 100_000; i++) {
String s = new String("Order-" + i);
}
// 잠시 휴식
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}
}
서로 다른 GC로 실행하고 로그를 확인하세요:
java -Xmx256M -XX:+UseG1GC -Xlog:gc* GCSimulator
java -Xmx256M -XX:+UseZGC -Xlog:gc* GCSimulator
무엇을 보게 될까요?
G1은 잦지만 짧은 일시정지를 수행합니다. ZGC/Shenandoah는 일시정지가 더 짧지만 더 자주 발생할 수 있습니다. Parallel GC는 일시정지는 더 길지만 빈도는 낮습니다.
8. GC 사용 시 흔한 실수와 주의점
오류 №1: GC가 메모리 문제를 모두 해결해 준다고 기대하기. GC는 만능 해결책이 아닙니다. 불필요한 객체에 대한 참조를 계속 보유하고 있으면 어떤 GC도 도와줄 수 없습니다 — 메모리 누수가 발생합니다.
오류 №2: System.gc()를 강제로 호출하기. JVM은 언제 수집할지 스스로 가장 잘 알고 있습니다. 강제 GC는 긴 일시정지를 유발하고 성능을 떨어뜨릴 수 있습니다.
오류 №3: GC 로그를 무시하기. GC 로그를 보지 않으면 애플리케이션이 정기적으로 Full GC로 인해 “멈추는” 문제를 놓칠 수 있습니다.
오류 №4: 더 이상 권장되지 않는 GC 사용. 예를 들어 CMS는 더 이상 발전하지 않습니다. G1 또는 최신 저지연 컬렉터로 전환하는 것이 좋습니다.
오류 №5: 과제에 맞지 않는 GC 선택. 지연 시간에 민감한 애플리케이션에서 Parallel GC를 사용하면 긴 일시정지를 감수해야 합니다. 반대로 배치 처리에서 ZGC를 켜면 불필요한 오버헤드가 발생할 수 있습니다.
GO TO FULL VERSION