본문 바로가기

탐구일지/Java

G1 GC는 왜 기본 GC가 되었을까?

1. 배경

 

Java 애플리케이션은 JVM 위에서 동작하며, 객체의 생성과 소멸은 Heap 메모리 안에서 이루어진다
그리고 JVM은 더 이상 사용되지 않는 객체를 자동으로 회수하기 위해 GC를 수행한다

 

GC는 개발자가 직접 메모리를 해제하지 않아도 된다는 장점을 제공하지만,

그 대가로 애플리케이션 실행 중 특정 시점에 메모리 회수를 위한 작업이 필요하다

 

이 과정에서 애플리케이션의 실행이 일시적으로 멈출 수 있다는 문제가 있고, 이를 STW(Stop-The-World)라 한다

이를 해결하기 위해 GC는 지속적으로 발전해왔다 ─ GC의 변천사

 

그렇다면, 다양한 GC가 존재하는 상황에서 왜 G1 GC가 기본 GC로 선택되었을까?

이 글에서는 이 질문에 대한 답을 해보려 한다

 

2. 기존 GC의 한계

 

앞서 살펴본 것처럼 GC는 각 시대의 요구사항에 맞추어 발전해왔다

초기 GC들은 비교적 단순한 구조와 작은 Heap 환경을 전제로 설계되었으며,

처리량을 높이거나 구현을 단순화하는 데에 초점이 맞춰져 있었다

 

하지만 현대의 애플리케이션 환경은 과거와는 크게 달라졌다

 

대규모 트래픽을 처리하는 서버 환경에서는 수 GB에서 수십 GB 이상의 Heap을 사용하는 경우가 일반적이며,

객체 생성과 소멸 또한 매우 빈번하게 발생한다

 

이러한 환경에서는 단순히 GC를 빠르게 수행하는 것만으로는 충분하지 않다

 

GC 수행 시간이 길어질수록 STW로 인한 지연이 서비스에 직접적인 영향을 주게 되고,

특히 대용량 Heap에서는 이러한 Pause 시간이 더욱 길어지는 문제가 발생한다

 

즉, 기존 GC들은 각각의 문제를 부분적으로 해결하는 데에는 성공했지만,

현대적인 서비스 환경에서 요구되는 처리량, 지연 시간, 안정성을 동시에 만족시키지는 못했다

 

물론 이러한 GC들이 쓸모없어진 것은 아니다

Heap이 작고 단순한 환경이나, 임베디드 환경과 같이 자원이 제한된 상황에서는 오히려 더 적합할 수 있다

 

하지만 대규모 서버 환경에서는 보다 균형 잡힌 성능과 예측 가능한 동작이 필요하게 되었고,

이러한 요구를 바탕으로 등장한 GC가 바로 G1 GC이다

 

3. G1 GC는 무엇을 해결하려 했는가?

 

G1 GC의 목표는 특정 성능을 극단적으로 최적화하는 것이 아니라, 다음과 같은 요소를 균형 있게 만족시키는 것이다

 

  • 대용량 Heap 환경에서도 안정적인 동작
  • 짧고 예측 가능한 Pause Time
  • 메모리 단편화 문제 해결

이러한 목표를 달성하기 위해 G1 GC는 기존 GC와는 다른 접근 방식을 선택했다

 

기존 GC가 "전체 Heap을 어떻게 수집할 것인가"에 초점을 맞추었다면,

G1 GC는 "어디를 수집할 것인가"를 선택하는 방식으로 설계되었다

 

G1 GC는 GC 알고리즘 자체를 단순히 개선한 것이 아니라,

메모리 수집 대상을 선택하는 전략을 도입함으로써 기존 GC의 한계를 해결하려고 했다

 

4. G1 GC의 핵심 설계 ─ Region 기반 구조

 

G1 GC의 가장 큰 특징은 Heap을 관리하는 방식에 있다

 

기존 GC들은 Heap을 Young 영역과 Old 영역으로 나누고, 각 영역을 기준으로 GC를 수행하는 구조를 사용했다

하지만 G1 GC는 이러한 고정된 영역 구조에서 벗어나, Heap을 동일한 크기의 여러 Region으로 나눠 관리한다

 

(E: Eden, S: Survivor, O: Old, H: Humongous)

 

각 Region은 상황에 따라 Eden, Survivor, Old와 같은 역할을 동적으로 가지게 되며,

특정 영역에 고정되지 않고 유연하게 사용된다

 

