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

주니어를 위한 가비지 컬렉터와 메모리 할당 전략2 - 클래식 가비지 컬렉터

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

3.5 클래식 가비지 컬렉터

왜 가비지 컬렉터가 이렇게 많을까?

JDK는 다양한 '클래식' 가비지 컬렉터들을 제공해 왔어요. 각각은 주로 처리량(throughput) 또는 지연 시간(latency) 중 하나를 최적화하는 데 집중합니다.

쉽게 말해서:

  • 처리량 중심: "빨리빨리 많은 일을 처리하자!"
  • 지연 시간 중심: "애플리케이션이 멈추는 시간을 최대한 줄이자!"

 

3.5.1 시리얼 컬렉터 (Serial Collector)

"할아버지 GC라고 불러주세요" 👴

  • JDK 1.3.1까지 유일한 GC였고, Hotspot 클라이언트 JVM의 기본이었어요
  • 특징: 단일 스레드로 모든 GC 작업을 혼자 다 해요
  • 단점: GC가 돌 때 모든 사용자 스레드가 멈춰요 → 가장 긴 STW 시간 😱
  • 언제 쓸까: 메모리 100MB 미만인 소규모 앱이나 단일 CPU 환경
  • 활성화: -XX:+UseSerialGC

 

시리얼 컬렉터는 가장 기초적이고 오래된 컬렉터예요. JDK 1.3.1 전까지는 핫스팟 가상 머신에서 유일한 선택지였다고 하네요!

이름만 봐도 알 수 있듯이 '단일 스레드'로 동작합니다. 근데 여기서 중요한 건, 단순히 "GC 스레드가 하나다"가 아니라...

가비지 컬렉션이 시작되면 회수가 완료될 때까지 다른 모든 작업 스레드가 멈춰있어야 해요!

😱 스톱 더 월드(Stop The World)의 공포

"스톱 더 월드"라는 말, 멋지게 들리지만 실제로는 무서운 현상이에요...

  • 사용자가 만든 스레드들은 자신이 언제 멈출지 제어할 수도, 알 수도 없어요
  • 상상해보세요... 컴퓨터가 한 시간마다 5분씩 멈춘다면? 😭

💡 핫스팟 개발팀의 명언

초기 핫스팟 가상 머신 설계자들이 이런 말을 했어요:

"어머니가 여러분 방을 청소할 때면 분명 의자에 가만히 있으라고 하거나 밖에 나가 있으라고 할 것이다. '나는 청소를 할 테니 너는 계속 어질러라' 하는 식으로는 청소가 되지 않는다."

합리적인 말이지만... 가비지 컬렉션은 방 청소보다 훨씬 복잡해요! 🤯

 

시리얼 컬렉터 동작 방식

시리얼과 시리얼 올드 컬렉터는 이렇게 동작해요:

CPU0: 사용자 스레드 → [멈춤] → GC 작업 → [재시작]
CPU1: 사용자 스레드 → [멈춤] → 대기중... → [재시작]  
CPU2: 사용자 스레드 → [멈춤] → 대기중... → [재시작]
CPU3: 사용자 스레드 → [멈춤] → 대기중... → [재시작]
  • 신세대: 마크-카피 알고리즘 사용
  • 구세대: 마크-컴팩트 알고리즘 사용
  • 결과: 모든 사용자 스레드 일시 정지 

 

그래도 시리얼 컬렉터가 살아있는 이유

"어? 그럼 시리얼 컬렉터는 구시대 유물 아닌가요?"

아니에요! JDK 1.3부터 지금까지 개발팀은 끊임없이 노력했어요:

📈 진화의 역사

  • 시리얼 컬렉터 → 패러렐 컬렉터 → CMS & G1 → 셰넌도어 & ZGC

하지만 아직도 완벽히 STW를 없앨 방법은 없어요. 더 나은 방법을 찾는 여정은 계속되고 있습니다!

시리얼 컬렉터의 숨겨진 장점들

  1. 단순함과 효율성
    • 다른 컬렉터의 단일 스레드 알고리즘보다 간단하고 효율적
    • 알고리즘 자체가 요구하는 메모리 사용량이 가장 적어요
  2. 특정 환경에서는 최고의 선택
    • 단일 코어 프로세서나 코어 수가 적은 환경
    • 스레드 상호 작용에 의한 오버헤드가 없어요
    • 온전히 가비지 컬렉션에만 집중 → 회수 효율 최대
  3. 메모리 제약 환경
    • 가용 메모리가 적은 환경에서 빛을 발해요
    • 임베디드 시스템이나 소형 애플리케이션에 적합

 

