2주차 Unit 4.3 — G1 GC의 리전(Region) 기반 레이아웃

Psj·2026년 5월 15일

F-lab

목록 보기
66/240

Unit 4.3 — G1 GC의 리전(Region) 기반 레이아웃

F-LAB JAVA · 2주차 · Phase 4 · G1 GC 심화


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • G1이 힙을 동일한 크기의 리전으로 분할하는 이유는?
  • 리전의 4가지 역할 (Eden, Survivor, Old, Humongous) 의 차이는?
  • Remembered Set (RSet) 은 무엇이며 왜 필요한가?
  • Card Table 은 RSet과 어떻게 협력하나?
  • Young CollectionMixed Collection 의 차이는?
  • -XX:G1HeapRegionSize 옵션을 어떻게 설정해야 하나?
  • ILIC 운영 서버에서 리전 크기를 언제 튜닝하나?

🎯 핵심 한 문장

G1은 힙을 1MB~32MB 크기의 동일한 리전 수천 개로 분할하고, 각 리전의 역할을 동적으로 부여한다.
리전 간 참조는 Remembered Set으로 추적하고, Card Table로 효율화한다.
이 구조 덕에 G1은 "필요한 리전만" 회수할 수 있어 STW 시간을 통제한다.

비유 — 거대 창고의 격자 관리

시스템비유
기존 GC창고를 "원자재", "반제품", "완제품" 큰 구역으로 나눔
G1 리전창고를 같은 크기의 1000개 격자로 분할
동적 역할각 격자가 그때그때 원자재든 반제품이든
Remembered Set"격자 A에서 격자 B의 물건 사용 중" 메모
Card Table큰 격자 안의 작은 카드 단위 추적 (효율)

→ 부분 청소가 자유로움. "오늘은 격자 5, 27, 99만 청소"


🧭 9개 섹션 로드맵

1. 리전 분할의 본질 — 왜 동일 크기
2. 리전의 4가지 역할
3. 리전 크기 결정 — 자동 vs 수동
4. Remembered Set — 리전 간 참조 추적
5. Card Table — RSet의 효율화
6. Young Collection vs Mixed Collection
7. ILIC 실무 — 리전 크기 튜닝
8. 흔한 실수 + 디버깅
9. 면접 질문 + 자기 점검

1️⃣ 리전 분할의 본질 — 왜 동일 크기

1.1 기존 GC vs G1 비교

기존 GC (Parallel, CMS):
  ┌──────── Young ─────────┬─────── Old ───────┐
  │ Eden    │ Surv │ Surv  │                    │
  │ (큰 영역) │ 작음  │ 작음  │  (가장 큰 영역)    │
  └────────┴──────┴──────┴────────────────────┘
  
  특징:
  - 영역 크기 고정 (실행 중 변경 불가)
  - 영역 위치 연속
  - 한 영역 = 한 가지 역할

G1 GC:
  ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
  │E│O│S│ │O│E│ │S│ │O│O│E│ │ │S│O│E│
  └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
  
  특징:
  - 모든 리전 같은 크기 (1MB ~ 32MB)
  - 리전 수: 2048개 권장 (목표)
  - 각 리전 역할은 동적
  - 시간이 지나면서 역할 변경

1.2 왜 동일 크기?

관리 단순화:

  • 모든 리전이 같은 크기 → 메모리 관리 단순
  • 빈 리전 찾기 쉬움 (어떤 빈 리전이든 OK)
  • 회수 후 재사용 자유

시간 예측 가능성:

  • 한 리전 회수 시간 = 거의 일정
  • "리전 N개 회수 = N × 평균 시간"
  • 정지시간 예측 모델 (Unit 4.2)의 기반

비유 — 표준 컨테이너:

표준 컨테이너 (G1):
  - 모든 컨테이너 20피트로 통일
  - 어디든 같은 크기
  - 운반/적재 단순화
  - 처리 시간 예측 가능

비표준 (기존 GC):
  - 컨테이너마다 크기 다름
  - 매번 처리 방식 다름
  - 시간 예측 어려움

1.3 동적 역할 부여

