본문 바로가기
카테고리 없음

주니어를 위한 가비지 컬렉터와 메모리 할당 전략3 - 저지연 가비지 컬렉터

by 코헤0121 2025. 10. 20.
728x90
반응형

3.6 저지연 가비지 컬렉터

가비지 컬렉터를 평가하는 3가지 핵심 지표가 있어요:

가비지 컬렉터의 불가능한 삼각형

이 세 가지를 모두 완벽하게 만족하는 컬렉터는 만들기 어려워요. 경제학의 '불가능의 삼각 정리'처럼 말이죠!

왜 지연 시간이 가장 중요해졌을까?

하드웨어 발전의 영향

  • 메모리 용량: 요즘 서버는 메모리가 넉넉해서 컬렉터가 조금 더 써도 괜찮아요
  • CPU 성능: 좋은 하드웨어 = 높은 처리량 = 컬렉터 영향 감소
  • 지연 시간: 하지만 이건 달라요! 메모리가 늘어날수록 청소 시간도 늘어나거든요

직관적인 예시

  • 1GB 힙 청소 시간 < 1TB 힙 청소 시간 (당연하죠!)
  • 그래서 지연 시간이 가장 해결하기 어려운 지표가 되었어요

가비지 컬렉터 진화 과정

컬렉터 역사 한눈에 보기

시리얼 → 패러렐 → CMS → G1 → ZGC/Shenandoah
  📍      📍      📍    📍        📍
전체정지  전체정지   부분동시  부분동시    거의동시

각 컬렉터의 동시성 비교

저지연 GC 비교표

  ZGC Shenandoah 세대 구분 ZGC
정지 시간 10ms 이하 (힙 크기 무관) 10ms 이하 10ms 이하 + 세대별 최적화
최대 힙 크기 최대 16TB (JDK 13+) 제한 없음 (실질적으로 TB급) 최대 16TB
동시 이주 ✅ 지원 ✅ 지원 ✅ 지원
세대 구분 ❌ 없음 (단일 세대) ❌ 없음 (단일 세대) ✅ Young/Old Gen 구분
가용성 Oracle JDK, OpenJDK OpenJDK (배포판에 따라 다름) Oracle JDK, OpenJDK (JDK 21+)
안정화 버전 JDK 15+ (프로덕션) JDK 15+ (프로덕션) JDK 21+ (아직 발전 중)
메모리 오버헤드 중간 정도 약간 더 높음 낮음 (세대별 관리로 효율적)
처리량 G1GC 대비 약간 낮음 G1GC 대비 약간 낮음 ZGC 대비 개선
JVM 옵션 비교 # JDK 15 이전 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -XX:SoftMaxHeapSize=30g -XX:+UseShenandoahGC -XX:ShenandoahGCHeuristics=adaptive # JDK 21+ -XX:+UseZGC -XX:+UseGenerationalZGC
주요 차이점 - Oracle과 OpenJDK 모두에서 사용 가능 - 16TB까지 안정적으로 지원 - 힙 크기에 관계없이 일관된 저지연 보장 - 단일 세대 방식으로 구현이 단순함 - RedHat 주도로 개발 - 일부 Oracle JDK 배포판에서는 제외될 수 있음 - ZGC와 유사한 성능이지만 약간 더 높은 메모리 오버헤드 - 다양한 휴리스틱 옵션 제공 - 더 나은 성능: 신세대/구세대 구분으로 효율성 향상 - 기본 모드로 격상 예정

동시 이주(Concurrent Relocation)의 비밀

기존 방식의 문제

  1. 객체 찾기 (동시 가능) ✅
    2. 객체 이동 (정지 필요) ❌ ← 여기서 병목! (SWT 쩐다!)
    3. 참조 업데이트 (정지 필요) ❌

ZGC/Shenandoah의 해법

  1. 객체 찾기 (동시) ✅
  2. 객체 이동 (동시!) ✅ ← 혁신!
  3. 참조 업데이트 (동시!) ✅

컬러드 포인터 (ZGC)

  • 64비트 포인터의 일부 비트를 메타데이터로 활용
  • 객체 상태를 포인터에 직접 인코딩
  • 별도 메타데이터 테이블 불필요!

포워딩 포인터 (Shenandoah)

  • 객체 헤더에 새 위치 정보 저장
  • 이동 중인 객체에 대한 접근을 투명하게 처리

Shenandoah

Shenandoah는 레드햇(Red Hat)이 독자적으로 개발한 첫 번째 핫스팟 가비지 컬렉터예요. 그런데 여기서 드라마가 시작돼요...

오라클의 견제

  • 🚫 JDK 12부터 지원 거부 입장 표명
  • 🔒 오라클 JDK에서는 조건부 컴파일로 완전히 제외
  • 💸 "유료 상용" 오라클 JDK < "무료 오픈소스" OpenJDK 기능 역전 현상

현재 상황

✅ OpenJDK (Red Hat, Amazon 등): Shenandoah 포함
❌ Oracle JDK: Shenandoah 제외

Shenandoah의 목표

핵심 미션: 힙 크기와 상관없이 GC 정지 시간을 10ms 이내로 제한! 이를 위해서는 표시 단계뿐만 아니라 객체 회수와 마무리 작업까지 사용자 스레드와 동시에 수행해야 했어요.

🤝 G1과의 공통점

Shenandoah는 사실 G1의 좋은 후계자예요:

비슷한 점들:

  • 힙을 리전으로 분할 관리
  • 거대 리전 (큰 객체) 지원
  • 가비지 우선 회수 전략
  • 🔄 코드 일부를 직접 공유 (버그 수정, 개선사항 동시 반영)

Shenandoah 덕분에 G1이 받은 혜택:

  • 🔧 전체 GC를 멀티스레드로 처리 가능해짐
  • 🐛 다양한 버그 수정사항 공유

**
차이점들**

  • 동시 이주(Concurrent Evacuation)
    • G1:        [표시: 동시] → [이주: 정지] ❌
      Shenandoah: [표시: 동시] → [이주: 동시] ✅
      
  • 세대 구분 없음 (JDK 21까지)
    • G1: 신세대/구세대 구분
    • Shenandoah: 모든 리전을 동일하게 처리 (복잡도 vs 성능 트레이드오프)
  • 연결 행렬(Connection Matrix)
    • 기존 방식 (기억 집합)
      • 메모리 많이 사용
      • 거짓 공유 문제 발생
    • Shenandoah 방식 (연결 행렬):

이차원 표로 간단하게 리전 간 참조 관리! 💡

Shenandoah의 9단계 동작 과정

전체 프로세스

1️⃣ 최초 표시      [🟥] GC 루트 직접 참조 객체 표시
2️⃣ 동시 표시      [🟦] 객체 그래프 탐색 (사용자와 동시)
3️⃣ 최종 표시      [🟥] 보류 표시 완료 + 회수집합 생성
4️⃣ 동시 청소      [🟦] 완전히 빈 리전들 청소
5️⃣ 동시 이주      [🟦] 🌟 핵심! 객체들을 새 리전으로 복사
6️⃣ 최초 참조 갱신  [🟥] GC/사용자 스레드 집결지 설정
7️⃣ 동시 참조 갱신  [🟦] 옛 주소 → 새 주소 참조 수정
8️⃣ 최종 참조 갱신  [🟥] GC 루트 참조 갱신
9️⃣ 동시 청소      [🟦] 최종 정리 작업

범례: 🟥 = 일시정지, 🟦 = 동시실행

동시 이주 단계

가장 혁신적인 부분이에요! 다른 컬렉터들이 못 한 걸 해냈거든요.

문제상황

GC 스레드: "이 객체를 A 위치에서 B 위치로 옮겨야지!"
사용자 스레드: "어? 나는 이 객체를 지금 쓰고 있는데?"

이런 동시성 문제를 어떻게 해결했을까요?

포워딩 포인터

1984년 로드니 A. 브룩스가 제안한 천재적인 아이디어로 포워딩 포인터를 붙이는 방안이에요

기본 개념

일반 객체:     [헤더][필드1][필드2][필드3]
Shenandoah:   [포워딩포인터][헤더][필드1][필드2][필드3]

동작 방식

1단계: 평상시

포워딩 포인터 → 자기 자신
[📍] → [객체헤더][데이터]

2단계: 이주 중

옛 객체: [📍] → 새 객체: [헤더][데이터]
         [헤더][데이터]     (새 위치)
         (옛 위치)

사용자가 옛 객체에 접근해도 자동으로 새 객체로 포워딩돼요!

동시성 문제와 해결책

위험한 시나리오

1. GC 스레드: 객체 복사 시작
2. 사용자 스레드: 옛 객체 필드 수정 ← 🚨 문제!
3. GC 스레드: 포워딩 포인터 업데이트

해결책: CAS(Compare-And-Swap) 기법

  • 포워딩 포인터 접근 시 동기화
  • 한 번에 하나의 스레드만 접근 허용

포워딩 포인터의 비용

장점:

  • ✅ 동시 이주 가능
  • ✅ 포인터 하나만 수정하면 참조 자동 포워딩

단점:

  • ❌ 모든 객체 접근에 추가 우회 비용
  • ❌ 읽기/쓰기 장벽 필요 (특히 읽기 장벽이 비쌈)
  • ❌ 메모리 오버헤드 (초기에는 5~10% 추가 사용)

계속되는 혁신과 개선

 

JDK 13: 로드 참조 장벽

이전 문제:

int value = obj.intField;// 읽기 장벽 발동 ❌ (불필요)
Object ref = obj.refField;// 읽기 장벽 발동 ✅ (필요)

개선 후:

  • 🎯 참조 타입에만 장벽 적용
  • 📈 원시 타입 작업에서 오버헤드 대폭 감소

JDK 13: 포워딩 포인터 객체 헤더 통합

JDK 12까지:

[포워딩포인터][마크워드][클래스워드][배열길이]

JDK 13부터:

[마크워드/포워딩포인터][클래스워드][배열길이]

마크워드의 락 플래그 활용:

  • 0b11: 미사용 상태 → 포워딩 포인터로 활용
  • 💾 메모리 5~10% 절약
  • ⚡ 캐시 적중률 향상
  • 📈 벤치마크에서 10~15% 성능 향상

JDK 17: 스택 워터마크

기존 문제:

GC 시작 → 모든 스레드 정지 → 스택 스캔 → 재개
                ↑ 수십 ms 소요 (병목!)

스택 워터마크 해법:

스택 구조:
┌─────────────────┐ ← 최상위 프레임(변화 있음)
├─ 워터마크 ─────┤
├─────────────────┤ ← 고정 프레임들(변화 없음)
├─────────────────┤(GC가 동시 스캔 가능!)
└─────────────────┘