시리얼 컬렉터 사용 체크리스트

언제 시리얼 컬렉터를 써야 할까요?

 이런 경우라면 시리얼 GC를 고려해보세요:

  • 힙 메모리가 100MB 미만인 소규모 애플리케이션
  • 단일 코어 또는 코어 수가 매우 적은 환경 (2코어 이하)
  • 임베디드 시스템이나 IoT 디바이스
  • 클라이언트 애플리케이션 (데스크톱 앱 등)
  • 메모리 사용량을 최소화해야 하는 환경

 이런 경우는 피하세요:

  • 멀티코어 서버 환경
  • 높은 처리량이 필요한 서비스
  • 실시간성이 중요한 애플리케이션
  • 사용자 경험이 중요한 대화형 애플리케이션

🎯 활성화 방법

시리얼 컬렉터를 사용하려면:

-XX:+UseSerialGC

참고: JDK 9 이후부터는 기본 GC가 G1이므로, 시리얼 GC를 쓰려면 명시적으로 설정해야 해요!

 

 

3.5.2 파뉴 컬렉터

파뉴 컬렉터는 한마디로 "시리얼 컬렉터의 멀티스레드 버전"이에요.

핵심 차이점

  • 시리얼: 단일 스레드로 GC 작업
  • 파뉴: 여러 스레드로 병렬 GC 작업

그 외에는 완전히 똑같아요!

  • 컬렉터 제어 매개변수 (-XX:SurvivorRatio 등)
  • 컬렉션 알고리즘 (마크-카피)
  • 스톱 더 월드 방식
  • 객체 할당 규칙, 회수 전략

심지어 구현 코드도 상당 부분 공유해요!

파뉴 컬렉터 동작 방식

시리얼과 비교해보면 차이가 확실히 보여요:

시리얼 방식:

CPU0: 사용자 스레드 → [멈춤] → GC 혼자 작업 → [재시작]
CPU1: 사용자 스레드 → [멈춤] → 대기중... → [재시작]  
CPU2: 사용자 스레드 → [멈춤] → 대기중... → [재시작]
CPU3: 사용자 스레드 → [멈춤] → 대기중... → [재시작]

파뉴 방식:

CPU0: 사용자 스레드 → [멈춤] → GC 스레드1 작업 → [재시작]
CPU1: 사용자 스레드 → [멈춤] → GC 스레드2 작업 → [재시작]  
CPU2: 사용자 스레드 → [멈춤] → GC 스레드3 작업 → [재시작]
CPU3: 사용자 스레드 → [멈춤] → GC 스레드4 작업 → [재시작]

결과: 여러 CPU가 동시에 GC 작업을 분담해서 전체 GC 시간 단축! 

 

파뉴의 비극적인(?) 운명

"그럼 파뉴가 시리얼보다 좋은 거네요?"

음... 그게 복잡해요. 파뉴의 인생 스토리를 들어보세요 

 

전성기 시절 (JDK 5~8)

JDK 5에 기념비적인 CMS 컬렉터가 등장했어요!

  • 최초의 동시성 지원 GC (사용자 스레드와 GC 스레드가 동시 실행) 하지만 CMS는 구세대 전용이었고...
  • CMS와 조합할 수 있는 신세대 컬렉터가 파뉴뿐이었어요!
# CMS 활성화하면 자동으로 파뉴 선택됨
-XX:+UseConcMarkSweepGC  # 파뉴 + CMS 조합

이때부터 파뉴는 "CMS의 파트너"로 핫스팟 서버에서 인기만점!

💔 몰락의 시작 (JDK 9~)

하지만 세상이 바뀌었어요...

G1 컬렉터의 등장:

  • 힙 전체를 다루는 통합 컬렉터
  • 신세대용 파트너가 필요 없어짐
  • CMS보다 훨씬 뛰어난 성능

JDK 9의 결정적 타격:

  • 파뉴 + CMS 조합을 공식 권장에서 제외
  • 파뉴 + 시리얼 올드, 시리얼 + CMS 조합도 지원 중단
  • -XX:+UseParNewGC 매개변수 삭제

결과: 파뉴는 CMS에만 의존하게 되었고, CMS가 폐기되면서 함께 역사 속으로... 

💡 역사적 의미: 파뉴는 핫스팟 가상 머신에서 사라진 최초의 가비지 컬렉터예요!

 파뉴의 성능 특성

✅ 장점:

  • 멀티코어에서 시리얼보다 빠름
  • GC 스레드 수 = CPU 코어 수 (기본값)
  • 코어가 많을수록 효율적인 자원 활용

