[JAVA] Garbage Collection 2

Seongje kim, 01 February 2020

지난 포스트에 이어서 Java 가비지 컬렉션 2 부 포스트입니다. 아래 1 부 포스트를 참고하시기 바랍니다.
Garbage Collection 1

G1(Garbage First) Garbage Collector


이번 포스트에서는 GC 수행 방식 중 하나인 ‘G1 GC’ 방식에 대해 알아볼 것이다.

우선 GC 의 성능을 결정하는 큰 이슈는 바로 ‘Stop the World 시간이 얼마나 걸리는가’ 이다. GC 가 발생하면 JVMGC 스레드를 제외한 모든 스레드를 일시 중단시키기 때문에 Stop the Wolrd 시간이 짧을수록 성능상 이점을 가질 수 있다. 그러므로 GC 튜닝은 이 Stop the World 시간을 줄이는 것이 주목적이다.

G1 GC 의 가장 큰 장점은 성능이다. 기존의 다른 어떤 방식보다 속도가 빠르다. 그래서 멀티 프로세서와 큰 메모리를 필요로 하는 프로그램 또는 서버 프로그램에 적합한 방식이다.

G1 방식의 처음 설계 목표는 아래와 같다.

  1. 기존 CMS 방식과 같이 다른 스레드와 함께 Concurrently 하게 동작해야 한다.
  2. 스레드 정지를 유발하는 긴 GC 없이 여유 공간을 압축해야 한다.
  3. 스레드 정지 시간을 조금 더 예측 가능해야 한다.
  4. 처리량(Throughput)에 대한 성능을 희생하지 않는다.

또한 G1 방식은 기존 CMS 방식을 대체하기 위해 설계되었기 때문에 CMS 방식의 단점을 보완하였다.

  1. Compaction 과정을 포함하는 알고리즘 사용
  2. GC 단순화 및 잠재적인 메모리 단편화 이슈 제거
  3. GC 로 인한 스레드 정지 시간 예측

결과적으로 처리량과 응답 시간에서 높은 성능을 목표로 만들어진 GC 방식이며 JDK 7(Update 4) 이후로 안정적으로 지원된다.

G1 방식의 힙 영역

이제까지 살펴봤던 GC 방식들은 모두 힙 영역을 Young 영역, Old 영역, Permanent(Java 8 이후, Metaspace) 영역으로 나누었다. 하지만 G1 은 이전 방식들과 다른 방법으로 접근한다.

G1 방식은 메모리를 페이징(Paging) 하듯이 논리적인 단위인 ‘Region’ 으로 나눠서 관리한다. CMS 방식과 달리 Region 이라는 논리적인 단위로 메모리를 관리함으로써 Compaction 단계를 진행하고 메모리 단편화 문제를 해결하며, Stop the World 시간을 예측할 수 있다.

전체 힙 메모리는 분할된 영역들(Region 단위)의 집합으로 나누어진다. Region 의 크기는 처음 JVM 이 시작될 때 정해지며 1 ~ 32 Mb 크기의 약 2000 개 지역으로 나뉜다. 그 후 Region 은 다시 Eden, Survivor, Old 영역으로 구분되며, GC 수행 후 살아남은 객체들은 이 영역에 따라 복사 및 이동된다. 각 영역은 이전처럼 각자의 역할을 수행하지만 고정된 크기가 아닌 유연하게 메모리를 할당받는다.

Garbage First

이 방식이 왜 ‘Garbage First’ 인지 알기 위해 아래 과정을 먼저 살펴보자.

  1. 우선 G1 은 전역으로 Concurrent 한 마킹(Marking)을 수행한다.
  2. 어떤 지역(Region)에서 많은 쓰레기 객체를 수집할 수 있는지 찾는다.
  3. 가장 많은 여유 공간을 만들어 내는 지역을 먼저 청소하고 압축(Compaction) 한다.

즉, 가장 많은 여유 공간을 만들어 내는 지역을 먼저 수집하기 때문에 Garbage First 라고 말한다.

G1 은 이러한 과정에서 정의한 스레드 정지 시간(Stop the World)에 대한 목표를 충족시키고, 이 정지 시간을 기반으로 수집할 지역의 개수를 선택한다. 이는 이전 데이터를 기반으로 예측하기 때문에 대부분의 경우를 충족시킨다.

정리하자면 G1 은 정의된 스레드 정지 시간에 맞춰 특정 지역(여유 공간 우선)에서 살아남은 객체들을 다른 지역으로 이동시키고, 남은 쓰레기 객체들을 청소한다. 이 과정에서 Compaction 이 이루어지며, 이는 처리량 향상과 시간 절약을 위해 병렬적(멀티 스레드)으로 수행된다.

G1 동작 과정


이제 G1 GC 가 단계별로 동작하는 과정을 살펴보자.

Young GC

위 그림을 보면 힙 영역이 여러 Region(Eden, Survivor, Old) 으로 나뉜 것을 볼 수 있다. Region 단위로 메모리를 관리하기 때문에 이전 방식들과는 달리 한 영역에 대한 연속된 공간을 할당할 필요가 없으며, 분리되어 있는 이들의 크기는 동적으로 할당된다. 즉, 이전의 방식들과 달리 필요에 따라 각 영역들을 편하게 Resize 할 수 있고, 유연한 메모리 관리가 가능하다.