결과:

  • 📊 JDK 11: 421μs → JDK 17: 63μs (최초 표시)
  • 📊 JDK 11: 1294μs → JDK 17: 328μs (최종 표시)

📊 실전 성능: 현실은?

🧪 위키백과 200GB 인덱싱 테스트

2015년 초기 버전:

컬렉터 실행시간 총정지시간 최장정지시간 평균정지시간
Shenandoah 387.6s 320ms 89.79ms 53.01ms
G1 312.0s 11.75s 1.24s 450.12ms
CMS 285.2s 12.78s 4.39s 852.26ms

분석:

  • 정지 시간: 극적으로 단축! (목표 달성)
  • 처리량: 가장 느림 (트레이드오프)
  • 🎯 10ms 목표: 아직 미달 (89.79ms)

📈 최신 성능 (OpenJDK 17)

평균 정지 시간:

  • 최초 표시: 63μs (0.063ms!)
  • 최종 표시: 328μs (0.328ms!)

드디어 1ms 미만 달성! 🎉

🔧 실전 사용 가이드

설정 예시

# 기본 Shenandoah 활성화
-XX:+UseShenandoahGC
-Xmx32g

# 휴리스틱 조정
-XX:ShenandoahGCHeuristics=adaptive# 적응형 (기본)
-XX:ShenandoahGCHeuristics=static# 정적
-XX:ShenandoahGCHeuristics=compact# 컴팩트 우선

# 모니터링
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps

주의사항

  1. 🚫 오라클 JDK에서는 사용 불가
  2. 💾 메모리 오버헤드 (5~10% 추가 사용)
  3. ⚡ 읽기/쓰기 장벽으로 인한 처리량 저하
  4. 🧪 상대적으로 새로운 기술 (안정성 고려 필요)

🎯 결론: Shenandoah의 미래

💪 Shenandoah의 전략: "짧은 보폭으로 빠르게"

레드햇은 처음부터 완벽을 추구하지 않았어요. 대신:

  • 🎯 작은 목표들로 분할
  • 📈 점진적 개선
  • 🔄 꾸준한 업데이트

🌟 인상적인 점들

  1. 🏢 기업 정치를 이겨낸 기술력
    • 오라클의 견제에도 불구하고 꾸준한 발전
    • 오픈소스 진영의 승리
  2. 🧠 혁신적인 아이디어들
    • 포워딩 포인터 활용한 동시 이주
    • 스택 워터마크 기법
    • 연결 행렬로 메모리 효율성 개선
  3. 📊 실용적인 성과
    • 1ms 미만 정지 시간 달성
    • JDK 8까지 백포팅으로 광범위한 적용 가능

🤔 선택 가이드

Shenandoah를 고려해보세요:

  • OpenJDK 사용 환경
    • Red Hat OpenJDK
    • Amazon Corretto
    • 기타 OpenJDK 배포판
  • 극저지연이 핵심 요구사항
    • 온라인 게임
    • 실시간 거래 시스템
    • 스트리밍 플랫폼
  • 처리량보다 응답성이 중요
  • 새로운 기술 도입에 적극적

다른 컬렉터가 나을 수도:

  • Oracle JDK 필수 사용
  • 안정성이 최우선
  • 처리량 중심 워크로드
  • 메모리 사용량에 민감

🚀 ZGC 완전정복: 오라클의 혁신적인 저지연 컬렉터 이야기

안녕하세요! 이번에는 정말 특별한 가비지 컬렉터를 소개해드릴 시간이에요. 바로 ZGC (Z Garbage Collector)!

"Z가 뭐지?" 하시는 분들도 계시겠지만, 이 친구는 정말 충격적인 성능을 보여주는 컬렉터거든요.

🤔 ZGC vs Shenandoah: 같은 목표, 다른 길

📊 공통 목표

둘 다 10ms 이하의 정지 시간을 목표로 하는 저지연 컬렉터예요. 하지만 구현 방식은 완전히 달라요!

Shenandoah: G1의 후계자 스타일
ZGC:        Azul의 PGC/C4 컬렉터 계보

🏢 개발 배경의 차이

Shenandoah:

  • 🏢 Red Hat 주도 (외부 개발)
  • 🚫 Oracle의 견제로 Oracle JDK에서 제외
  • 📖 오픈소스 진영의 역작

ZGC:

  • 🏛️ Oracle 직접 개발 (내부 프로젝트)
  • ✅ JDK 11부터 실험버전, JDK 15부터 정식 지원
  • 💰 원래 상용화 계획이었지만 무료로 공개

언젠간 고치겠지... 하면서 정치적 이슈로 갈라선 두 컬렉터가 서로 다른 길을 걸어온 거죠.

🏗️ ZGC의 혁신적인 구조

🧩 동적 리전 관리

기존 컬렉터들과 달리 ZGC는 동적 크기 리전을 사용해요:

G1/Shenandoah: 고정 크기 리전 (예: 16MB)
ZGC:           3가지 동적 크기 리전
               - 소리전: 2MB (256KB 미만 객체)
               - 중리전: 32MB (256KB~4MB 객체)
               - 대리전: 가변 (4MB 이상 객체, 객체당 전용 리전)

대리전의 특별한 점:

  • 🎯 하나의 큰 객체 = 하나의 전용 리전
  • 🚫 재할당하지 않음 (복사 비용이 너무 크기 때문)
  • 📏 최소 4MB, 2MB의 배수 크기