❌ 단점:

  • 단일 코어에서는 시리얼보다 느림 (스레드 오버헤드 때문)
  • 하이퍼스레딩 환경에서도 보장 못함
  • 스레드 간 상호작용 비용 존재

📊 언제 빨라질까?

코어 수가 적을 때: 시리얼 > 파뉴 (오버헤드가 더 큰 손실)
코어 수가 많을 때: 파뉴 > 시리얼 (병렬처리 효과가 더 큰 이득)

💡 팁: 코어가 많은 시스템에서는 -XX:ParallelGCThreads로 GC 스레드 수를 제한할 수 있어요!

 

병렬 vs 동시 - 헷갈리는 용어 정리

파뉴를 계기로 중요한 개념을 짚고 넘어가겠어요!

🔄 병렬 (Parallel)

  • GC 스레드들끼리의 관계
  • 여러 GC 스레드가 동시에 작업
  • 이때 사용자 스레드는 정지 상태 (STW)
  • 예시: 파뉴, 패러렐 GC

🤝 동시 (Concurrent)

  • GC 스레드와 사용자 스레드의 관계
  • GC 스레드와 사용자 스레드가 동시에 실행
  • 사용자 스레드가 멈추지 않음 (애플리케이션 계속 응답)
  • 단, GC가 시스템 자원 일부를 점유해서 처리량은 낮아질 수 있음
  • 예시: CMS, G1

💡 쉬운 기억법:

병렬 = "GC끼리 협력" (사용자는 대기)
동시 = "GC와 사용자가 공존" (모두 활동)

 

📋 파뉴 컬렉터 정리

🎯 파뉴를 써야 했던 경우 (과거형):

✅ CMS와 함께 사용할 때 (JDK 5~8)
✅ 멀티코어 서버 환경
✅ 신세대 처리 성능이 중요한 경우

❌ 파뉴를 쓰면 안 되는 경우:

❌ 단일 코어나 듀얼 코어 환경
❌ JDK 9 이상 (공식 지원 중단)
❌ 현재 새로운 프로젝트 (G1이나 ZGC 사용 권장)

파뉴 컬렉터는 "과도기적 존재"였어요.

시리얼에서 진정한 동시성 컬렉터(CMS)로 가는 징검다리 역할을 했지만, 결국 더 나은 G1에게 자리를 내줬죠.

하지만 의미가 없는 건 아니에요!

  • 멀티스레드 GC의 가능성을 보여줬고
  • CMS 같은 혁신적 컬렉터의 파트너 역할을 했으니까요

 

3.5.3 패러렐 스캐빈지 컬렉터

파뉴 컬렉터에 이어서, 패러렐 스캐빈지 컬렉터(Parallel Scavenge, 줄여서 PS)를 알아보겠어요!

"어? 파뉴도 패러렐인데 또 패러렐이라고?" 🤔

맞아요, 둘 다 멀티스레드로 동작해서 헷갈리기 쉬워요. 하지만 완전히 다른 철학을 가진 컬렉터예요!

 

PS 컬렉터만의 특별함 - "처리량이 곧 정의"

처리량이란 뭘까?

처리량은 이런 공식으로 계산해요:

처리량 = 사용자 코드 실행 시간 / (사용자 코드 실행 시간 + GC 실행 시간)

쉬운 예시:

  • 총 100분 작업 중 GC에 1분 소요 → 처리량 99% 
  • 총 100분 작업 중 GC에 5분 소요 → 처리량 95% 

 

처리량 vs 응답시간 - 어떤 게 중요할까?

 응답시간 중심 (CMS 스타일):

  • 사용자 경험이 최우선
  • "클릭했을 때 빨리 반응해야 해!"
  • 웹 애플리케이션, GUI 프로그램에 적합

 처리량 중심 (PS 스타일):

  • 전체적인 작업 효율성이 최우선
  • "총 작업 시간이 단축되면 OK!"
  • 배치 처리, 데이터 분석, 과학 계산에 적합

예시로 이해하기:

100개 파일을 처리하는 작업이 있다면:

  • 응답시간 중심: 각 파일을 빠르게 처리 (1개씩 빨리빨리)
  • 처리량 중심: 100개 전체를 가장 효율적으로 처리 (전체 시간 최소화)

 

PS 컬렉터의 핵심 매개변수들

PS 컬렉터는 처리량 제어를 위한 강력한 도구들을 제공해요!

1️⃣ -XX:MaxGCPauseMillis - "정지 시간 최대 한계선"