각 리전은 시간에 따라 역할 변경:

시점 T1:
  ┌─┬─┬─┬─┬─┬─┐
  │E│E│E│ │ │ │  ← Eden 3개 (앱이 객체 빠르게 할당 중)
  └─┴─┴─┴─┴─┴─┘

시점 T2 (Eden 가득 → Young GC):
  ┌─┬─┬─┬─┬─┬─┐
  │ │ │ │S│ │ │  ← Eden 비고, Survivor 1개에 생존 객체 모음
  └─┴─┴─┴─┴─┴─┘

시점 T3 (앱이 계속 객체 생성):
  ┌─┬─┬─┬─┬─┬─┐
  │E│E│ │S│ │ │  ← Eden 다시 생기고, Survivor 유지
  └─┴─┴─┴─┴─┴─┘

시점 T4 (Survivor의 객체가 살아남아 Old로 승격):
  ┌─┬─┬─┬─┬─┬─┐
  │E│E│E│O│ │ │  ← S → O (역할 변경)
  └─┴─┴─┴─┴─┴─┘

리전 자체는 그대로, 역할만 변경.

1.4 자기 점검 답변

리전 단위로 쪼개면 어떤 이점이 있는가?

:
1. 부분 회수: 필요한 리전만 처리 → STW 단축
2. 시간 예측: 리전당 시간 일정 → 정지시간 계산 가능
3. 유연성: 역할 동적 변경 → 메모리 활용 효율
4. 단편화 줄임: 빈 리전 재사용 쉬움
5. 확장성: 큰 힙에서도 동작 (수천 리전)


2️⃣ 리전의 4가지 역할

2.1 4가지 역할 개요

G1 리전의 역할:

  E = Eden        : 새 객체가 처음 할당되는 곳
  S = Survivor    : Young GC 후 살아남은 객체가 임시로
  O = Old         : 오래 살아남은 객체
  H = Humongous   : 거대 객체 전용 (Unit 4.4)
  
  (빈 리전)       : 아직 할당 안 됨

2.2 Eden 리전

역할: 새 객체의 첫 거주지.

Shipment s = new Shipment("BL-001");
// → Eden 리전 중 하나에 할당

특징:

  • TLAB(Thread Local Allocation Buffer)이 Eden 안에 있음
  • 빠른 할당 (bump allocation)
  • 짧은 수명 가정 (대부분 객체)
  • Young GC 대상

비율 (기본):

  • 전체 리전의 약 5% (동적 조정)
  • -XX:G1NewSizePercent=5 (최소)
  • -XX:G1MaxNewSizePercent=60 (최대)

2.3 Survivor 리전

역할: Young GC에서 살아남은 객체의 임시 거처.

Young GC 흐름:
  Eden 가득 → Young GC 발동
    ↓
  Eden + Survivor의 객체 중 살아있는 것:
    Survivor 리전으로 복사 (Evacuation)
    ↓
  새 Survivor 리전 + 비워진 Eden

특징:

  • 일정 횟수 이상 살아남으면 Old로 승격 (보통 6~15회)
  • -XX:MaxTenuringThreshold=15 (조정 가능)
  • 짧은 수명의 객체 vs 진짜 오래 살 객체 구분

2.4 Old 리전

역할: 오래 살아남은 객체의 거주지.

승격(Promotion) 조건:
  - Survivor에서 N번 살아남음 (Tenuring Threshold)
  - 또는 Survivor 공간 부족

특징:

  • 캐시, 싱글톤 빈, 장기 유지 객체
  • Mixed Collection 대상 (Young + 일부 Old)
  • 가득 차면 Full GC 발동

2.5 Humongous 리전

역할: 매우 큰 객체 전용.

거대 객체 기준:
  객체 크기 > 리전 크기 / 2

예: 리전 16MB라면
  9MB 객체 = Humongous
  여러 리전을 연속 차지

특징:

  • Old gen에 속함
  • Mixed Collection의 효율 떨어뜨림
  • 다음 Unit 4.4에서 상세

2.6 빈 리전

역할: 아직 사용 안 된 리전.

