728x90
아래에서는 JVM 힙(Heap) 메모리 사용량이 증가함에 따라 Minor GC(Young GC)가 발생하고, 이로 인한 CPU 사용량 상승이 이어지다가 결국 Full GC로 넘어가는 과정을 단계별로 자세히 설명하고, 이해를 돕기 위한 ASCII 그림도 함께 제시합니다.
1. JVM 힙 구조 개요
┌─────────────────────────────────────────────────────┐
│ JVM Heap │
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Young Gen │ │ Old Gen │ │
│ │ │ │ (Tenured) │ │
│ │ ┌───┐ ┌───┐ ┌─┐ │ Metaspace/│
│ │ │Eden│Survivor0│Survivor1│ │ PermGen │
│ │ └───┘ └───┘ └───┘ │ │ │
│ └───────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────┘
- Eden: 객체가 처음 생성되는 영역
- Survivor 영역: Eden에서 살아남은 객체들이 옮겨지는 곳(두 개가 서로 토글)
- Old Gen: 여러 번 살아남은(승격된) 객체들이 쌓이는 곳
- PermGen/Metaspace: 클래스 메타데이터·JIT 컴파일 코드 등이 저장
2. Minor GC(Young GC) 발생 과정
- Eden满 (Allocated Eden) 감지
애플리케이션이 객체를 생성하여 Eden 영역이 포화 상태에 이르면 Minor GC가 트리거됩니다. - Minor GC 수행
- Mark: Eden과 Survivor0 영역의 참조를 검사하여 살아 있는 객체를 식별
- Copy: 살아남은 객체를 Survivor1으로 복사
- Sweep(implicit): Eden과 이전 Survivor0를 깨끗이 비움
- Promotion: 일정 횟수 이상 살아남은 객체들은 Old Gen으로 승격
- 결과
- Eden이 비워지고, Survivor와 Old Gen으로 일부 객체 이동
- CPU 사용량: GC 스레드가 동작하며 일시적으로 CPU 사용량 상승
- Stop-the-World: GC 동안 애플리케이션 스레드는 일시 중단
[Minor GC 전] [Minor GC 후]
┌─────────┐ ┌─────────┐
│ Eden │■■■■■■■■■■ │ Eden │ (비워짐)
│ S0 │■■■ │ S0 │ (비워짐)
│ S1 │ │ S1 │■■■■■ (새 복사본)
│ Old Gen │■■■■■■■■ │ Old Gen │■■■■■■■ (승격 포함)
└─────────┘ └─────────┘
3. Minor GC 후 CPU 사용량 상승과 Old Gen 포화
- Minor GC가 반복되어 Eden→Survivor→Old Gen 승격 작업이 계속되면
- Old Gen 영역이 점점 채워지고,
- Survivor 영역에서도 복사 비용이 누적되어 GC 주기가 짧아지고 CPU 부하가 증가
CPU 사용량 증가 이유
- GC 스레드가 Mark-Copy 작업에 집중
- 애플리케이션 스레드가 멈추는 시간이 늘어나며 시스템 전체적인 처리율 저하
4. Old Gen 임계치 도달 → Full GC 발생
- Promotion 실패 / Old Gen 부족 감지
- Survivor에서 Old Gen으로 승격할 공간이 충분치 않을 때
- Old Gen 내부 단편화(Fragmentation)가 심할 때
- Full GC 트리거
- Young + Old Gen + PermGen/Metaspace 전 영역에 대해
- Mark: 모든 루트(Root) 참조에서 도달 가능한 객체 식별
- Sweep/Compact: 불필요 객체 제거 후 메모리 단편화 해소를 위해 힙을 압축(Compaction)
- 대규모 CPU 사용량
- Heap 전체를 대상으로 수행하므로 CPU 사용량이 더욱 크게 증가
- Stop-the-World 시간이 길어지며 애플리케이션 응답 지연 발생
[Full GC 단계]
┌───────────────────────────────────────────────┐
│ 1. MARK (모든 객체 도달성 탐색) │
└───────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────┐
│ 2. SWEEP (불필요 객체 제거) │
└───────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────┐
│ 3. COMPACT (단편화 해소를 위해 유효객체 이동) │
└───────────────────────────────────────────────┘
5. 전체 흐름 다이어그램
┌─────────────────────────────────────────────────────────────────┐
│ 애플리케이션 객체 생성 → Eden 포화? │
│ │ │
│ └── No → 정상 객체 생성 지속 │
│ │ │
│ Yes │
│ │ │
│ ┌───────────────┐ │
│ │ Minor GC │<────── Eden, S0 검사/복사 ─────┐│
│ └───────────────┘ ││
│ │ ││
│ Survivor→Old Gen 승격 및 Eden/S0 클리어 ││
│ │ ││
│ Old Gen 포화 임박? ─── No → Minor GC 주기 반복 ││
│ │ ││
│ Yes ││
│ │ ││
│ ┌───────────────┐ ││
│ │ Full GC │<───── Heap 전체 Mark & Sweep ───┘│
│ └───────────────┘ │
│ │ │
│ 메모리 단편화 해소(Compact) 및 힙 전체 클리어 │
│ │ │
│ 애플리케이션 재개 │
└─────────────────────────────────────────────────────────────────┘
요약
- Eden 포화 → Minor GC 실행 → Eden/S0 비움, 일부 객체 Old Gen 승격
- Minor GC 반복 → Old Gen 거의 포화, GC 주기 짧아져 CPU 사용량 급증
- Old Gen에 승격 실패/단편화 심해지면 → Full GC 실행 → 힙 전체 정리·압축
- Full GC 동안 CPU 사용량 매우 높아지고, 애플리케이션은 Stop-the-World
이 과정을 통해 JVM은 메모리를 주기적으로 회수하고 단편화를 방지하지만, GC가 자주·오래 실행되면 성능 저하의 주요 원인이 될 수 있습니다. GC 튜닝(Young/Old Gen 크기 조절, GC 알고리즘 선택 등)을 통해 애플리케이션 특성에 맞는 최적화를 고려해야 합니다.
1. 기본 GC: G1 (Garbage-First) GC
- 특징
- 힙을 동일 크기의 “Region”(예: 1~32 MB)으로 분할
- Young 영역(일부 Region)과 Old 영역(나머지 Region)으로 동적으로 할당
- Pause Time 목표 설정(예:
-XX:MaxGCPauseMillis=200)에 맞춰 GC 작업 계획
- 동작 단계
- Young GC
- Eden Region 포화 시, 살아남은 객체를 Survivor로 복사(Region 단위)
- Survivor→Old Gen 승격
- Mixed GC
- Old Gen 점유율이
-XX:InitiatingHeapOccupancyPercent이상일 때 트리거 - Young GC에 추가로 일정 수의 Old Region을 처리(마킹+복사)
- Full GC를 최대한 미루도록 설계
- Old Gen 점유율이
- Young GC
Adaptive IHOP(Adaptive Initiating Heap Occupancy Percent)은 G1 GC가 Initial Mark(Concurrent Start) 사이클을 언제 시작할지를 런타임 환경에 맞춰 자동으로 조절해 주는 기능입니다.
- 초기 임계치 결정
-XX:InitiatingHeapOccupancyPercent로 지정한 값(기본 45%)을 초기값으로 사용- Old Gen 크기의 일정 비율을 넘으면 첫 번째 Concurrent Start가 트리거됨 (stackoverflow.com)
- 관측 기반 자동 조정
- G1 GC는 마킹(Marking) 단계 소요 시간과 Old Gen에 할당되는 객체 양을 관측
- 이 데이터를 바탕으로, 다음 사이클에서는 더 적절한 IHOP 임계치를 계산
- 결과적으로 초기값을 기준으로 점차 목표 Pause Time(
-XX:MaxGCPauseMillis)에 맞추도록 튜닝
- 내부 동작
- G1은 “첫 번째 Mixed GC가 시작될 때의 Old Gen 사용량”을위 공식으로 잡힌 지점에 맞추려 함
현재 최대 Old Gen 크기 ────────────────────── - XX:G1HeapReservePercent(버퍼 공간)- 이 버퍼는 G1이 Mixed GC 없이도 할당을 계속 허용할 최소 여유 공간 역할 (docs.oracle.com)
- 비활성화 방법
- 적응형 조정이 불필요하거나 고정값만 사용하고 싶다면옵션을 추가해 꺼 버릴 수 있음 (docs.oracle.com)
-XX:-G1UseAdaptiveIHOP
왜 Adaptive IHOP인가?
- 워크로드 변화 대응
- 메모리 할당 패턴이나 객체 생존율이 달라져도, GC가 최적의 시점에 마킹을 시작
- Full GC 회피
- 적절한 시점에 Concurrent Marking을 시작해 Old Gen이 과도하게 채워지기 전 Mixed GC로 회수
- Pause Time 제어
- 마킹 작업 시간이 길어지면 더 일찍 마킹을 시작하도록 임계치를 낮춰, 목표 지연 시간 유지
Adaptive IHOP을 통해 G1 GC는 애플리케이션 특성과 실행 환경에 맞춰 더 스마트하게 Mixed GC 주기를 조절하고, 불필요한 Full GC와 긴 Stop-the-World를 최소화합니다.
'개발자로서 살아남기' 카테고리의 다른 글
| 챌린지 배틀 회고 근데 이제 OOP와 도메인 주도 설계를 곁들인 (0) | 2025.08.01 |
|---|---|
| 개발자로서 살아남기 - 루아스크립트를 실무에서 사용한 이유 (0) | 2025.08.01 |
| JAVA - Synchronized와 ThreadLocal 을 이용한 동시성 문제 해결 (0) | 2024.05.03 |
| 스카우터를 활용한 서비스 성능 최적화 (0) | 2023.09.19 |
| Git 원격 저장소 성능 유지하기 (feat - Git 크라켄 성능 최적화) (1) | 2023.08.29 |