1. 들어가며
어느 날, 운영 중인 애플리케이션에서 갑작스럽게 응답 지연이 발생했다. 시스템 모니터링 결과, GC가 과도하게 실행되면서 STW(Stop-The-World) 시간이 증가하고 있었다. 처음에는 단순한 Minor GC 문제로 보였지만, 자세히 분석해보니 Old 영역에서 발생하는 Major GC와 Full GC가 원인이었다. 이 문제를 해결하기 위해 Heap Dump 분석, GC 로그 모니터링, JVM 옵션 튜닝을 진행하며 얻은 인사이트를 공유하고자 한다.
2. JVM의 GC 종류와 특성
GC의 종류 및 동작 방식
GC는 JVM의 메모리 관리를 자동화하는 핵심 기능이지만, 잘못 설정하면 애플리케이션 성능에 심각한 영향을 미칠 수 있다. 대표적인 GC 방식은 다음과 같다.
- SerialGC
- 단일 스레드로 GC 수행 (STW 시간이 길다)
- 주로 단순한 애플리케이션이나 작은 메모리 환경에서 사용됨
- ParallelGC (JDK 8 기본 GC)
- Young GC를 병렬로 처리하여 STW 시간을 줄임
- -XX:+ParallelGCThreads=n 옵션으로 GC 스레드 수 설정 가능
- ParallelOldGC를 사용하면 Old 영역도 병렬로 처리 가능
- CMS (Concurrent Mark-Sweep) GC
- STW 시간을 최소화하기 위해 Old 영역을 동시 처리
- 단점: CPU 부하 증가, Compaction 기능 부족 → 메모리 단편화 발생 가능
- G1GC (JDK 9 이후 기본 GC)
- Region 단위로 메모리를 관리하여 효율적인 GC 수행
- Mixed GC를 통해 Old 영역을 점진적으로 정리하여 Full GC 발생을 줄임
- Humongous 객체를 위한 전용 영역이 존재
- ZGC / Shenandoah GC (JDK 11 이상)
- 초저지연 GC, STW 시간이 10ms 이하로 유지됨
- JDK 8 기반 환경에서는 고려 대상에서 제외됨
GC의 Stop-The-World(STW)
GC는 메모리를 정리하는 동안 애플리케이션을 일시적으로 멈춘다. 이를 STW(Stop-The-World)라고 하며, GC 알고리즘에 따라 다음과 같이 다르게 발생한다.
- SerialGC: STW를 단일 스레드로 처리 → 지연 시간이 가장 김
- ParallelGC: 멀티스레드로 Young GC 수행 → 속도 개선
- CMS GC: Old 영역을 동시 마킹하여 STW 감소
- G1GC: Region 기반 GC로 STW를 최소화하려 함
3. GC의 기본 동작 원리
GC의 동작 단계
GC는 크게 Mark & Sweep & Compaction 과정을 거친다.
- Mark 단계: GC가 Stack 변수를 탐색하며 참조되는 객체를 표시
- Sweep 단계: Mark 되지 않은 객체를 정리하여 메모리 확보
- Compaction 단계: 메모리를 단편화되지 않도록 정리하여 연속적으로 배치
객체의 이동 경로
JVM에서 객체는 Young → Survivor → Old 영역으로 이동한다.
- Young 영역에서 생성된 객체가 살아남으면 Survivor 영역으로 이동
- Survivor 영역에서 MaxTenuringThreshold 이상 살아남으면 Old 영역으로 승격
- Old 영역이 가득 차면 Major GC 발생
Minor GC / Major GC / Full GC 차이점
- Minor GC: Young 영역이 가득 차면 발생 (빠름)
- Major GC: Old 영역이 가득 차면 발생 (느림, STW 영향 큼)
- Full GC: Heap 전체를 정리 (Young + Old + Metaspace) → 가장 느림
4. Metaspace와 메모리 관리
Metaspace란? (JDK 8 이후 도입)
- 기존의 PermGen을 대체하는 JVM의 네이티브 메모리 영역
- 클래스 메타데이터(Class Metadata) 저장
- 기본적으로 Full GC에서만 정리되지만, 일부 GC(G1GC, ZGC)에서는 Major GC 중에도 정리될 수 있음
Metaspace 관리 최적화
- -XX:MetaspaceSize 및 -XX:MaxMetaspaceSize 옵션 설정으로 메모리 사용 조절
- -XX:+ClassUnloadingWithConcurrentMark 활성화하여 클래스 언로드 최적화
5. G1GC 최적화 경험
운영 환경에서는 JDK 8의 기본 GC인 ParallelGC 대신, STW 시간을 줄이기 위해 G1GC를 선택했다. 그러나 G1GC도 특정 상황에서는 Full GC가 발생할 수 있다.
G1GC에서 Full GC가 발생하는 경우
- Humongous 객체가 과도하게 많음 → Region을 초과하는 큰 객체 할당 문제
- Mixed GC가 Old 영역을 충분히 정리하지 못함 → Full GC로 전환
- Metaspace 부족 → 클래스 언로드 필요
- Region 부족 → 새로운 객체 할당이 불가능
GC 최적화 적용 사례
✅ Heap Dump 및 GC 로그 분석
- jmap -dump:format=b,file=heap_dump.hprof 활용하여 불필요한 객체 확인
- Eclipse MAT을 사용하여 객체 참조 문제 분석
✅ UseStringDeduplication 적용
- Thread Dump에서 new String(byte[]) 호출이 과도하게 발생하는 것을 발견
- -XX:+UseStringDeduplication 옵션 활성화하여 메모리 사용 최적화
✅ GC 튜닝 옵션 적용
- -XX:G1HeapRegionSize=8m → Region 크기 조절
- -XX:InitiatingHeapOccupancyPercent=30 → Mixed GC 조기 실행
6. 마무리하며
GC 최적화는 단순한 JVM 옵션 변경이 아니라, 애플리케이션의 메모리 패턴을 깊이 이해하고 실험을 통해 개선해야 한다. Heap Dump 및 GC 로그를 활용하여 시스템의 메모리 문제를 파악하고, 적절한 GC 정책을 선택하는 것이 중요하다. 앞으로도 지속적으로 GC 최적화를 연구하며 성능을 개선해 나갈 계획이다.
'언어 > Java' 카테고리의 다른 글
[Java] ThreadPool 비교: 어떤 쓰레드 풀이 가장 좋을까? (0) | 2025.02.04 |
---|---|
[Java] 동기식 처리와 비동기식 처리 ( feat. 블록킹 논블록킹을 곁들인 ) (0) | 2025.01.11 |
[Java] record type (0) | 2022.12.06 |
[Java] KST 시간변환 (0) | 2022.08.16 |
[JUnit5] 테스트 코드 시작해보기 - 2 (0) | 2022.07.15 |
댓글