-XX:MaxGCPauseMillis=200  # 최대 200ms까지만 멈춰!

⚠️ 함정 주의!

  • 값을 작게 설정한다고 시스템이 빨라지는 게 아니에요!
  • 정지시간을 줄이면 → 신세대 크기 감소 → 더 자주 GC 발생

실제 예시:

기존: 10초마다 GC, 매번 100ms 정지
설정: MaxGCPauseMillis=70
결과: 5초마다 GC, 매번 70ms 정지
→ 개별 정지시간은 줄었지만 전체 처리량은 감소! 📉

2️⃣ -XX:GCTimeRatio - "처리량 직접 지정"

-XX:GCTimeRatio=99  # 기본값, GC 시간을 전체의 1%로 제한

계산 공식:

  • N 값 설정 → GC 시간이 전체의 1/(1+N) 이하가 되도록 조절
  • 기본값 99 → GC가 전체 시간의 1% 이하
  • 값이 클수록 → 처리량 높음, GC 시간 적음

예시들:

GCTimeRatio=19  → GC 시간 5% 이하 (1/20)
GCTimeRatio=99  → GC 시간 1% 이하 (1/100) ← 기본값
GCTimeRatio=199 → GC 시간 0.5% 이하 (1/200)

 

-XX:+UseAdaptiveSizePolicy 

이 옵션을 켜면 마법이 일어나요

 자동 조절되는 것들:

  • 신세대 크기 (-Xmn)
  • 에덴과 생존자 공간 비율 (-XX:SurvivorRatio)
  • 구세대 승격 임계값 (-XX:PretenureSizeThreshold)
  • 기타 세부 설정들...

🧠 동작 원리:

  1. 가상 머신이 성능 모니터링 정보 수집
  2. 설정한 목표 (MaxGCPauseMillis, GCTimeRatio)에 맞춰
  3. 모든 매개변수를 동적으로 자동 조율 

 

 "GC 인간공학" - 사람을 위한 설계

전통적인 방식:

# 복잡한 수동 설정 😰
-Xmn512m 
-XX:SurvivorRatio=8 
-XX:PretenureSizeThreshold=1048576
-XX:+UseParallelGC
...

적응형 조율 방식:

# 단순한 목표 설정 😊
-Xmx4g                     # 최대 힙 크기만 설정
-XX:MaxGCPauseMillis=200   # 또는 정지시간 목표
-XX:GCTimeRatio=99         # 또는 처리량 목표  
-XX:+UseAdaptiveSizePolicy # 자동 조율 활성화

결과: 복잡한 세부 설정에서 해방! 🎉

🔍 PS vs 파뉴 - 차이점 정리

구분 파뉴 (ParNew) PS (Parallel Scavenge)

목표 CMS와의 호환성 처리량 최대화
초점 지연시간 최소화 전체 효율성
조합 CMS와 페어링 Parallel Old와 페어링
자동 튜닝 ❌ 수동 설정 ✅ 적응형 조율
제어 매개변수 기본적인 것들 정교한 처리량 제어
적합한 용도 응답성 중요한 앱 배치/분석 작업

 

📋 PS 컬렉터 사용 가이드

✅ PS 컬렉터를 써야 하는 경우:

🎯 처리량이 최우선인 상황:

  • 배치 처리 작업
  • 데이터 분석 프로그램
  • 과학 계산 애플리케이션
  • 백엔드 API 서버 (응답시간보다 처리량 중심)

👨‍💼 운영자 관점에서:

  • GC 튜닝 경험이 부족한 경우
  • 복잡한 설정 없이 좋은 성능을 원하는 경우
  • "일단 돌아가게만 해주세요!" 하는 상황

❌ PS 컬렉터를 피해야 하는 경우:

  • 실시간 응답이 중요한 웹 애플리케이션
  • 사용자 인터랙션이 많은 GUI 프로그램
  • 지연시간에 민감한 API 서비스
  • 게임 서버 (끊김 현상 발생 가능)

 

🎯 기본 설정 예시:

# 처리량 중심 배치 작업용
-XX:+UseParallelGC
-XX:+UseParallelOldGC  
-Xmx8g
-XX:GCTimeRatio=99
-XX:+UseAdaptiveSizePolicy

# 적당한 응답성도 고려한 서버용  
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-Xmx4g  
-XX:MaxGCPauseMillis=200
-XX:GCTimeRatio=19
-XX:+UseAdaptiveSizePolicy

 

3.5.4 시리얼 올드 컬렉터

시리얼 올드 컬렉터는 이름에서 알 수 있듯이 시리얼 컬렉터의 구세대용 버전이에요.