이러한 구조를 통해 G1 GC는 전체 Heap이 아닌 일부 Region만 선택적으로 수집할 수 있게 된다

G1 GC는 Heap 전체를 대상으로 GC를 수집하는 것이 아닌, "가비지가 많이 쌓인 영역"을 우선적으로 선택해 수집한다

 

이것이 바로 G1(Garbage First)이라는 이름이 붙은 이유이다

 

5. G1 GC의 동작 흐름

 

G1 GC는 다음과 같이 세 가지 흐름으로 나눌 수 있다

 

  • Young-Only GC
  • Concurrent Marking
  • Mixed GC (Space Reclamation)

이 세 단계는 서로 독립적인 것이 아니라 Heap 상태에 따라 유기적으로 연결되어 동작한다

 

  1. Young-Only GC
    G1 GC는 기본적으로 Young 영역(Eden, Survivor)을 중심으로 GC를 수행한다

    이 단계에서는 기존 GC인 Minor GC와 유사하게 새롭게 생성된 객체들을 빠르게 회수하는데 초점을 둔다
    객체는 Eden에서 생성되고, GC 이후 살아남은 객체는 Survivor 또는 Old 영역으로 이동한다

    이 과정은 STW로 수행되지만, 전체 Heap이 아니라 일부 Region만을 대상으로 하기 때문에
    비교적 짧은 Pause Time을 유지할 수 있다

    G1 GC는 초기에는 이러한 Young-Only GC를 반복하면서 Heap을 관리한다



    G1 GC에서 객체 할당 시 Eden 영역에 공간이 부족해지면 Young GC가 트리거된다
    이때 객체를 다른 Region에 우회 배치하는 것이 아니라, GC를 통해 메모리를 확보한 뒤 할당이 진행된다

    GC 이후 JVM이 다음 사이클을 위해 판단을 통해 동적으로 Eden 영역의 개수를 조정할 수 있다

    그렇다면 가장 초기의 Eden 영역은 1개 뿐인가?

    답은 "그렇지 않다"다
    JVM은 초기 Young 영역을 설정할 수 있다

    다음과 같은 설정을 예시로 들어보자

    -Xms8g
    -Xmx8g
    -XX:G1NewSizePercent=5
    -XX:G1MaxNewSizePercent=60
    -XX:G1HeapRegionSize=4m

    Heap 크기를 8GB로 고정하고, Young 영역의 최소 사이즈를 5%로 지정하자
    그리고 각 Region의 크기를 4MB로 지정하면


    초기 할당되는 Young 영역은 Eden + Survivor이며, 초기 Survivor의 갯수는 거의 없으므로
    약 100개의 Young 영역이 생기며, 이 중 대부분이 Eden 영역으로 사용된다

  2. Concurrent Marking
    Young-Only GC는 짧은 Pause Time을 유지하면서 객체를 빠르게 회수하는 데는 효과적이지만,
    Old 영역에 대한 정보는 충분히 반영하지 못한다

    Heap 사용량이 증가하고 Old 영역에 객체가 쌓이게 되면,
    어떤 영역에 가비지가 많이 존재하는지를 파악하는 과정이 필요하게 된다

    이때 수행되는 것이 Concurrent Marking이다

    Concurrent Marking이 이전 GC의 Marking과 다른 이유는 애플리케이션 실행과 동시에 수행된다는 것이다
    이를 통해 기존 GC에 비해 Pause Time 증가를 최소화할 수 있다

    이 과정으로 이후 어떤 Region을 우선적으로 수집할지를 결정하게 된다

    결과적으로 G1 GC는 전체 Heap을 한 번에 수집하는 방식이 아니라,
    가비지가 많은 Region을 선별적으로 수집하는 전략을 사용할 수 있게 된다

  3. Mixed GC (Space Reclamation)
    Concurrent Marking이 완료되면 G1 GC는 실제로 메모리를 회수하는 단계인 Mixed GC를 수행한다
    Mixed GC는 Young 영역뿐만 아니라 Old 영역 중 일부 Region을 함께 수집하는 GC이다

    이때 모든 Old 영역을 대상으로 GC를 수행하는 것이 아니라,
    Concurrent Marking 결과를 기반으로 가비지가 많이 포함된 Region만 선택적으로 수집하게 된다

    이를 통해 불필요한 GC 작업을 줄이면서도 효율적으로 메모리를 회수할 수 있다

    G1 GC는 이 과정에서 객체를 다른 Region으로 복사하는 Evacuation 방식을 사용한다
    살아있는 객체는 새로운 Region으로 이동하게 되고, 기동 Region은 비워지면서 재사용 가능한 상태가 된다

    이러한 과정은 자연스럽게 메모리 재배치를 동반하기 때문에, Compaction의 효과도 자연스럽게 갖게 된다

    결과적으로 G1 GC는 전체 Heap을 한 번에 정리하는 방식이 아니라,
    효율이 높은 영역만 선택적으로 정리하는 방식으로 안정적인 메모리 관리를 수행하게 된다