먼저 G1Young GC 가 발생하면 Eden 또는 Survivor 에서 살아남은 객체들은 한 개 이상의 다른 Survivor 으로 이동한다. 물론 이 과정에서 Stop the World 가 발생하지만 앞서 설명했듯이 병렬(멀티 스레드)로 처리되기 때문에 이 소요 시간이 최소화된다. 또한 이때, GCEdenSurvivor 의 크기를 계산하여 따로 저장하는데, 이는 다음 GC 에서의 Stop the World 목표 시간과 각 Region 의 크기 조정에 이용된다.

위 그림과 같이 Young GC 수행 결과, 살아남은 객체들은 Survivor 또는 Old 로 이동된다. 또한 이들은 Region 이동과 함께 Compaction 된 상태가 된다. 단, Old 로 이동되는 객체들은 Age 기준을 초과한 객체들이다.

Old GC

G1Old GC 또한 CMS 방식과 마찬가지로 Stop the World 시간을 최소화 하는 것을 목표로 한다.

1. Initial Mark

최초 Initial Mark 단계에서는 Stop the Wolrd 가 발생한다. 보통 Young GCMark 단계와 같이 수행되는데, Old Region 의 객체를 참조하고 있는 객체가 존재하는 Survivor Region 을 식별(Marking)한다.

2. Root Region Scanning

Root Region Scanning 단계는 애플리케이션의 다른 스레드와 함께 동작하며 Initial Mark 단계에서 식별한 Survivor Region 을 스캔(Scan)하여 Old Region 의 객체를 참조하고 있는 객체를 식별한다. 또한 이 단계는 Young GC 가 발생하기 전까지 끝나야 한다.

3. Concurrent Marking

Concurrent Marking 단계에서는 힙 영역 전체에 걸쳐 참조되고 있는 객체(즉, 살아남은 객체)를 찾는다. 이는 여유 공간을 가장 많이 만들 수 있는(쓰레기 객체가 제일 많은) Region 계산을 포함한다. 이 또한 애플리케이션 스레드와 함께 동작하는데, Young GC 에 의해 Interrupt 될 수 있다.

그 결과, 완전히 비어있는 Region 을 찾게되면 해당 RegionUnused 상태로 만든다.

4. Remark

Remark 단계에서 다시 Stop the Wolrd 가 발생하며, 힙 영역의 살아있는 객체에 대한 식별 작업을 마저 완료한다. 즉, Concurrent Marking 단계에 대한 검증 단계이다. 이때, ‘Snapshot-At-The-Beginning(SATB)’ 이라는 알고리즘을 사용하는데, 이는 CMS 방식의 Remark 단계에서 사용되는 알고리즘보다 더 빠른 장점을 가진다.

이 단계의 결과, 빈 Region 들을 모두 제거하고 메모리를 다시 확보함으로써 모든 Region 중 살아남은 Region 에 대한 계산이 완료된다.

5. Cleanup & Copying

Cleanup & Copying 단계에서는 이전까지 식별한 살아남은 객체들을 비어있는 영역으로 옮기고, 쓰레기 객체들을 삭제 후 비워진 영역을 Unused 상태로 만든다. 이때, 살아남은 객체들을 비어있는 영역으로 옮기는 과정에서 Stop the Wolrd 가 발생한다.

이 단계는 살아있는 객체가 아주 적은 Old Region (보통 수집이 가장 빠르게 진행되는 지역)을 ‘GC pause (mixed)’ 라는 로그만 표시해놓고 Young GC 수행시 같이 수집한다.
즉, YoungOld Region 이 동시간에 수집되며 Compaction 까지 포함한다.

정리


G1 GC 의 주 목표는 제한된 GC 응답 시간과 큰 힙 메모리 공간을 필요로 하는 애플리케이션에서의 속도와 처리율를 향상 시키는 것이다. 좀 더 구체적으로 G1 방식을 적용하는 목표 대상은 힙 영역이 6 GB 이상, 스레드 정지 시간이 0.5 초 이내로 예측 가능한 수준을 필요로 하는 프로그램들이다.

만일 기존 CMS GC 또는 Paralled Old GC 를 사용하는 애플리케이션들이 아래와 같은 경우, G1 방식으로 교체했을 때 성능상 이득을 볼 수 있을 것이다.

  1. Major GC(Full GC) 시간이 너무 길거나 자주 발생할 때
  2. 객체 할당 비율과 할당된 객체들이 살아남아 Old 영역으로 이동하는 비율의 큰 차이가 있을 떄
  3. GCCompaction 시간에서 성능상 손해가 나타날 때 (약 0.5 ~ 1 초 보다 더 길어질 때)

하지만 기존 애플리케이션에서 사용하는 GC 방식이 긴 스레드 정지 시간을 유발하지 않는다면 G1 GC 는 선택 사항일 뿐, G1 방식과 최신 Java 버전은 필요충분 조건이 아니다.


이것으로 지난 포스트와 함께 Java 가비지 컬렉션에 대해 알아보았습니다.

GC 는 Java 기반의 서비스 애플리케이션에서 성능과 연관된 중요 이슈이기 때문에 이를 정확히 이해하고 사용하는 것이 중요하다고 생각합니다. 더 나아가 모든 Java 기반 서비스 애플리케이션에서 필수적이지는 않지만 GC 성능을 향상시키기 위해 GC 튜닝을 고려한다면 기존 애플리케이션의 성능 향상을 기대해 볼 수 있을 것이라고 생각합니다. 아래 글들을 통해 GC 튜닝에 대한 더 자세한 정보를 참고하시기 바랍니다.

Garbage Collection 모니터링 방법
Garbage Collection 튜닝

감사합니다.