🎨 컬러 포인터: ZGC의 핵심 기술

💡 혁신적인 아이디어

기존 방식:

객체 정보를 어디에 저장할까?
→ 객체 헤더에 추가 필드 생성

ZGC 방식:

포인터 자체에 정보를 저장하자!
→ 컬러 포인터(Colored Pointer) 탄생

🔍 64비트 포인터의 활용

현실적 제약 활용:

이론상 64비트 = 16EB 메모리 (말도 안 되게 큼!)
실제 Linux = 46비트 = 64TB (충분히 큼)
ZGC 제한   = 44비트 = 16TB (여전히 충분)

남는 4비트를 플래그로 활용! 💡

컬러 포인터 구조:

┌─────────────┬──┬──┬──┬──┬─────────────────────────────────┐
│ 사용안함(16) │M1│M0│R │F │    객체주소(44비트)              │
└─────────────┴──┴──┴──┴──┴─────────────────────────────────┘
                 │  │  │  └─ Finalizable
                 │  │  └──── Remapped
                 │  └─────── Marked0
                 └────────── Marked1

🌟 컬러 포인터의 3대 장점

1️⃣ 즉시 리전 재활용

Shenandoah: 객체이주 → 참조갱신 완료 대기 → 리전 재활용
ZGC:        객체이주 → 즉시 리전 재활용! ✨

빈 리전 하나만 있으면 가비지 컬렉션 완료 가능!

2️⃣ 메모리 장벽 최소화

  • 🚫 쓰기 장벽 완전 제거 (세대 구분 없어서 가능)
  • 📖 읽기 장벽만 사용 (훨씬 효율적)
  • ⚡ 런타임 성능 향상

3️⃣ 확장 가능성

  • 📈 상위 16비트 활용 가능 (Linux 미사용 영역)
  • 🔮 미래에 힙 제한을 64TB로 확장 가능
  • 🧠 추적 정보 등 추가 기능 저장 공간 확보

⚙️ 메모리 다중 매핑의 마법

🤔 문제: 하드웨어는 컬러 포인터를 모른다

프로세서 생각: "0x0000100012345678? 이건 그냥 메모리 주소네!"
ZGC 생각:     "이건 Remapped 플래그가 설정된 포인터야!"

💡 해결책: 다중 매핑 기술

핵심 아이디어:

서로 다른 가상 주소 → 같은 물리 메모리 매핑

0x0000000012345678 ─┐
0x0000040012345678 ─┼─→ 같은 물리 메모리
0x0000080012345678 ─┘
0x0000100012345678 ─┘

비유해드릴게요:

"A아파트 102동 1004호"라는 같은 주소가

서울, 부산, 인천에 따라 다른 물리적 위치를 가질 수 있죠?

ZGC도 비슷하게 여러 가상 주소가 같은 물리 메모리를 가리켜요!

🔄 ZGC의 4단계 동작 과정

1단계: 동시 표시 (Concurrent Marking)

🎯 목표: 살아있는 객체들 찾기
🟦 실행: 사용자 스레드와 동시 진행
📊 작업: Marked0/Marked1 플래그 갱신

2단계: 동시 재배치 준비 (Concurrent Relocation Prepare)

🎯 목표: 회수할 리전들을 재배치 집합으로 선정
🟦 실행: 사용자 스레드와 동시 진행
🔍 특징: G1과 달리 모든 리전 스캔!

G1 vs ZGC 비교

G1: 기억 집합 관리 비용 선택 → 점진적 스캔
ZGC: 광범위 스캔 비용 선택 → 기억 집합 불필요

3단계: 동시 재배치 (Concurrent Relocation) ⭐

🎯 목표: 객체들을 새 리전으로 복사
🟦 실행: 사용자 스레드와 동시 진행
✨ 핵심: 포워드 테이블 + 자가 치유

자가 치유(Self-Healing)의 마법:

사용자가 옛 객체 접근 시:

  1. 읽기 장벽 감지
  2. 포워드 테이블 확인
  3. 새 객체로 자동 포워딩
  4. 참조 자체를 새 주소로 갱신 (한 번만!)

Shenandoah: 접근할 때마다 포워딩 오버헤드
ZGC: 첫 접근 시에만 오버헤드! 🎉

4단계: 동시 재매핑 (Concurrent Remapping)

🎯 목표: 힙 전체의 낡은 참조들 정리
🟦 실행: 사용자 스레드와 동시 진행
🧠 영리함: 다음 표시 단계와 통합! (객체 그래프 탐색 1회 절약)

💪 ZGC vs 다른 컬렉터들

절충점 비교

컬렉터 장점 단점
G1 기억 집합으로 점진적 회수 메모리 오버헤드 + 쓰기 장벽
Shenandoah 동시 이주 지원 읽기/쓰기 장벽 모두 사용
ZGC 쓰기 장벽 완전 제거 객체 할당 속도 제한

⚠️ ZGC의 약점: 할당 속도 제한

문제 시나리오:

  1. ZGC가 거대한 힙에서 10분간 동시 회수 시작
  2. 애플리케이션이 매우 빠르게 새 객체 생성
  3. 회수 속도 < 할당 속도
  4. 부유 쓰레기 대량 발생
  5. 힙 공간 고갈 위험! 😱