빈 리전 풀:
  - 새 Eden이 필요할 때 여기서 가져옴
  - GC 후 회수된 리전이 여기로
  - 재사용 가능

2.7 ILIC 시나리오로 보기

// ILIC의 일반적인 요청 처리
@GetMapping("/shipments/{id}")
public ShipmentResponse get(@PathVariable Long id) {
    Shipment s = repository.findById(id).get();   // ← 객체 N개 생성
    return ShipmentResponse.from(s);              // ← 또 객체 생성
}

객체들의 생애:

1. 요청 처리 중:
   Shipment, ShipmentResponse, Cargo 등 → Eden 리전

2. 요청 종료 직후:
   대부분 객체가 참조 끊어짐 → 곧 Young GC 대상

3. Young GC 발동:
   살아있는 것 → Survivor로 이동
   대부분 → 그냥 회수

4. 시간이 지나면:
   Spring Bean들 (ShipmentService 등) → Old로 승격
   Repository 캐시 등 → Old에 살아있음

5. 더 오래:
   Old gen에 살아있는 객체들이 누적
   → 가끔 Mixed GC로 Old 일부 회수

→ 전형적인 Spring 앱의 메모리 패턴.


3️⃣ 리전 크기 결정 — 자동 vs 수동

3.1 자동 결정 (기본)

G1은 힙 크기를 보고 리전 크기 자동 결정.

공식:
  리전 크기 = Heap 크기 / 2048
  (그리고 2의 제곱수로 반올림: 1MB, 2MB, 4MB, 8MB, 16MB, 32MB)

예시:
  Heap 1GB    → 1024/2048 = 0.5MB → 1MB 리전 → 약 1024개
  Heap 4GB    → 4096/2048 = 2MB → 약 2048개 ★
  Heap 16GB   → 16384/2048 = 8MB → 약 2048개
  Heap 64GB   → 64GB/2048 = 32MB → 약 2048개
  Heap 128GB  → 32MB 리전 (최대) → 약 4096개

G1은 약 2048개 리전을 목표.

3.2 수동 설정

-XX:G1HeapRegionSize=16m

값 제약:

  • 1MB, 2MB, 4MB, 8MB, 16MB, 32MB만 가능
  • 다른 값 입력 시 가장 가까운 값으로 자동 조정

3.3 리전 크기의 트레이드오프

작은 리전 (예: 1MB)

장점:

  • 더 세밀한 회수 단위
  • 정지시간 더 짧을 수 있음 (한 리전 처리 빠름)

단점:

  • 리전 수 폭증 → 관리 메타데이터 ↑
  • Card Table 크기 ↑
  • 큰 객체가 자주 Humongous → 효율 ↓

큰 리전 (예: 32MB)

장점:

  • 큰 객체 처리 효율 ↑ (Humongous 줄어듦)
  • 메타데이터 ↓

단점:

  • 한 리전 처리 시간 ↑
  • 정지시간 변동 폭 ↑

3.4 자기 점검 답변

리전 크기를 너무 작게 하면 어떤 문제가?

:
1. 리전 수 폭증 → 관리 오버헤드 증가
2. Card Table 크기 증가 → 메모리 사용 ↑
3. 큰 객체가 자주 Humongous 처리 대상 → GC 효율 ↓
4. Remembered Set 갱신 빈도 ↑

3.5 ILIC 시나리오 — 어떤 크기를 쓸 것인가

일반 권장:
  G1의 자동 결정 신뢰 (대부분 OK)

조정이 필요한 경우:
  - 큰 객체 자주 생성 (16MB 캐시 객체 등)
    → 리전 크기 32MB로 늘려 Humongous 줄이기
  
  - 작은 객체만 생성 + 정지시간 더 단축 원함
    → 리전 작게 (단, 보통 의미 큼 X)
  
  - Heap 매우 큼 (128GB+)
    → 32MB 자동 선택. 그대로 OK

박승제씨가 ILIC에서 직접 조정할 일은 거의 없음.
모니터링 결과 큰 객체 문제 보이면 그때 조정.


4️⃣ Remembered Set — 리전 간 참조 추적

4.1 문제 — 리전 간 참조