기본 특징

  • 단일 스레드로 동작 (시리얼 컬렉터와 동일)
  • 마크-컴팩트 알고리즘 사용
  • 주로 클라이언트용 핫스팟 가상머신에서 활용

 

동작 방식

CPU0: 사용자 스레드 → [멈춤] → GC 혼자 구세대 정리 → [재시작]
CPU1: 사용자 스레드 → [멈춤] → 대기중... → [재시작]  
CPU2: 사용자 스레드 → [멈춤] → 대기중... → [재시작]
CPU3: 사용자 스레드 → [멈춤] → 대기중... → [재시작]

서버에서 시리얼 올드를 쓰는 이유?

"서버에서 단일 스레드 GC를 쓴다고요?"

맞아요, 보통은 안 써요. 하지만 특수한 상황 두 가지가 있어요:

1️⃣ 과거 PS 컬렉터와의 조합 (JDK 5 이전)

  • JDK 6 이전에는 패러렐 올드가 없었어요
  • PS 컬렉터를 쓰려면 구세대는 시리얼 올드와 강제 매칭
  • 신세대는 멀티스레드, 구세대는 단일 스레드인 기괴한 조합...

2️⃣ CMS 컬렉터의 백업 플랜

  • CMS 컬렉터가 실패했을 때의 비상용
  • "동시 모드 실패(Concurrent Mode Failure)" 발생 시
  • 시리얼 올드가 긴급 출동해서 정리 작업
  • 참고: PS + 시리얼 올드 조합은 JDK 14에서 폐기 예정, JDK 15부터 완전 제거됐어요.

 

패러렐 올드 컬렉터 - "PS의 진정한 파트너 등장!"

패러렐 올드 컬렉터는 PS 컬렉터가 그토록 기다렸던 구세대 전용 파트너예요!

JDK 6 이전의 암흑기

신세대: PS (멀티스레드) ⚡
구세대: 시리얼 올드 (단일스레드) 🐌

결과: 불균형한 성능, 전체 처리량 제한

JDK 6 이후의 해방:

신세대: PS (멀티스레드) ⚡  
구세대: 패러렐 올드 (멀티스레드) ⚡

결과: 완벽한 하모니, 처리량 극대화!

 

성능 비교 - 왜 패러렐 올드가 필요했을까?

🐌 기존 문제점:

  • 서버용 멀티코어에서 구세대만 단일 스레드
  • 구세대 메모리는 크고 하드웨어는 좋은데 활용 못함
  • 전체 처리량이 파뉴 + CMS보다 떨어짐

⚡ 패러렐 올드의 해결책:

  • 멀티스레드로 병렬 회수 지원
  • 마크-컴팩트 알고리즘 기반
  • 서버용 프로세서의 병렬 처리 역량 완전 활용

실제 성능 시나리오:

16코어 서버에서 큰 힙 메모리 처리:

[PS + 시리얼 올드]
신세대: 16코어 모두 활용 → 빠름
구세대: 1코어만 사용 → 병목 발생
전체: 구세대에 발목 잡힘

[PS + 패러렐 올드]  
신세대: 16코어 모두 활용 → 빠름
구세대: 16코어 모두 활용 → 빠름  
전체: 진정한 처리량 극대화!

🎯 언제 PS + 패러렐 올드 조합을 쓸까?

✅ 추천 상황:

  • 처리량이 최우선인 배치 처리
  • 프로세서 자원이 부족한 환경
  • 멀티코어 서버에서 효율성 극대화 원할 때
  • 복잡한 GC 튜닝 없이 좋은 성능 원할 때

활성화 방법:

-XX:+UseParallelGC  # PS + 패러렐 올드 조합 활성화

적합한 애플리케이션 타입:

  • 대용량 데이터 처리
  • 과학적 계산 프로그램
  • ETL (Extract, Transform, Load) 작업
  • 보고서 생성 시스템
  • 배치 작업 서버

 

다른 조합들과의 비교

조합 신세대 구세대 특징 적합한 용도

시리얼 + 시리얼 올드 단일스레드 단일스레드 단순함, 메모리 효율적 소형 앱, 임베디드
파뉴 + CMS 멀티스레드 동시실행 낮은 지연시간 응답성 중요한 웹앱
PS + 패러렐 올드 멀티스레드 멀티스레드 높은 처리량 배치, 분석 작업

 

실전 설정 예시

기본 처리량 최적화:

# 처리량 우선 서버 설정
-XX:+UseParallelGC
-Xmx8g
-XX:GCTimeRatio=99
-XX:+UseAdaptiveSizePolicy

적당한 응답성 고려:

# 처리량 + 어느정도 응답성
-XX:+UseParallelGC
-Xmx4g
-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=8  # 코어 수에 맞게 조정

🔍 패러렐 올드의 동작 방식

모든 CPU가 동시에 구세대 GC 작업:

CPU0: GC 스레드1 → 마크-컴팩트 작업
CPU1: GC 스레드2 → 마크-컴팩트 작업  
CPU2: GC 스레드3 → 마크-컴팩트 작업
CPU3: GC 스레드4 → 마크-컴팩트 작업

결과: 구세대 정리 시간 대폭 단축!

 

 

3.5.6 CMS 컬렉터 

드디어 왔어요! CMS(Concurrent Mark Sweep) 컬렉터 차례입니다!

언젠간 정리해야겠다 생각했던 동시성 GC의 혁명적 존재... 지금 바로 파헤쳐보겠어요. CMS는 정말 "게임 체인저"였어요. 하지만 완벽하지 않았기에 결국 역사 속으로 사라진 비운의 주인공이기도 하죠.

CMS의 혁명적 목표 - "멈추지 말고 계속 일하자!"

CMS의 핵심 철학: 가비지 컬렉션 일시 정지 시간을 최소로 줄이자!

왜 이게 혁명적이었을까?

기존 컬렉터들:

  • GC 시작 → 모든 사용자 스레드 정지 → GC 완료 → 재시작
  • "잠깐만, 청소 끝날 때까지 모두 멈춰!"

CMS의 도전:

  • GC와 사용자 스레드가 동시에 실행
  • "청소하면서도 계속 일할 수 있어!"

실제 영향은?

인터넷 서비스 백엔드브라우저-서버 시스템에서:

  • 사용자가 클릭했을 때 → 즉시 응답
  • 서비스 응답 시간 = 사용자 경험의 핵심
  • STW 시간이 길면 → "서버가 먹통인가?" 😰

CMS는 바로 이런 요구에 맞춰 태어났어요!

 

CMS의 4단계 동작 방식

CMS는 기존보다 훨씬 복잡해요. 4단계로 나뉘어 동작합니다:

1️⃣ 최초 표시 (Initial Mark)

상태: STW 발생 (하지만 아주 짧음)
작업: GC 루트와 직접 연결된 객체들만 표시
시간: 매우 빠름 ⚡

2️⃣ 동시 표시 (Concurrent Mark)

상태: 사용자 스레드와 동시 실행! 🎉
작업: 객체 그래프 전체 탐색
시간: 오래 걸림 (하지만 STW 없음)

3️⃣ 재표시 (Remark)

상태: STW 발생 (최초 표시보다 살짝 김)
작업: 동시 표시 중 변경된 참조 관계 수정
시간: 동시 표시보다는 훨씬 짧음

4️⃣ 동시 쓸기 (Concurrent Sweep)

상태: 사용자 스레드와 동시 실행! 🎉
작업: 죽은 객체들 회수
특징: 살아있는 객체는 안 옮김

 

💡 핵심 포인트

가장 오래 걸리는 2단계(동시 표시)와 4단계(동시 쓸기)에서 STW가 없어요! 그래서 "사용자 스레드와 동시에 수행된다"고 말하는 거예요.

 

 CMS의 성과와 한계

CMS는 분명 "동시적 짧은 정지 시간 컬렉터"로 불릴 만큼 훌륭했어요. 하지만 완벽하지 않았죠...

장점

  • 정지 시간 대폭 단축
  • 응답성 중요한 애플리케이션에 최적화
  • 동시성의 첫 번째 성공작

 

 단점 1: 프로세서 자원에 민감

GC 스레드 수 공식: (CPU 코어 수 + 3) / 4

실제 영향:

4코어 시스템: GC가 CPU의 25% 사용 → 괜찮음
2코어 시스템: GC가 CPU의 50% 사용 → 심각한 성능 저하! 😱

해결 시도: 점진적 동시 마크 스윕(iCMS)

  • GC 스레드와 사용자 스레드를 교대로 실행
  • 마치 단일 코어 시절 멀티태스킹처럼
  • 결과: 전체 시간은 늘어나지만 영향은 분산
  • 현실: 성과가 미미해서 JDK 7부터 폐기, JDK 9에서 완전 제거

단점 2: 부유 쓰레기와 동시 모드 실패