현재 해결책:

  • 🏗️ 힙 크기를 최대한 크게 설정
  • 🔮 세대 구분 ZGC 도입 (JDK 21+)

🌐 NUMA 메모리 최적화

NUMA란?

  • 🖥️ 멀티코어 프로세서에서 각 코어가 메모리 직접 관리
  • 📍 로컬 메모리 접근 = 빠름
  • 🌐 원격 메모리 접근 = 느림

ZGC의 영리함:

객체 생성 요청한 스레드의 프로세서 → 해당 프로세서의 로컬 메모리에 우선 할당!
→ 메모리 접근 효율 극대화 ⚡

🔧 ZGC 설정 가이드

기본 설정

# JDK 15+ (정식 지원)-XX:+UseZGC
-Xmx32g

# JDK 11~14 (실험 버전)-XX:+UnlockExperimentalVMOptions
-XX:+UseZGC

고급 설정

# 소프트 힙 제한 (추천)-XX:SoftMaxHeapSize=30g

# NUMA 최적화-XX:+UseTransparentHugePages

# 모니터링-XX:+PrintGC
-XX:+PrintGCDetails

주의사항

# 이런 옵션들과 호환 안됨
-XX:+UseCompressedOops# 압축 포인터 불가
-XX:+Use32BitClassPointers# 32비트 클래스 포인터 불가

ZGC 선택 가이드

ZGC가 최적인 경우

  1. 🎯 극저지연이 핵심 요구사항
    • 실시간 거래 시스템
    • 온라인 게임 서버
    • 실시간 스트리밍
  2. 📊 대용량 힙 사용
    • 8GB+ 메모리 환경
    • 빅데이터 처리
    • 대규모 인메모리 캐시
  3. 64비트 플랫폼
    • Linux, macOS, Windows (64비트만)
    • 16TB 이하 힙 크기 (충분히 큼!)

다른 컬렉터 고려할 경우

  1. 📱 32비트 환경 → 불가능
  2. 🔧 압축 포인터 필수 → G1 추천
  3. 💾 메모리 극도로 제한적 → G1/Parallel 고려
  4. 🎯 처리량 최우선 → Parallel 여전히 유효

🔥 세대 구분 ZGC: 차세대 가비지 컬렉터의 완성형

이번에는 ZGC의 진화된 형태인 세대 구분 ZGC에 대해 이야기해볼게요.

"ZGC도 충분히 좋은데 뭐가 더 필요해?" 하실 수도 있지만, 이 친구는 정말 게임 체인저예요!

🤔 왜 세대 구분이 필요했을까?

🎯 기본 ZGC의 한계

문제 상황:

모든 객체를 함께 보관 → 매번 전체 힙 스캔 필요
- 젊은 객체 (금방 죽을 예정) 💀
- 늙은 객체 (오래 살 예정) 👴
→ 비효율적인 작업!

약한 세대 가설의 활용:

젊은 객체는 일찍 죽는다!
→ 젊은 객체만 더 자주 회수하면
→ 적은 노력으로 많은 메모리 확보 가능! 💡

🚀 JDK 21의 세대 구분 ZGC

🔧 활성화 방법

# 기본 ZGC (여전히 세대 구분 없음)
java -XX:+UseZGC MyApp

# 세대 구분 ZGC 활성화
java -XX:+UseZGC -XX:+ZGenerational MyApp

미래 계획:

현재: -XX:+UseZGC = 기본 ZGC
미래: -XX:+UseZGC = 세대 구분 ZGC (기본)
      -XX:-ZGenerational = 세대 구분 비활성화
최종: 세대 구분 ZGC만 남길 예정

🎨 향상된 컬러 포인터

기존 ZGC:

  • 📖 읽기 장벽만 사용
  • 🚫 쓰기 장벽 없음

세대 구분 ZGC:

읽기 장벽 + 쓰기 장벽 조합
- 컬러 포인터에 새로운 메타데이터 추가
- 쓰기 장벽이 세대 간 참조 효율적 추적
- 읽기 장벽 작업을 쓰기 장벽으로 일부 이전
  (읽기 >> 쓰기 빈도이므로 전체 작업량 감소!)

🛠️ 핵심 개선 기술들

1️⃣ 다중 매핑 메모리 제거

기존 문제:

ZGC는 같은 힙을 3개 가상 주소로 매핑
→ ps 명령어로 보면 실제의 3배 메모리 사용으로 표시 😅
→ 혼란 야기

세대 구분 ZGC 해결:

  • ✅ 읽기/쓰기 장벽 명확히 구분 → 다중 매핑 불필요
  • 📊 사용자 관점에서 정확한 메모리 사용량 측정 가능
  • 🎯 확보된 메타데이터 비트로 힙 크기 16TB+ 확장 가능

2️⃣ 이중 버퍼 기억 집합 관리

기존 카드 테이블 방식:

카드 테이블: 1바이트 = 힙의 512바이트 대응
문제: 더럽혀진 512바이트 전체를 스캔해야 함 (비효율)

세대 구분 ZGC 비트맵 방식:

비트맵: 1비트 = 객체 필드 주소 1개 (정확!)
이중 버퍼: [애플리케이션용] ↔ [GC 전용] 원자적 교환
→ 서로 간섭 없이 동시 작업 가능! 🎯

3️⃣ 밀집도 기반 리전 처리

영리한 전략:

최근 할당 리전 → 살아있는 객체 많음 (밀집도 높음)
오래된 리전 → 죽은 객체 많음 (밀집도 낮음)