리전 분할의 부작용:

  Old 리전 A의 객체 ───참조──► Young 리전 B의 객체

  Young GC를 하려면:
  "B 리전의 객체 중 살아있는 것?" 
  → Young 리전만 보면 안 됨!
  → Old 리전의 참조도 확인해야 함
  
  만약 Old 전체를 매번 스캔하면?
  → Young GC가 Old 스캔만큼 느려짐
  → STW 시간 폭발 → G1의 의미 사라짐

4.2 해결 — Remembered Set (RSet)

각 리전이 자기를 가리키는 외부 참조를 기록.

Region B의 RSet:
  "Region A의 카드 X에서 나에게 참조 있음"
  "Region C의 카드 Y에서 나에게 참조 있음"
  ...

Young GC 시:

  • Region B를 회수하려면
  • B의 RSet 보면 "어디서 나를 참조하는지" 즉시 알 수 있음
  • Old 전체 스캔 불필요

4.3 RSet의 구조

RSet (개념적):
  ┌─────────────────────────────────────┐
  │ Region B의 RSet                      │
  ├─────────────────────────────────────┤
  │ "Region A의 카드 0x123에 참조 있음"  │
  │ "Region C의 카드 0x456에 참조 있음"  │
  │ "Region X의 카드 0x789에 참조 있음"  │
  └─────────────────────────────────────┘

각 리전 = 자기 RSet 보유.

4.4 RSet 갱신 — Write Barrier

언제 RSet이 업데이트되나?

// 객체 필드에 참조 할당
shipment.cargo = c;

JVM의 동작:
1. 평소처럼 필드 할당 (putfield 명령)
2. Write Barrier 발동: "이게 리전 경계 넘는 참조인가?"
3. 만약 그렇다면 RSet 업데이트:

  • cargo가 가리키는 리전의 RSet에
  • "shipment가 있는 리전에서 참조" 기록
Write Barrier 비용:
  - 모든 참조 할당마다 약간의 오버헤드
  - 보통 2~5% 처리량 감소
  - 트레이드오프: GC 속도 vs 일반 코드 속도

4.5 RSet의 비용

Heap이 클수록:
  - 리전 수 많음
  - RSet도 많음
  - 메모리 사용량 증가 가능

추정:
  - 일반: Heap의 1~3%
  - 최악: Heap의 20%까지 (다수의 cross-region 참조)

ILIC 운영 시 RSet 크기 모니터링은:

# GC 로그에서 확인
-Xlog:gc+remset=trace

5️⃣ Card Table — RSet의 효율화

5.1 RSet만으로는 부족

RSet 갱신을 매 참조 할당마다 하면?
→ 비용 너무 큼

해결책: 더 큰 단위로 추적
  → Card

5.2 Card의 개념

Card Table:
  - 힙을 512 byte 또는 1KB 단위 "카드"로 분할
  - 각 카드마다 1 byte의 상태 플래그
  - "dirty" / "clean"

힙 분할:
  ┌──────────────────────────────────────┐
  │ Heap (예: 4GB)                        │
  │ ┌────┬────┬────┬────┬────┬────┬────┐│
  │ │카드│카드│카드│카드│카드│카드│ ... ││  ← 512B 단위
  │ └────┴────┴────┴────┴────┴────┴────┘│
  └──────────────────────────────────────┘

Card Table (별도 메모리):
  ┌──────────────────────────────────────┐
  │ [0|0|1|0|1|0|0|0|1|0| ... ]          │  ← 카드별 1바이트
  │  ↑     ↑     ↑     ↑                  │
  │  clean dirty clean dirty               │
  └──────────────────────────────────────┘

5.3 동작 원리

1. 참조 할당 시:
   shipment.cargo = c;
   
2. Write Barrier:
   - shipment가 있는 카드의 위치 계산
   - 그 카드를 "dirty" 마킹
   - 끝. (RSet 직접 갱신 X)

3. 나중에 (백그라운드 또는 GC 직전):
   - dirty 카드들을 스캔
   - 카드 안의 모든 객체 검사
   - cross-region 참조 발견 시 RSet 갱신