부유 쓰레기(Floating Garbage)란?

  • 표시 스레드가 지나간 후에 생긴 쓰레기
  • 쓸기 단계에서 회수 불가
  • 다음 GC까지 대기해야 함

메모리 확보 딜레마

일반 컬렉터: 메모리 가득 찰 때까지 대기
CMS: 동시 쓸기 중에도 사용자 스레드가 메모리 사용
→ 미리 여유 공간 확보 필요!

임계값 변화:

  • JDK 5: 구세대 68% 차면 CMS 시작 (보수적)
  • JDK 6: 92%로 상향 조정

위험한 상황:

구세대 92% 찬 상태에서 CMS 시작
→ 갑자기 메모리 많이 필요한 상황 발생
→ 동시 모드 실패!
→ 시리얼 올드 컬렉터 긴급 투입
→ 매우 긴 STW 발생 💀

튜닝 매개변수:

-XX:CMSInitiatingOccupancyFraction=75  # 75%에서 시작

 

단점 3: 파편화 문제

마크-스윕 알고리즘의 숙명: 파편화 발생

문제 시나리오:

구세대 전체: 500MB 여유 공간
하지만: 최대 연속 공간이 10MB만 가능
요청: 50MB 연속 공간이 필요한 큰 객체
결과: 전체 GC 불가피! 😭

해결 시도한 매개변수들 (JDK 9에서 모두 제거):

# 전체 GC 시 조각 모음 수행 (기본 활성화)
-XX:+UseCMSCompactAtFullCollection

# N번의 전체 GC 후 조각 모음 (기본값 0)
-XX:CMSFullGCsBeforeCompaction=3

딜레마:

  • 조각 모음 안 하면 → 파편화 심해짐
  • 조각 모음 하면 → 긴 STW 발생 (살아있는 객체 이동 필요)

📊 CMS 실전 설정 예시

# 기본 CMS 설정
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC  # 신세대는 파뉴 자동 선택

# 메모리 임계값 조정
-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSInitiatingOccupancyOnly

# 파편화 대응 (JDK 8 이하)
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=2

🎭 CMS의 종말과 유산

  • 후계자들 등장
    • G1: 파편화 해결 + 예측 가능한 정지 시간
    • 셰넌도어: 동시 이동 가능
    • ZGC: 힙 크기와 무관한 짧은 정지 시간
  • 이로 인해서 JDK 9: 폐기 예정 발표, JDK 14: 완전 제거

💭 CMS를 돌아보며...

CMS는 "혁신적이었지만 완벽하지 않은" 컬렉터였어요.

의미 있는 도전:

  • 동시성 GC의 가능성을 처음 보여줌
  • 응답 시간 중심 사고의 전환점
  • 후속 컬렉터들의 영감이 됨

아쉬운 한계:

  • 프로세서 자원 의존성
  • 부유 쓰레기와 동시 모드 실패
  • 해결하기 어려운 파편화

CMS가 있었기에 G1이 태어날 수 있었고, 더 나은 동시성 GC들이 계속 발전하고 있어요.


3.5.7 G1 컬렉터 

G1'Garbage First'의 줄임말이에요. 이름부터 뭔가 있어 보이죠?
G1은 JDK 7부터 등장했지만, 실제로 완전한 기능을 갖춘 컬렉터가 되기까지는 무려 10년이 걸렸어요! 

G1의 핵심 아이디어

기존 컬렉터들은 이런 식이었어요:

  • "신세대 전체를 청소하자!" (마이너 GC)
  • "구세대 전체를 청소하자!" (메이저 GC)
  • "아니면 아예 다 청소하자!" (전체 GC)

하지만 G1은 다르게 생각했어요
"어디가 쓰레기가 가장 많지? 거기부터 치우자!"
이게 바로 '가비지 우선(Garbage First)'의 핵심이에요!

 

G1의 혁신적인 메모리 구조

리전(Region) 기반 레이아웃

G1은 기존의 고정된 세대 구분을 버리고, 힙을 동일한 크기의 리전으로 나눴어요.

기존: [신세대] [구세대] 이렇게 딱 나뉘어져 있었다면
G1:   [R1][R2][R3][R4][R5][R6][R7][R8] 이런 식으로 나눠요!

각 리전은 필요에 따라:

  • 🐣 에덴 공간 (새로운 객체들)
  • 🛡️ 생존자 공간 (살아남은 객체들)
  • 👴 구세대 공간 (오래된 객체들)
  • 🦣 거대 리전 (큰 객체들)

이 될 수 있어요!

리전 크기 설정