처리 방식:
1. 밀집도 낮은 리전 우선 회수 📊
2. 밀집도 높은 리전은 자연스럽게 노화 👴
3. 노화된 리전은 다음 GC에서 밀집도 낮아짐
4. 효율적인 순환 구조 완성!

4️⃣ 거대 객체의 스마트 처리

기존 문제:

거대 객체를 구세대로 이동? → 복사 비용 엄청남 💸

세대 구분 ZGC 해결:

1. 거대 객체를 신세대에 할당
2. 금방 죽으면 → 신세대 GC에서 빠르게 회수 ⚡
3. 오래 살면 → 리전 자체가 구세대로 승격 (복사 없음!)

📊 압도적인 성능 향상

🚀 Apache Cassandra 벤치마크

기본 ZGC 대비 세대 구분 ZGC:
처리량 4배 향상! 🎉

"NoSQL 데이터베이스에서 이 정도 차이라니!"

💾 메모리 효율성 개선

  • 🔄 다중 매핑 제거로 메모리 오버헤드 감소
  • 📈 더 정확한 메모리 사용량 측정
  • 🎯 전체 자원 사용량 최적화

⚡ 다양한 장벽 최적화

컴파일된 애플리케이션 코드에 더 많은 GC 코드 통합
→ 기억 집합 장벽, 시작 단계 스냅숏 표시 장벽,
  쓰기 장벽 버퍼, 장벽 패치 등 수많은 최적화 기법 적용
→ 처리량 극대화!

🔄 동작 방식의 진화

📋 기본 ZGC vs 세대 구분 ZGC

기본 ZGC:

1. 동시 표시 (전체 힙)
2. 동시 재배치 준비 (전체 힙)
3. 동시 재배치 (선택된 리전)
4. 동시 재매핑 (전체 힙)

세대 구분 ZGC:

신세대 GC (자주):
1. 신세대 표시
2. 밀집도 분석 → 회수 대상 선정
3. 동시 재배치 (효율성 극대화)
4. 생존 객체 → 생존자/구세대 승격

구세대 GC (가끔):
1. 전체 표시 (필요시에만)
2. 구세대 회수

🎯 실전 사용 가이드

설정 예시

# 세대 구분 ZGC 기본 설정-XX:+UseZGC
-XX:+ZGenerational
-Xmx32g

# 고급 설정-XX:+UseZGC
-XX:+ZGenerational
-XX:SoftMaxHeapSize=30g
-XX:+UseLargePages

# 모니터링 강화-XX:+PrintGC
-XX:+PrintGCDetails
-Xlog:gc*:gc.log

✅ 세대 구분 ZGC가 최적인 경우

  1. 📊 객체 생성이 매우 빈번한 애플리케이션
    • 웹 서버 (요청당 많은 임시 객체)
    • 스트리밍 처리 (지속적인 데이터 흐름)
    • 실시간 분석 시스템
  2. 🎯 기본 ZGC에서 할당 속도 제한 경험한 경우
    • 회수 속도 < 할당 속도 상황
    • 힙 크기를 계속 늘려야 했던 경우
  3. 💾 메모리 사용량 최적화가 중요한 환경
    • 컨테이너 환경
    • 메모리 제한이 엄격한 서버

🤔 아직 기본 ZGC 고려할 경우

  1. 🧪 검증된 안정성 우선
    • 미션 크리티컬한 서비스
    • 새로운 기능 도입에 보수적인 환경
  2. 📈 객체 수명이 비슷한 워크로드
    • 배치 처리 시스템
    • 장시간 실행되는 계산 작업

3.7 적합한 가비지 컬렉터 선택하기

지금까지 다양한 가비지 컬렉터들을 살펴봤는데, 이제 가장 중요한 질문이 남았어요.

"그래서 결국 뭘 써야 하는데?"

이번에는 실전에서 어떤 컬렉터를 선택해야 하는지, 그리고 정말 특별한 컬렉터 하나까지 소개해드릴게요!

🎯 엡실론 컬렉터: 아무것도 안 하는 컬렉터?

😲 충격적인 컨셉

JDK 11에 엡실론(Epsilon) 컬렉터라는 정말 특별한 친구가 추가되었어요.

엡실론 컬렉터의 특징:

  • 가비지 컬렉션을 전혀 하지 않음!
  • "No-Op Garbage Collector"
  • 일명 "일하지 않는" 컬렉터

"말도 안 된다고?" 하시는 분들, 잠깐만요! 이 친구도 나름의 이유가 있어요.

🛠️ 엡실론의 진짜 역할

가비지 컬렉터 ≠ 단순한 쓰레기 수거꾼

실제로는 "자동 메모리 관리 서브시스템"이에요:

  • 힙 관리 및 레이아웃
  • 객체 할당 처리
  • 인터프리터/컴파일러 연동
  • 모니터링 시스템 연동

엡실론도 최소한 힙 관리와 객체 할당은 해야 JVM이 돌아가거든요.

🎪 엡실론의 실제 용도

1️⃣ 성능 테스트 및 벤치마킹

"GC의 영향을 완전히 배제하고 싶어!"
→ 엡실론으로 순수 애플리케이션 성능 측정

2️⃣ 통합 인터페이스 검증

JDK 10부터 가비지 컬렉터 통합 인터페이스 도입
→ 엡실론이 이 인터페이스의 참조 구현 역할

