포스트

JVM 시리즈 - 구세대 가비지 컬렉터의 종류

구세대 가비지 컬렉터 정리

시리얼 컬렉터, 파뉴 컬렉터, 패러랠 스캐빈저 컬렉터, 시리얼 올드 컬렉터, 패러랠 올드 컬렉터, CMS 컬렉터, G1 컬렉터로 총 7개가 있다.

시리얼 컬렉터

가장 기초적이고 오래된 컬럭터다. 단일 스레드로 동작하며, 가비지 컬렉션이 시작되면 회수가 완료될 때까지 다른 모든 작업 스레드가 멈춰 있어야 한다. 효율적이라는 이점이 있고, 회수 효율이 좋지만 ‘스톱 더 월드’로 인한 지연시간이 있다.

–XX:+UseSerialGC 매개변수로 시리얼 컬렉터를 사용할 수 있다.

파뉴 컬렉터

여러 스레드를 활용하여 시리얼 컬렉터를 병렬화한 버전이다. 컬렉션 알고리즘, 객체 할당 규칙 등 모든 것이 시리얼 컬렉터와 완전히 같다. 하지만 인기가 높은데, CMS 컬렉터와 조합하여 사용할 수 있는 유일한 컬렉터이기 때문에 핫스팟 서버 버전에서 인기가 많다. 그러나, CMS 보다 더 진보된 G! 이 등장하여, 이제 파뉴 컬렉터는 사용되지 않는다.

패러렐 스캐빈저 컬렉터

줄여서 PS 컬렉터라고 한다. 마크-카피 알고리즘에 기초하며 여러 스레드를 이용해 병렬로 회수를 진행하는 등, 많은 면에서 파뉴 컬렉터와 닮았다. CMS 같은 컬렉터는 사용자 스레드의 일시 정지 시간을 줄이는데 집중하지만, PS 컬렉터는 처치량을 제어하는 것이 목표다.

처리량 = (사용자 코드 실행 시간) / (사용자 코드 실행 시간 + 가비지 컬렉터 실행 시간)

PS 컬렉터는 처리량을 제어할 수 있는 변수를 제공한다.

–XX:MaxGCPauseMillis : 정지 시간의 최댓값 지정 –XX:GCTimeRatio : 처리량을 직접 지정

정지시간이 줄어버리면, 처리량이 낮아진다. 메모리의 크기를 줄여서 처리 시간 자체를 줄일 수 있는 것이다. 처리량은 정수로 기본값은 99 이다. 애플리케이션 실행 : 처리 실행 시간이 99:1 이라는 의미이다. 이 값이 N 이라면 가비지 컬렉터가 사용자 코드 실행 시간의 1/(1+N) 이상을 소비하게 될 것이다.

시리얼 올드 컬렉터

단일 스레드 컬렉터이며, 마크-컴팩트 알고리즘을 쓴다.

패러렐 올드 컬렉터

멀티스레드를 이용한 병렬 회수를 지원하며 마크-컴팩트 알고리즘을 기초로 구현했다.

CMS 컬렉터

마크-스윕을 사용하며, 사용자 스레드와 동시에 수행한다. CMS 컬렉터의 목적은 가비지 컬렉션에 따른 일시 정지 시간을 최소로 줄이는 것이다. 자바 어플리케이션은 서비스 응답 시간이 중요하기 때문에 시스템의 일시 정지 시간이 짧아야 한다.
동작 방식은 다음 4 단계로 구성되어 있다.

  1. 최초 표시
  2. 동시 표시
  3. 재표시
  4. 동시 쓸기

1,3, 단계는 여전히 ‘스톱 더 월드’ 방식이다. GC 루트와 직접 연결된 객체를 1단계에서 표시한다. 2단계에서 객체 그래프 전체를 탐색하는데, GC 스레드와 사용자 스레드가 동시에 실행된다. 3단계에서 동시 표시 도중에 사용자 스레드가 참조 관계를 변경한 객체를 바로잡는다. 4단계에서 죽었다고 판단한 객체를 쓸어담는다.
동시 회수 능력 덕분에 정지 시간이 매우 짧지만 다음과 같은 단점이 있다.

  1. CMS 는 프로세스 자원에 아주 민감하다.
  2. Floating garbage 를 처리하지 못해서 동시 모드 실패를 유지할 수 있다.
  3. 마크-스윕 알고리즘은 회수 작업 끝에 상당한 파편화를 일으킨다. 파편화의 문제는 큰 객체를 할당하기 어렵다는 것이다. 이 경우엔, 전체 GC 를 수행해야 할 수도 있다.

G1 컬렉터

G1 은 Garbage First(가비지 우선)를 짧게 줄인 표현이다. 부분 회수 라는 컬렉션 설계 아이디어와, 리전을 회수 단위로 하는 메모리 레이아웃 구조를 사용했다. 세대 단위 컬렉션 이론을 활용하며, 연속된 자바 힙을 동일 크기의 여러 독립 리전으로 나눈다. 리전 각각에 역할별 전략을 구분하여 적용할 수 있기에 회수 효율이 극대화된다.
큰 객체를 저장하기 위한 거대 리전도 있다. 리전 용량의 절반보다 큰 객체를 큰 객체로 취급한다. 리전 용량은 –XX:G1HeapRegionSize 매개 변수로 설정되며, 범위는 1mb 에서 32mb 까지이다. 거대 리전은 연속되어 있기에 큰 객체를 저장할 시 있다.
G1 은 등장하기 위해서 다음 문제들을 해결해야만 했다.

  1. 객체들의 리전 간 참조 문제가 있다. 이를 해결하기 위해 기억 집합을 사용햐애 하는데, G1 은 해시 테이블로 각각의 리전들이 기억 집합을 보유하고 있다. ‘내가 가리키는 대상’, ‘나를 가리키는 대상’을 양방향으로 관리하기에 성능은 좋지만 메모리가 많이 필요하다. 다른 컬렉터보다 메모리를 많이 사용하는 이유는 이 때문이다.
  2. 동시 표시 단계 동안 사용자 스레드와 GC 스레드가 서로 간섭해서는 안된다. G1 은 시작 단계 스냅숏을 활용하고, 각 리전을 위한 TAMS 라는 포인터를 사용했다. 이 포인터에는 GC 과정동안 생성되는 새로운 객체가 알당된다. 이 새로운 객체는 회수 대상에서 제외된다.
  3. 신뢰할 수 있는 정지 시간 예측 모델을 구현해야 한다. 사용자가 변수로 설정한 –XX:MaxGCPauseMillis 를 지키기 위해서는 어떻게 해야 할까. 가비지 컬렉션이 이루어지는 동안 리전별 회수 시간, 리전별 기억 집합의 정합성 확인 등 각 단계의 소요 시간을 확인하고, 통계를 분석한다. 이를 통해서 지연 시간을 예측할 수 있다.

정지 시간의 기댓값을 사용자가 설정할 수 있다는 것은 G1 의 큰 장점이다. 이 값을 잘 조율하면 우리는 운영하는 애플리케이션의 이상적인 ‘처리량 대 지연 시간’ 균형점을 찾을 수 있다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.