6. G1 GC의 특징과 고려사항

 

G1 GC에서는 다른 GC에서 갖고 있지 않는 여러 특징들이 있다

 

  • Humongous Object
    G1 GC에서는 일반적인 객체와 다르게 큰 객체를 별도로 처리한다

    G1 GC에서 특정 객체의 크기가 하나의 Region 크기의 절반을 초과하는 경우,
    이를 Humongous Object로 분류한다 

    이러한 객체는 Eden 영역을 거치지 않고, Old 영역의 연속된 Region에 직접 할당된다
    이는 큰 객체를 여러 번 복사하는 비용을 줄이기 위한 설계이지만, 동시에 몇 가지 단점을 발생시킨다

    첫째, Humongous Object는 연속된 Region을 필요로 하기 때문에, 외부 단편화로 할당에 실패할 수 있다


    둘째, Region 단위로 메모리를 할당하기에, 객체 크기가 Region 크기와 맞지 않을 경우 내부 단편화가 발생할 수 있다


  • SATB(Snapshot-At-The-Beginning)
    G1 GC의 Concurrent Marking 단계에서는 SATB 방식이 사용된다

    이는 마킹 시작 시점의 객체 상태를 기준으로 살아있는 객체를 판단하는 방식으로,
    GC 수행 중 참조가 변경되더라도 초기 상태를 유지할 수 있도록 보장한다

    이는 GC 수행 중 객체 상태가 변경되더라도 Marking의 일관성을 유지하기 위한 설계이다
    이를 통해 애플리케이션 실행과 동시에 수행되는 환경에서도 안정적인 Marking을 수행할 수 있다

    하지만 SATB 방식은 Concurrent Marking의 안정성을 보장하는 대신,
    GC 시작 이후에 더 이상 참조되지 않는 객체도 살아있는 것으로 판단하여
    다음 GC 사이클까지 남게 되는 Floating Garbage를 발생시킬 수 있다

    또한 참조 변경 시 Write Barrier를 통해 기존 참조 객체를 별도로 기록해야 하기 때문에
    추가적인 메모리와 CPU 오버헤드가 발생한다

  • Pause Prediction
    G1 GC의 또 다른 중요한  특징은 GC 수행 시간을 예측하고 제어하려는 점에 있다고 앞서 언급했다
    G1 GC는 이러한 문제를 해결하기 위해 사용자가 설정한 목표 Pause Time을 기준으로 GC를 수행하도록 설계되었다

     -XX:MaxGCPauseMillis 옵션을 통해 목표 Pause Time을 설정할 수 있으며,
    G1 GC는 이 목표를 만족시키기 위해 한 번의 GC에서 수집할 Region의 갯수를 동적으로 조절한다

    이를 통해 애플리케이션의 응답 지연을 일정 수준 이하로 유지할 수 있으며,
    특히 지연에 민감한 서비스 환경에서 안정적인 동작을 기대할 수 있다
7. 마무리하며


지금까지 살펴본 것처럼 G1 GC는 기존 GC들이 가지던 한계를 보완하기 위해 등장하였다

 

단순히 GC 수행 속도를 높이는 것이 아니라 Region 기반 구조를 통해 수집 대상을 선택하고,
Concurrent Marking과 STAB을 통해 안정성을 확보하며,

Mixed GC를 통해 효율적인 메모리 회수를 수행한다

또한 Pause Prediction을 통해 GC 수행 시간을 제어하려는 시도를 통해 기존 GC들과는 다른 방향의 발전을 이루었다

 

물론 Humongous Object와 같은 예외적인 경우에서는 여전히 단편화 문제가 발생할 수 있으며,

모든 상황에서 완벽한 GC라고 보기는 어렵다

 

그럼에도 불구하고 G1 GC는 처리량, 지연 시간, 안정성이라는 서로 상충되는 요소들을 가장 균형있게 만족시키는 GC이다

 

이러한 이유로 G1 GC는 대부분의 서버 환경에서 안정적으로 사용할 수 있는 기본 GC로 선택되었다