3️⃣ 마이크로서비스/서버리스 환경

전통 자바의 약점:

  • 메모리 많이 사용
  • 구동 시간 오래 걸림
  • JIT 최적화 지연

단기 실행 서비스:

  • 몇 초~몇 분만 실행
  • 힙이 가득 차기 전에 종료
  • GC 부하 제거로 빠른 실행!

📊 가비지 컬렉터 선택 매트릭스

🎯 워크로드별 최적 선택

워크로드 유형 추천 컬렉터 이유

마이크로서비스 Epsilon, G1 짧은 실행시간, 빠른 시작

웹 애플리케이션 G1, ZGC 응답시간 중요, 중간 크기 힙
실시간 거래 ZGC, Shenandoah 극저지연 필요
빅데이터 처리 Parallel, ZGC 처리량 또는 대용량 힙
배치 작업 Parallel 처리량 최우선
게임 서버 ZGC, G1 일정한 응답시간

💾 힙 크기별 가이드

< 1GB:     G1, Parallel
1GB~8GB:   G1 (안정적 선택)
8GB~32GB:  G1, ZGC (성능 테스트 후 결정)
32GB+:     ZGC, Shenandoah (저지연 우선)

⏱️ 지연시간 요구사항별

수 초 OK:       Parallel (처리량 최우선)
수백 ms OK:     G1 (균형잡힌 선택)
수십 ms 필요:   G1 튜닝, ZGC 고려
10ms 이하:      ZGC, Shenandoah
1ms 이하:       ZGC (세대 구분), Shenandoah

🔍 실전 선택 프로세스

1단계: 기본 조건 확인

# 플랫폼 체크
- 32비트? → Serial, Parallel만 가능
- 64비트? → 모든 컬렉터 선택 가능

# JDK 버전 체크
- JDK 8: G1, CMS, Parallel
- JDK 11+: G1, ZGC, Shenandoah, Epsilon
- JDK 21+: 세대 구분 ZGC 추가

2단계: 비즈니스 요구사항 분석

핵심 질문들:

  • 최대 힙 크기는?
  • 허용 가능한 최대 정지시간은?
  • 처리량과 지연시간 중 뭐가 더 중요한가?
  • 메모리 사용량에 제약이 있나?

3단계: 초기 선택

보수적 선택: G1 (대부분 상황에서 무난)
공격적 선택: ZGC (최신 기술, 최고 성능)
특수 목적: Epsilon (단기 실행), Parallel (배치)

4단계: 성능 테스트

# 기본 설정으로 테스트-XX:+UseG1GC
-XX:+UseZGC -XX:+ZGenerational
-XX:+UseParallelGC

# 지표 측정- 처리량 (TPS, QPS)
- 평균/p99 응답시간
- GC 빈도 및 정지시간
- 메모리 사용량

⚙️ 컬렉터별 기본 설정 가이드

G1GC (안정적 선택)

# 기본 설정
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m

# 튜닝 옵션
-XX:G1NewSizePercent=20
-XX:G1MaxNewSizePercent=40

ZGC (고성능 선택)

# 세대 구분 ZGC (JDK 21+)-XX:+UseZGC
-XX:+ZGenerational
-XX:SoftMaxHeapSize=30g

# 기본 ZGC-XX:+UseZGC
-Xmx32g

Parallel GC (처리량 우선)

# 기본 설정
-XX:+UseParallelGC
-XX:ParallelGCThreads=8

# 튜닝
-XX:MaxGCPauseMillis=500
-XX:GCTimeRatio=99

Epsilon (특수 용도)

# 활성화-XX:+UnlockExperimentalVMOptions
-XX:+UseEpsilonGC

# 메모리 모니터링 필수-Xmx1g  # 충분한 힙 크기 설정

📈 모니터링과 튜닝

핵심 지표

# GC 로깅 활성화-Xlog:gc*:gc.log:time

# 모니터링 도구- JVisualVM
- GCEasy.io (로그 분석)
- Prometheus + Grafana
- APM 도구 (New Relic, DataDog 등)

문제 해결 체크리스트

1. GC 빈도가 너무 높다면?

  • 힙 크기 증가 고려
  • 객체 풀링 도입
  • 메모리 누수 확인

2. 정지 시간이 너무 길다면?

  • 저지연 컬렉터로 변경 (ZGC, Shenandoah)
  • 힙 크기 조정
  • GC 파라미터 튜닝

3. 처리량이 부족하다면?

  • Parallel GC 고려
  • JIT 컴파일 최적화
  • 하드웨어 리소스 확인

🎯 추천 선택 전략

🥇 2024년 추천 순위

1순위: G1GC

✅ 검증된 안정성
✅ 대부분 상황에서 우수한 성능
✅ 풍부한 튜닝 옵션
✅ 광범위한 호환성

2순위: ZGC (세대 구분)

✅ 최고 수준의 지연시간
✅ 뛰어난 처리량
✅ 대용량 힙 지원
⚠️ 상대적으로 새로운 기술

3순위: Parallel GC

✅ 최고의 처리량
✅ 간단한 설정
❌ 높은 정지시간

🚀 미래 지향적 선택

현재 검증된 선택: G1GC
미래를 준비한 선택: ZGC (세대 구분)
특수 목적: Epsilon, Shenandoah
레거시 지원: Parallel GC

