가비지 컬렉션(GC)의 기본 원리와 세대별(Generational) 메모리 관리

가비지 컬렉션(GC)의 기본 원리와 세대별(Generational) 메모리 관리

지난 포스팅에서는 객체들이 생성되고 저장되는 공간인 JVM 힙(Heap) 메모리의 세부 구조를 살펴보았습니다. C나 C++ 같은 언어에서는 개발자가 직접 메모리를 할당하고 해제해야 하지만, Java는 가비지 컬렉션(Garbage Collection, 이하 GC)이라는 훌륭한 매커니즘이 이 작업을 자동으로 수행해 줍니다.

하지만 자동이라는 말이 ‘신경 쓰지 않아도 된다’는 의미는 절대 아닙니다. 트래픽이 집중되는 엔터프라이즈 환경에서 GC가 동작하는 방식을 이해하지 못하면, 원인을 알 수 없는 서버 멈춤(Stop-The-World) 현상으로 인해 서비스 품질이 심각하게 저하될 수 있습니다. 이번 글에서는 GC가 어떻게 쓰레기 객체를 식별하고 청소하는지, 그리고 세대별 메모리 구조에서 이 과정이 어떻게 효율적으로 일어나는지 완벽하게 파헤쳐 보겠습니다.

1. 쓰레기를 식별하는 기본 원리: Reachability와 Mark-and-Sweep

GC가 메모리를 청소하기 위해서는 먼저 어떤 객체가 ‘사용 중’이고, 어떤 객체가 ‘버려진 쓰레기’인지 식별해야 합니다. JVM은 이를 위해 ‘도달 가능성(Reachability)’이라는 개념을 사용합니다.

  • Reachable vs Unreachable: 실행 중인 스레드, 정적(Static) 변수, JNI 참조 등에서 접근할 수 있는 객체를 ‘Reachable(도달 가능)’ 상태라고 합니다. 반대로 어떤 곳에서도 참조하고 있지 않아 더 이상 접근할 수 없는 객체는 ‘Unreachable(도달 불가능)’ 상태가 되며, 이들이 바로 GC의 청소 대상입니다.
  • Mark-and-Sweep 알고리즘: GC의 가장 기본적인 동작 방식입니다.
    1. Mark (식별): GC는 최상위 경로(GC Root)에서부터 연결된 모든 참조를 탐색하며 사용 중인 객체에 마킹(Mark)을 합니다.
    2. Sweep (청소): 마킹되지 않은 객체(Unreachable)들을 메모리에서 완전히 삭제하여 공간을 확보합니다.
    3. Compact (압축 – 선택적): 청소 후 메모리에 남은 빈 공간(단편화)을 없애기 위해 살아남은 객체들을 한곳으로 모아 압축하는 과정을 거치기도 합니다.

2. Young Generation의 초고속 청소: Minor GC

이전 글에서 언급한 ‘대부분의 객체는 금방 쓰레기가 된다’는 가설에 따라, 새롭게 생성된 객체들은 Young Generation의 Eden 영역에 먼저 할당됩니다.

  1. Eden 영역이 꽉 차게 되면 Minor GC가 발생합니다.
  2. Mark-and-Sweep 과정을 거쳐 살아남은(Reachable) 객체들은 두 개의 Survivor 영역 중 하나로 이동합니다.
  3. 이때 살아남은 객체들은 살아남은 횟수를 의미하는 Age bit(나이)가 1씩 증가합니다.
  4. 다음 Minor GC가 발생하면, Eden 영역의 생존 객체와 기존 Survivor 영역의 생존 객체들이 모두 비어있는 다른 Survivor 영역으로 한꺼번에 이동합니다. (즉, 두 개의 Survivor 영역 중 하나는 반드시 비어있어야 합니다.)

Minor GC는 대상 범위가 좁고 수명이 짧은 객체들을 빠르게 치우기 때문에, 애플리케이션의 성능에 거의 영향을 주지 않을 만큼 매우 짧은 시간 안에 완료됩니다.

3. Old Generation의 무거운 청소: Major GC (Full GC)

Minor GC 과정에서 여러 번 살아남아 Age bit가 특정 임계치(예: 15)에 도달한 객체들은 “이 객체는 앞으로도 계속 사용될 중요한 데이터구나”라고 판단되어 Old Generation 영역으로 이동(Promotion)하게 됩니다.

시간이 흘러 Old Generation 영역마저 꽉 차게 되면 마침내 Major GC (또는 Full GC)가 발생합니다.

  • Stop-The-World의 공포: Old 영역은 Young 영역보다 크기가 훨씬 크고, 객체들의 참조 관계도 복잡하게 얽혀 있습니다. 따라서 Major GC는 매우 무겁고 오래 걸리는 작업입니다. 안전한 메모리 정리를 위해 JVM은 GC를 수행하는 스레드를 제외한 애플리케이션의 모든 스레드를 일시 정지시키는데, 이를 Stop-The-World(STW)라고 부릅니다.
  • 성능 저하의 주범: 이 STW 시간이 1초 이상 길어지게 되면, 사용자는 웹사이트가 멈추거나 응답이 없는(Timeout) 현상을 겪게 됩니다. 우리가 수행하는 ‘GC 튜닝’의 궁극적인 목적은 바로 이 Major GC의 발생 횟수를 줄이고, Stop-The-World 시간을 최소화하는 것입니다.

결론: GC 원리 이해가 튜닝의 시작이다

가비지 컬렉션은 Mark-and-Sweep이라는 명확한 원리와, 객체의 생존 기간을 활용한 효율적인 세대별 분리를 통해 메모리를 관리합니다. Minor GC와 Major GC가 발생하는 타이밍과 원리를 명확히 이해해야만, 향후 애플리케이션에 맞는 GC 알고리즘을 선택하고 튜닝할 수 있습니다.

다음 포스팅에서는 이러한 GC를 수행하는 다양한 방식들, ‘[7번] 다양한 GC 알고리즘(Serial, Parallel, CMS)의 특징과 장단점 비교’에 대해 본격적으로 파헤쳐 보겠습니다. 서비스의 트래픽 규모에 따라 어떤 GC를 선택해야 하는지 확인해 보세요.

댓글 남기기