5.4 비용 비교

방법 1: 매번 RSet 갱신
  - 정확함
  - 매번 비싼 작업
  - 처리량 손실 큼

방법 2: Card Table 사용 (G1의 선택)
  - 약간의 부정확함 (dirty 카드에 cross-ref 없을 수도)
  - 매우 빠른 마킹 (1 byte 쓰기)
  - 백그라운드 처리 가능

5.5 Card Table 크기

Heap 4GB, Card 512 bytes:
  카드 수 = 4GB / 512B = 8M개
  Card Table 크기 = 8MB

Heap 32GB:
  Card Table 크기 = 64MB

→ 무시 못할 메모리 비용.
→ 그래도 RSet 직접 갱신보다 훨씬 효율적.

5.6 Refinement Threads

G1은 백그라운드 스레드(Refinement)가:
  - dirty 카드를 주기적으로 스캔
  - 진짜 cross-region 참조 발견 시 RSet 갱신
  - dirty → clean

이 스레드들이 처리 못 따라가면:
  - dirty 카드 누적
  - GC 시 한꺼번에 처리 → STW 증가

옵션:

-XX:G1ConcRefinementThreads=4   # Refinement 스레드 수

기본값은 시스템 코어 수에 따라 자동.

5.7 SATB (Snapshot-At-The-Beginning)

G1의 또 다른 핵심 메커니즘:

GC 시작 시점의 객체 그래프를 "스냅샷"
  ↓
GC 중에 변경된 참조는 따로 기록 (SATB Buffer)
  ↓
GC가 그 스냅샷 기준으로 마킹 작업
  ↓
변경분은 별도로 처리

→ GC 중에도 앱이 동작 가능 (Concurrent).
→ Phase 4.5에서 더 자세히.


6️⃣ Young Collection vs Mixed Collection

6.1 G1의 GC 종류

G1의 GC 사이클:

1. Young Collection (Young GC)
   - Eden + Survivor만 회수
   - STW
   - 빈번 (수 초~분 단위)

2. Mixed Collection
   - Young + 일부 Old 리전 회수
   - STW
   - Concurrent Marking 후 발동

3. Full GC (최후의 수단)
   - 전체 회수
   - 매우 긴 STW
   - 발생하면 안 됨 (튜닝 실패 의미)

6.2 Young Collection 흐름

1. Eden 리전들 가득 참

2. Young Collection 시작 (STW)
   - 회수 대상: 모든 Eden + 현재 Survivor

3. 마킹:
   - GC Roots에서 도달 가능한 객체 찾기
   - RSet 사용 (Old → Young 참조)

4. Evacuation (복사):
   - 살아있는 객체를 새 Survivor로
   - 충분히 오래된 객체는 Old로 승격
   - 또는 새 빈 리전 사용

5. Eden 비움, 사이클 종료
Before Young GC:
  E1, E2, E3 (Eden, 가득)
  S1 (Survivor)
  O1, O2 (Old)

After Young GC:
  (Eden 비었음)
  S2 (새 Survivor, 살아남은 객체)
  O1, O2, O3 (Old, 일부 승격 추가)

6.3 Concurrent Marking — Mixed Collection 준비

Old 사용량이 임계치 도달 (-XX:InitiatingHeapOccupancyPercent=45)
  ↓
Concurrent Marking 시작 (앱과 동시)
  - GC Roots부터 그래프 탐색
  - 살아있는 객체 마킹
  - 각 Old 리전의 "garbage 비율" 계산
  ↓
Marking 완료
  ↓
Mixed Collection 시작 가능

6.4 Mixed Collection 흐름

1. Young + 가장 garbage 많은 Old 리전 선택

2. Mixed Collection 시작 (STW)
   - Young 회수 + 선택된 Old 회수
   - 일반 Young GC보다 약간 길음

3. 살아있는 객체를 다른 리전으로 복사

4. 빈 Old 리전 확보

→ Old 전체를 한 번에 처리하지 않고 여러 사이클에 걸쳐 점진 처리.
→ STW 시간 통제 가능.

6.5 Full GC — 피해야 할 사태