가비지 컬렉터 선택에 정답은 없어요. 각자의 상황과 요구사항에 따라 최적해가 달라지거든요.

핵심 원칙:

  • 🧪 실제 워크로드로 테스트하기
  • 📊 지표 기반으로 판단하기
  • 🔄 지속적으로 모니터링하기
  • ⚖️ 트레이드오프 이해하기

엡실론 컬렉터처럼, 때로는 "아무것도 안 하는" 것이 정답일 수도 있어요. 마이크로서비스 시대에는 더욱 그렇죠.

컬렉터 선택 기준

3가지 주요 요인으로 결정:

  • 애플리케이션 목적: 데이터 분석(처리량 우선) vs SLA 서비스(지연시간 우선) vs 클라이언트 앱(메모리 사용량)
  • 하드웨어 환경: 아키텍처, 프로세서 수, 메모리 용량, OS
  • JDK 제공자: Oracle, OpenJDK, Zulu 등

실전 선택 가이드

브라우저-서버 시스템 예시:

  • 자금 여유 + 경험 부족 → Azul C4 (상용)
  • 제어 능력 있음 → ZGC 시도
  • 레거시 시스템 → 4-6GB 이하면 CMS, 그 이상이면 G1

Oracle 공식 가이드:

  • ~100MB 데이터 → Serial
  • 단일 프로세서 → Serial
  • 최대 성능 우선 → Parallel
  • 응답시간 중요 → G1
  • 매우 빠른 응답 필요 → ZGC

JDK 9 통합 로깅

  • Xlog 매개변수 구조:
-Xlog:[selector]:[output]:[decorators]:[output-options]

주요 GC 로그 옵션:

  • 기본: -Xlog:gc (구 -XX:+PrintGC)
  • 상세: -Xlog:gc* (구 -XX:+PrintGCDetails)
  • 힙 정보: -Xlog:gc+heap=debug
  • 정지시간: -Xlog:safepoint

로그 레벨: Trace > Debug > Info(기본) > Warning > Error > Off


GC와 관련하여 간략하게 표로 정리한 것들입니다. 아래 표에서 언급되는 가비지 컬렉터는 핫스팟(HotSpot) JVM에서 사용되는 것들입니다.

JDK 버전JVM기본 가비지 컬렉터주요 다른 가비지 컬렉터 / 상태

JDK 1.3 이하 HotSpot JVM 시리얼 컬렉터 (Serial Collector) JDK 1.3.1까지 유일한 선택

JDK 1.4 시리얼 컬렉터 (Serial Collector) 패러럴 스캐빈지 (Parallel Scavenge), 패러럴 올드 (Parallel Old) - 사용 가능
JDK 5 패러럴 컬렉터 (Parallel Collector) 시리얼 컬렉터 (Serial Collector), CMS 컬렉터 (CMS Collector) - 사용 가능
JDK 6 패러럴 컬렉터 (Parallel Collector) CMS 컬렉터 (CMS Collector) - 최적화 및 지원 강화. 시리얼 컬렉터 (Serial Collector) - 사용 가능.
JDK 7 패러럴 컬렉터 (Parallel Collector) G1 컬렉터 (G1 Collector) - 초기 접근(early access) 버전으로 도입. CMS 컬렉터 (CMS Collector), 시리얼 컬렉터 (Serial Collector) - 사용 가능.
JDK 8 패러럴 컬렉터 (Parallel Collector) G1 컬렉터 (G1 Collector) - 중요한 역할. CMS 컬렉터 (CMS Collector), 시리얼 컬렉터 (Serial Collector) - 사용 가능.
JDK 9 G1 컬렉터 (G1 Collector) CMS 컬렉터 (CMS Collector) - Deprecated(더 이상 사용되지 않음). 시리얼 컬렉터 (Serial Collector), 패러럴 컬렉터 (Parallel Collector) - 사용 가능.
JDK 11 G1 컬렉터 (G1 Collector) ZGC, 쉐년도어 (Shenandoah) - 실험적(experimental)으로 도입. 시리얼 컬렉터 (Serial Collector), 패러럴 컬렉터 (Parallel Collector) - 사용 가능.
JDK 12 G1 컬렉터 (G1 컬렉터) 쉐년도어 (Shenandoah) - 도입. ZGC - 실험적. 시리얼 컬렉터 (Serial Collector), 패러럴 컬렉터 (Parallel Collector) - 사용 가능.
JDK 14 G1 컬렉터 (G1 컬렉터) 패러럴 올드 GC (Parallel Old GC) - Deprecated(더 이상 사용되지 않음). ZGC, 쉐넌도어 - 사용 가능.
JDK 15 G1 컬렉터 (G1 컬렉터) ZGC, 쉐년도어 (Shenandoah) - 프로덕션 레디(production-ready)로 전환.
JDK 17 G1 컬렉터 (G1 컬렉터) ZGC, 쉐넌도어 (Shenandoah) - 상당한 성능 향상.
JDK 21 G1 컬렉터 (G1 컬렉터) 세대 구분 ZGC (Generational ZGC) - 도입. ZGC, 쉐넌도어 - 사용 가능.
  • 기본 가비지 컬렉터:
    • JDK 5부터 JDK 8까지는 패러럴 컬렉터가 기본으로 제공되었습니다.
    • JDK 9부터는 G1 컬렉터가 기본 가비지 컬렉터로 지정되었으며, 현재(JDK 21까지)까지 유지되고 있습니다.
728x90
반응형