본 글은 ORACLE의 Getting Started with the G1 Garbage Collector의 일부를 번역한 글입니다. 구형 Garbage Collector들에 대해 먼저 알아보고 읽으시는 것을 추천드립니다.
구형 가비지 콜렉터(Serial, Parallel, CMS...)의 힙 구조는 Young/Old/Permanent Generation 세 구역으로 나누어 진다.
모든 메모리 객체는 이 세 구역 내에 포함된다.
그러나 G1 콜렉터는 다른 접근 방식을 취한다.
G1의 힙은 동일한 사이즈의 Region 으로 나뉜다. 그것들은 가상 메모리의 연속적인 범위를 차지하고 있다.
Region 또한 Eden, Survivor, Old 영역으로 나뉘며, 같은 종류의 Region은 같은 역할을 한다. 그러나 각 영역의 전체 크기에는 고정된 사이즈가 없다. 그래서 굉장히 유동적인 메모리 사용이 가능해진다.
가비지 콜렉팅이 진행될 때, G1 콜렉터는 CMS 콜렉터와 비슷한 방식으로 작동한다. G1 콜렉터는 힙에 존재하는 객체의 생존 여부를 판단하기 위해 concurrent한 전역 마킹 단계(global marking phase)를 거친다. 마킹이 완료되면, G1 콜렉터는 어떤 region이 대부분 마킹이 안 되어있는 (많은 free space를 창출할) 상태인지 알게 되고, 그런 region들부터 먼저 콜렉팅한다. 그래서 이 콜렉팅이 Garbage-First라고 불리는 것이다.
이름에 걸맞게, G1 GC는 회수될 객체(garbage)로 가득찬 힙 공간을 콜렉팅하고 compaction 하는 것에 집중한다. 그리고 유저가 정의한 목표 정지 시간을 만족시키기 위해 일시정지 예측 모델을 사용하고, 명시된 목표 정지 시간을 기준으로 콜렉팅할 region의 개수를 설정한다.
G1 콜렉터에 의해 콜렉팅 대상으로 식별된 region들은 evacuation(대피) 방식으로 가비지 콜렉팅된다. G1 콜렉터는 하나 이상의 region에서 생존된 객체를 복사하여 단 하나의 region에 붙여넣는다. 이 과정에서 메모리 compaction 및 공간 창출이 일어난다. evacuation은 멀티프로세싱 환경에서 병렬적으로 수행되는데, 그 덕에 GC를 위한 일시정지 시간을 줄이고 처리량을 높인다. 그래서 각 G1 GC는, 단편화를 줄이기 위해 연속적으로 작동하고, 유저 정의 중지 시간을 준수한다. G1은 구형 콜렉팅 방식보다 앞선다고 할 수 있는데, 왜냐하면 CMS는 compaction을 하지 않고, Parallel GC는 거대한 일시정지 시간을 소모하는 전체 compaction만을 수행하기 때문이다.
하지만 G1이 실시간 대응 콜렉터는 아니라는 것에 유의해야 한다. G1은 높은 확률로 목표 일시정지 시간을 충족시키지만, 항상 그렇지는 않다. G1은 과거에 수행된 콜렉팅에 기반하여, 유저가 정의한 일시정지 시간동안 어느정도의 region이 콜렉팅 될 수 있을지 예측한다. 그래서 콜렉터는 region을 콜렉팅하는 데에 드는 비용에 대한 합리적이고 정확한 모델을 가지고 있다. 그 모델로 정지 시간동안 어느 정도의 region이 콜렉팅 될 수 있을지 판단한다.
G1 가비지 콜렉팅 기능을 단계 별로 알아보자.
힙이 고정된 사이즈의 region으로 쪼개진다.
Region의 크기는 시작 시 JVM에 의해 정의된다. 대략 2천 개의 region을 1~32MB의 크기로 설정한다.
Region은 Eden, Survivor, Old generation을 의미하는 영역으로 맵핑된다.
생존 객체들은 다른 region으로 evacuated(대피, 즉 복사)된다. Region은 stop-the-world를 동반하거나 동반하지 않고 병렬적으로 콜렉팅되도록 설계되어 있다.
그림에서 보이듯, 각 region은 Eden, Survivor, Old generation으로 할당될 수 있다. 그리고 4번째 타입인 Humongous region이 존재한다. 이 region은 기본 region의 크기의 50% 이상을 차지하는 객체를 담기 위해 설계된 region으로, 연속된 공간을 차지한다.
힙은 대략 2000개의 region으로 쪼개진다. 크기는 최소 1MB, 최대 32MB 이며, 아래 그림에서 파란색은 Old, 초록색은 Young generation을 의미한다.
G1에서는 구형 콜렉터처럼 각 region이 연속적일 필요가 없음을 인지하자.
마킹된 생존 객체는 Survivor region으로 옮겨진다. 생존 임계값(aging threshold)이 충족되는 경우, Old generation region으로 옮겨지도록 promote 된다.
위 과정에서는 stop-the-world가 일어난다. 다음 Young GC를 위해 Eden, Survivor 영역의 크기가 계산되고, 소모 비용 정보는 크기 계산을 돕기 위해 보관된다. 목표 일시정지 시간과 같은 정보들도 계산에 활용된다.
이 접근 방식은 region의 크기를 매우 쉽게 조정하게 해준다. 필요한 대로 줄이거나, 키울 수 있다.
생존 객체는 Survivor 혹은 Old generation region으로 옮겨진다.
지금까지의 Young GC 과정을 요약해보자.
G1의 Old GC 과정은 아래와 같다. (Young GC도 일부 포함)
(STW는 stop-the-world가 포함된 과정)
단계 | 설명 |
---|---|
(1) Initial Mark (STW) | Old 영역의 객체를 참조할 수도 있는 region을 Survivor에서 마킹 |
(2) Root Region Scanning | Old 영역에게 참조되는 객체를 Survivor region에서 탐색함. 앱이 작동하는 사이에도 일어나고, Young GC가 시작되기 전에 완료되어야 함. |
(3) Concurrent Marking | 전체 힙에서 생존 객체를 탐색함. 앱이 작동하는 사이에도 일어나고, Young GC에 의해서 중단될 수도 있음. |
(4) Remark (STW) | 힙에서 생존 객체를 마킹하는 것을 완료시킴. (CMS 콜렉터보다 빠르게 작동하는 것을 가능케하는 SATB 알고리즘을 사용함) |
(5) Cleanup (STW & Concurrent) | 생존 객체를 계산하고, region을 회수. 비게 되는 영역을 초기화하고 free space로 변경함. |
(*) Copying (STW) | 사용 가능 region으로 생존 객체를 evacuate함. 이것은 GC Pause 라고 로그가 남겨져 있는 Young 혹은 Old generation에 적용된다. |
생존 객체에 대한 초기 마킹은 Young GC에서 이루어진다.
빈 region이 발견되면, Remark phase에서 즉각적으로 제거된다. 그리고 생존 여부를 결정하는 소모 비용 정보도 계산된다.
빈 region이 제거되고, 회수된다. Region의 생존 여부가 계산된다.
G1은 가장 낮은 생명력(liveness)을 가진 region을 선택하고, 이 region들은 가장 빨리 콜렉팅 될 수 있다. Young GC에 의해 해당 region들은 동시에 콜렉팅되기 때문에, Young/Old generation이 동시에 콜렉팅된다.
선택된 region들은 콜렉팅되고 compaction이 이루어진다.
지금까지의 Old GC 과정의 키포인트를 정리해보겠다.