발생 조건:
  - Mixed Collection이 따라가지 못함
  - Old 가득 + Concurrent Marking 실패
  - 매우 큰 객체 할당 시 공간 부족

동작:
  - Serial Full GC (단일 스레드, 매우 느림)
  - 또는 Parallel Full GC (Java 10+)
  - STW 수 초 ~ 수십 초

대응:
  - Heap 크기 증가
  - InitiatingHeapOccupancyPercent 조정
  - 메모리 누수 확인

6.6 GC 로그 보기

[gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 100M->10M(256M) 15.234ms
[gc] GC(1) Pause Young (Concurrent Start) (G1 Evacuation Pause) 110M->20M(256M) 18.567ms
[gc] GC(2) Pause Remark 22M->22M(256M) 5.123ms
[gc] GC(3) Pause Cleanup 22M->22M(256M) 1.234ms
[gc] GC(4) Pause Young (Mixed) (G1 Evacuation Pause) 22M->15M(256M) 30.456ms
[gc] GC(5) Pause Young (Normal) 50M->15M(256M) 12.789ms

해독:

  • (Normal): 일반 Young GC
  • (Concurrent Start): Concurrent Marking 시작
  • Remark: Concurrent Marking의 마무리 (짧은 STW)
  • Cleanup: 리전 정리 (짧은 STW)
  • (Mixed): Young + Old 일부 처리

→ G1의 사이클이 로그로 보임.


7️⃣ ILIC 실무 — 리전 크기 튜닝

7.1 언제 튜닝하나

대원칙: G1의 자동 설정 신뢰

튜닝 검토 시점:
  1. Humongous 리전 발생 빈번 (GC 로그)
  2. Young GC 시간이 일관되지 않음 (변동 큼)
  3. 큰 객체 할당 패턴 (캐시, 대용량 컬렉션)

7.2 Humongous 빈번 시

증상 (GC 로그):
  Pause Young (Concurrent Start) (G1 Humongous Allocation) ...
  ↑ 이런 메시지가 자주

원인:
  객체 > 리전 / 2

해결:
  1. 리전 크기 증가
     -XX:G1HeapRegionSize=32m
  
  2. 큰 객체 자체를 작게 (코드 변경)
     - 큰 배열을 작은 단위로 분할
     - 큰 String → byte[] 처리
  
  3. 캐시 라이브러리 검토
     - 한 큰 객체 vs 여러 작은 객체

7.3 GC 로그 모니터링 ★

ILIC 운영에서 가장 중요한 작업:

# Java 11+의 통합 로그
java -Xlog:gc*:file=/var/log/gc-%t.log:time,uptime,level,tags:filecount=10,filesize=100M \
     -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -Xms4g -Xmx4g \
     -jar ilic-app.jar

분석 도구:

  • GCViewer: GUI, 무료
  • GCEasy (gceasy.io): 웹 기반, 자세한 리포트
  • JClarity Censum: 상용

체크 지표:

✓ 평균 GC STW 시간
✓ P99 GC STW 시간
✓ Full GC 빈도 (이상적: 0)
✓ Young vs Mixed 비율
✓ Concurrent Cycle 실패 여부
✓ Humongous 발생 여부

7.4 ILIC 시나리오 — 운영 사고 분석

상황: "응답시간이 갑자기 길어졌어요"

# 1. GC 로그 확인
tail -100 /var/log/gc-*.log | grep "Pause"

# 결과:
[gc] GC(127) Pause Full (G1 Evacuation Pause) 3500.123ms
                ↑ Full GC 발생!

# 2. Heap dump 확보
jmap -dump:format=b,file=/tmp/heap.hprof <PID>

# 3. MAT로 분석
# - Histogram에서 의외로 많은 클래스 확인
# - "Path to GC Roots" 추적

7.5 운영 옵션 풀세트 (G1 GC)

# ILIC 권장 JVM 옵션

# === 메모리 ===
-Xms4g                                # 초기 힙
-Xmx4g                                # 최대 힙 (Xms와 동일 권장)
-XX:MetaspaceSize=256m                # Metaspace 초기
-XX:MaxMetaspaceSize=512m             # Metaspace 최대

# === G1 GC ===
-XX:+UseG1GC                          # G1 GC 명시
-XX:MaxGCPauseMillis=200              # 정지시간 목표
-XX:G1HeapRegionSize=8m               # 리전 크기 (자동도 OK)
-XX:InitiatingHeapOccupancyPercent=45 # Concurrent Marking 시작 임계치

# === GC 로깅 ===
-Xlog:gc*:file=/var/log/gc-%t.log:time,uptime,level,tags:filecount=10,filesize=100M

# === OOM 대응 ===
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heap-dumps/
-XX:OnOutOfMemoryError="kill -9 %p"   # OOM 시 즉시 종료 (재시작 위해)

# === JFR (선택) ===
-XX:StartFlightRecording=disk=true,maxsize=1g,maxage=24h,filename=/var/log/jfr/app.jfr

7.6 ILIC의 일반적인 GC 패턴

정상 ILIC 앱 (Heap 4GB):

매분:
  Young GC 약 3~5회
  STW 평균: 20~50ms
  P99 STW: 100~150ms

매시간:
  Mixed GC 약 1~3회
  STW: 30~80ms

매일:
  Full GC: 0회 (이상적)
  Concurrent Marking: 약 24~48회

→ 이 패턴 벗어나면 문제.


8️⃣ 흔한 실수 + 디버깅

실수 1 — 리전 크기 무리한 조정

# ❌ 1MB 리전 강제 (작은 리전)
-XX:G1HeapRegionSize=1m

# 32GB Heap이라면?
# 리전 수 = 32GB / 1MB = 32,768개
# 메타데이터 폭증

→ 자동 결정 신뢰. 정말 필요할 때만 조정.

실수 2 — Heap 동적 조정

# ❌ 초기와 최대 다르게
-Xms256m -Xmx4g

# 문제:
# - JVM이 힙을 점진적으로 늘림
# - 늘릴 때마다 GC 발동
# - 워밍업 시간 증가

→ 운영은 -Xms-Xmx를 같은 값으로.

실수 3 — MaxGCPauseMillis를 너무 작게

# ❌ 10ms 목표
-XX:MaxGCPauseMillis=10

# 문제:
# - G1이 회수할 수 있는 리전 수 줄어듦
# - 더 자주 GC 발동
# - Old 가득 차서 Full GC 위험

→ ILIC는 100~200ms가 적절. 그 이하는 ZGC 고려.

실수 4 — Full GC를 무시

GC 로그:
  Pause Full (Allocation Failure) 5000ms
  
"가끔 발생하는 거니까 괜찮아"

❌ 절대 아님:
  - Full GC = 튜닝 실패 신호
  - 누수 가능성 높음
  - SLA 위반

조사:
1. Heap dump
2. Old gen 사용률 추적
3. Concurrent Marking 실패 여부

실수 5 — RSet 크기 무시

GC 로그에서:
  - 갑자기 Young GC 시간 증가
  - 원인: cross-region 참조 많아져서 RSet 처리 비용 ↑

가능성:
  - 큰 객체 그래프 (Cargo가 수많은 Item 참조 등)
  - 캐시가 다양한 리전의 객체 가리킴

해결:

  • 객체 그래프 단순화
  • 캐시 구조 개선

실수 6 — Concurrent Mode Failure

GC 로그:
  to-space exhausted
  Full GC 발동

원인:

  • Mixed GC가 Old 가득 차는 속도 못 따라감
  • 또는 evacuation 중 공간 부족

해결:

  • Heap 크기 증가
  • -XX:InitiatingHeapOccupancyPercent 낮춤 (Concurrent 일찍 시작)
  • -XX:G1ReservePercent 증가 (예비 공간)

디버깅 도구

# 1. GC 로그 실시간
tail -f /var/log/gc.log

# 2. JFR 분석
jcmd <PID> JFR.start duration=60s filename=app.jfr
# JDK Mission Control로 분석

# 3. RSet 정보
jcmd <PID> GC.heap_info

# 4. Heap 상세
jcmd <PID> GC.class_histogram

9️⃣ 면접 질문 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
G1이 리전으로 나누는 이유?부분 회수 + 시간 예측 + 유연성
리전 크기 결정?Heap/2048, 2의 제곱수 자동. 1MB~32MB
리전의 4가지 역할?Eden, Survivor, Old, Humongous
Remembered Set이란?각 리전이 자기를 가리키는 외부 참조 기록
RSet이 왜 필요?Young GC 시 Old 전체 스캔 회피
Card Table은?힙을 512B 카드로 분할. dirty 마킹으로 RSet 효율화
Write Barrier 비용?참조 할당마다 ~2-5% 처리량 손실
Young vs Mixed Collection?Young만 vs Young + 일부 Old
Mixed Collection 트리거?Concurrent Marking 완료 + Old 사용량 임계치
Full GC 발생 시 대응?누수 확인, Heap 증가, InitiatingHeapOccupancyPercent 조정

9.2 자기 점검 체크리스트

기본 이해

  • 리전 분할의 3가지 이점을 안다 (부분 회수, 시간 예측, 유연성)
  • 리전의 4가지 역할을 안다
  • 리전 크기 자동 결정 공식을 안다 (Heap/2048)
  • RSet과 Card Table의 협력을 설명할 수 있다
  • Young / Mixed / Full GC 차이를 안다

실전 적용

  • G1 GC 로그를 읽을 수 있다
  • Humongous 빈번 시 대응할 수 있다
  • Full GC 원인을 추적할 수 있다
  • 리전 크기 튜닝의 트레이드오프를 안다
  • ILIC 운영 옵션을 설정할 수 있다

면접 대비 — 5분 답변

  • G1 리전 모델의 본질
  • RSet + Card Table의 협력
  • Young/Mixed GC의 차이
  • 리전 크기 결정 기준
  • 실무 GC 튜닝 접근

🎯 핵심 요약 — 3줄 정리

1. G1의 핵심 = 동일 크기 리전 분할

  • 1MB~32MB 리전 약 2048개
  • 각 리전 역할 동적 (Eden/Survivor/Old/Humongous)
  • 부분 회수 가능 → 시간 통제

2. Remembered Set + Card Table

  • RSet: 각 리전이 자기를 가리키는 외부 참조 기록
  • Card Table: 512B 카드 단위 dirty 마킹 → 효율화
  • Young GC 시 Old 전체 스캔 회피

3. GC 종류 3가지

  • Young Collection: Eden + Survivor만, 빈번
  • Mixed Collection: Young + 일부 Old, Concurrent 후
  • Full GC: 최후의 수단 (튜닝 실패 신호)

📚 다음으로...

Unit 4.4 — 거대 리전 (Humongous Region)

이번 Unit에서 리전 모델을 봤다면, 다음은 큰 객체의 특수 처리.

  • 거대 객체 기준 (리전 / 2)
  • Humongous 리전의 동작
  • 거대 객체가 GC 효율에 미치는 영향
  • ILIC 코드에서 거대 객체 회피
  • 운영에서 Humongous 검출

Phase 4 진행 상황

🚀 Phase 4 — G1 GC 심화
  ✅ Unit 4.1 참조 카운팅의 한계
  ✅ Unit 4.2 G1 GC의 등장 배경
  ✅ Unit 4.3 리전 기반 레이아웃 ← 여기
  ⏭ Unit 4.4 거대 리전
  ⏭ Unit 4.5 우선순위 기반 회수

2주차 진행 상황

✅ Phase 1 — 자바 변수 ↔ 메모리 매핑 (1.1 ~ 1.6 완주)
✅ Phase 2 — JVM 메서드 실행 메커니즘 (2.1 ~ 2.4 완주)
✅ Phase 3 — 바이트코드와 상수 풀 (3.1 ~ 3.4 완주, 정점)
🚀 Phase 4 — G1 GC 심화 (3/5 진행)
⏭ Phase 5 — 컬렉션 내부 구조
⏭ Phase 6 — Reflection & Iterator
⏭ Phase 7 — Buffer
profile
Software Developer

0개의 댓글