-XX:G1HeapRegionSize=16m  # 16MB로 설정
  • 범위: 1MB ~ 32MB (2의 제곱수만 가능)
  • 리전 크기의 절반보다 큰 객체는 거대 리전에 저장돼요

 

 G1의 동작 과정

G1은 크게 4단계로 동작해요:

1단계: 최초 표시 (Initial Mark)

  • GC 루트에서 직접 참조하는 객체들을 표시
  • 사용자 스레드를 잠깐 멈춤 (마이너 GC와 같이 실행되서 추가 정지 시간 없음!)
  • ⏱️ 매우 빠름

2단계: 동시 표시 (Concurrent Mark)

  • 객체 그래프를 재귀적으로 스캔
  • 사용자 스레드와 동시에 실행
  • 📊 각 리전의 쓰레기 누적값 계산

3단계: 재표시 (Remark)

  • 동시 표시 중 변경된 객체들을 다시 처리
  • 사용자 스레드를 잠깐 멈춤
  • 매우 빠름 (변경된 것들만 처리)

4단계: 복사 및 청소 (Evacuation)

  • 통계 데이터 기반으로 회수 계획 수립
  • 효과가 큰 리전부터 우선 회수
  • 살아남은 객체들을 새로운 리전으로 이주
  • 사용자 스레드를 잠시 멈춤 (병렬 처리)

정지 시간 예측 모델

G1의 가장 큰 장점 중 하나는 정지 시간을 예측할 수 있다는 거예요!

-XX:MaxGCPauseMillis=200  # 목표 정지 시간을 200ms로 설정

어떻게 예측하나요?

G1은 감소 평균(Decaying Average)을 사용해요:

  • 각 리전의 회수 시간을 기록
  • 기억 집합의 더럽혀진 카드 개수 추적
  • 최근 데이터에 더 높은 가중치 부여
  • 이 정보로 "어느 리전을 회수하면 목표 시간 내에 최대 효과를 낼지" 예측!

현실적인 목표 설정이 중요해요!

❌ -XX:MaxGCPauseMillis=20   # 너무 짧음! 
✅ -XX:MaxGCPauseMillis=200  # 적절함

왜 20ms는 안 되나요?

  • 목표가 너무 짧으면 → 조금만 회수하고 끝남
  • 회수 속도 < 할당 속도 → 쓰레기가 계속 쌓임
  • 결국 힙이 가득 차서 → 전체 GC 발생 → 더 오래 멈춤 😱

적정 목표 시간: 100~300ms 정도가 좋아요!

G1의 장점들

메모리 파편화 없음

  • CMS: 마크-스윕 → 파편화 발생
  • G1: 마크-컴팩트 + 마크-카피 → 깔끔하게 정리!

정지 시간 제어 가능

  • 사용자가 목표 시간 설정 가능

효율적인 회수 전략

  • 쓰레기가 많은 리전부터 우선 처리

G1의 단점들

메모리 오버헤드

  • 초기에는 힙의 20% 이상 추가 사용
  • 하지만 지속적으로 개선되고 있어요! (현재는 1.3GB 정도로 줄어듦)

실행 오버헤드

  • 복잡한 카드 테이블 관리
  • 사전/사후 쓰기 장벽 등으로 인한 부하

언제 G1을 써야 할까?

경험상 이런 기준이 있어요:

  • 힙 크기 6GB 미만: 예전엔 CMS가 나았음
  • 힙 크기 6~8GB 이상: G1이 확실히 유리!

하지만 지금은 CMS가 없으니까... 고민할 필요 없어요! 그냥 G1 쓰세요 😄

 

G1 설정 팁

기본 설정

# JDK 9부터는 G1이 기본 컬렉터예요!
-XX:+UseG1GC                    # G1 활성화
-XX:MaxGCPauseMillis=200        # 목표 정지 시간
-XX:G1HeapRegionSize=16m        # 리전 크기

모니터링용 설정

-XX:+PrintGC                    # GC 로그 출력
-XX:+PrintGCDetails             # 상세 GC 로그
-XX:+PrintGCTimeStamps          # 타임스탬프 추가

핵심 포인트 정리

G1을 선택해야 하는 경우

  1. 큰 힙을 사용하는 애플리케이션 (6GB+)
  2. 정지 시간에 민감한 서비스
  3. 장시간 실행되는 서버 애플리케이션
  4. 메모리 파편화가 걱정되는 경우

주의사항

  1. 현실적인 목표 시간 설정 (100~300ms)
  2. 메모리 오버헤드 고려 (추가로 10~20% 사용)
  3. 애플리케이션 특성에 맞는 튜닝 필요

 

 

728x90
반응형