이전 시간에는 JDK 8 버전까지 사용했었던 GC들에 알아보는 시간을 가졌습니다. 이번 시간에는 JDK 9 버전 이후로 사용되는 GC에 대해 공부해보는 시간을 가지려고 합니다.
참고: Garbage Collection(GC) - (1)
JDK 8이하 버전의 GC에서는 기존의 JVM에서 Heap 영역을 물리적으로 분리해 Minor GC와 Major GC의 역할이 명확했습니다. 하지만 JDK 9부터 도입된 GC들은 기존 GC들의 문제를 해결하기 위해 다음과 같은 목표들을 설정했습니다.
지금부터 새로 생긴 GC들의 동작 과정을 보면서 각 GC에 대한 이해도를 높이고 새롭게 나온 GC들의 성능이 왜 뛰어난지에 대해 알아보는 시간을 가져보겠습니다.
G1 GC는 JDK 9부터 default GC로 선정되었으며 이는 성능상으로 비교했을 때 타 GC 보다 뛰어난 성능을 보인다는 것을 알 수 있습니다. 그럼 G1 GC가 어떻게 다른 GC들보다 성능이 우수한지 알아보겠습니다. (JDK 9 이전 default GC : Parallel GC)
기존의 Heap 영역을 Young/Old 분리하여 진행하던 GC 방식과 완전히 다르게 진행됩니다.
G1 GC는 CMS GC를 대체하기 위해 고안된 GC이며 대용량의 메모리가 있는 멀티 프로세서 시스템을 위해 제작되었습니다. 빠른 처리 속도를 지원하면서 STW를 최소화합니다. CMS GC보다 효율적으로 동시에 Application과 GC를 진행할 수 있고, 메모리 Compaction 과정까지 지원합니다.
G1인 이유는 Garbage가 존재하는 Heap 영역의 region을 먼저 회수하다 해서 불린 이유이며. 빈 공간 확보를 타 GC보다 더 빨리하여 조기 승격(Young -> Old)를 방지하며 Old generation을 한가하게 만들 수 있는 이점이 존재합니다.
기존 GC를 요약해보면 다음과 같은 영역으로 이루어져 있습니다.
G1 GC는 해당 영역들이 하나의 큰 메모리 영역을 region
으로 분리해 각각의 영역에 논리적인 개념을 정의합니다.
region
영역의 일정 부분을 채울 정도(1/2, 1/3)로 클 경우 해당 영역으로 이동한다G1 GC는 기존의 Young/Old와 같이 구분된 영역이 아닌 일정 크기의 논리적 단위인 region으로 구분해 각각의 region에 개념적으로 각각의 영역을 정의합니다.
G1 GC는 위 그림처럼 Young-only Phase와 Space Reclamation Phase를 반복한다. 사이클 중 모든 원은 STW가 발생한 것을 나타낸 것이고, 원의 크기에 따라 STW 소요 시간이 달라진다고 생각하면 좋을 것 같습니다.
파란 원은 Minor GC(Evacuation Pause)가 진행함에 따라 STW가 발생한 것이고, 주황 원은 Major GC(Concurrent Cycle)이 진행하면서 객체를 마킹 및 기타 과정을 하기 위해 STW가 발생한 것이고, 빨간 원은 Mixed GC를 진행함에 따라 STW가 발생한 것입니다.
G1 GC의 수행 과정은 크게 세가지로 나누어집니다.
G1 GC 동작 과정
이제 각 동작이 어떻게 수행되는지 알아보겠습니다.
연속되지 않은 메모리 공간에 Young Generation이 Region 단위로 메모리에 할당되어 있습니다.
GC가 실행되면 Young Generation 영역에서 살아있는 객체들은 Survivor Region이나 Old Generation 영역으로 이동시키며 압축을 진행합니다.
최종적으로 이동을 마치면 기존에 Eden 영역에 생성된 객체들 중 살아남은 객체들은 Survivor region으로 이동되거나 Old Generation 영역으로 이동되며 압축하여 다음과 같이 적은 용량을 사용합니다.
Initial Mark 단계에서는 Old Region에 존재하는 객체들이 참조하는 Survivor Region이 있는지 파악해 마킹하는 단계입니다.
Initial Mark 단계가 완료되면 Initial Mark 단계에서 마킹된 Survivor Region에서 Old Region에 대해 참조하고 있는 객체를 마킹합니다. 해당 내용은 Minor GC가 발생하기 이전에 동작을 완료합니다.
Concurrent Marking 단계에서는 Old Generation 내에 생존해 있는 모든 객체를 마킹합니다. 마킹을 통해 참조하지 않는 객체들을 확인합니다.
Remark 단계에서는 앞서 진행한 Initial Marking 과정에서 확인된 참조되지 않는 객체들을 회수합니다.
Copying/Cleanup 단계에서는 STW가 발생하며, 살아있는 객체의 비율이 낮은 영역 순으로 순차적으로 수거합니다.
Major GC가 끝난 이후, 살아있는 객체가 새로운 Region으로 압축이 되어 이동하니 보다 더 효율적으로 이용이 가능해집니다.
Mixed GC는 Young 영역과 Old 영역의 Garbage를 수집합니다. 한 번에 Old 영역의 Garbage를 수집하는 것은 비용이 크므로 Mixed GC는 기본적으로 8회 수행합니다.
Mixed GC는 Minor GC에서 수행하는 단계와 동일하지만, 추가로 Old 영역의 Garbage를 수집합니다. 즉, Mixed GC는 Minor GC와 Old 영역의 GC를 혼합한 과정이라고 할 수 있습니다.
Shenandoah GC는 '큰 GC 작업을 적은 횟수로 수행하는 것보다 작은 GC 작업을 여러분 수행하는게 더 좋다'는 개념을 적용해 만들어진 GC 입니다.
CMS가 가진 메모리 단편화 이슈와 G1 GC가 가진 pause 이슈를 해결하였으며 heap 사이즈에 영향을 받지 않고 일정한 pause 시간이 소요됩니다.
Root Set을 스캔하여 참조하고 있는 객체를 찾아 마킹하는 단계
애플리케이션 실행 도중에 현재 살아있는 객체에 대해 마킹하는 단계.
모든 대기열을 비우고 Root set을 다시 스캔하며 Concurrent Marking
단계를 종료합니다.
또한 복사할 빈 영역을 확인하여 초기화 시키며 다음 단계를 대기하는 단계
garbage 영역에서 Concurrent Mark
단계 이후 더 이상 현재 살아있지 않은 객체들을 즉각적으로 회수
개체를 다른 region으로 복사하는 과정
참조 업데이트 과정을 초기화하는 단계. 다음 단계를 위해 준비하는 단계
Heap 을 선형으로 스캔하여 Concurrent Evacuation
동작 도중 이동된 객체들에 대해 참조를 업데이트하는 과정
Root Set을 업데이트 하여 객체 참조 관계를 다시 업데이트 합니다. 이 때, 마지막으로 STW가 발생
모든 과정이 완료된 후, 참조가 없는 객체들을 회수 진행
JDK 11부터 Ellipson GC라는 No-Op Garbage Collector를 도입해 가능한 가장 낮은 GC 오버헤드를 약속하는 GC를 선보였습니다.
Ellipson GC 작동하지 않는 가비지 수집기입니다. 무슨 의미인지 모르시겠다고요??
Ellipson GC는 어플리케이션에서 사용 가능한 힙이 충분하다고 알고 있는 경우 굳이 JVM의 리소스를 사용해 GC 작업을 실행하는 것을 원치 않을 때 사용하는 GC입니다.
어플리케이션이 사용할 Heap 용량이 충분하지 않을 경우(사용 가능한 Heap 공간이 없을 경우), Ellipson GC는 지금까지 본 GC들과 달리 OutOfMemoryException
을 터뜨립니다. 왜냐면 메모리 회수 코드가 구현되지 않았기 때문이죠.
따라서 해당 GC는 어플리케이션이 사용 가능한 Heap이 충분하다는 것을 아는 경우 굳이 GC를 JVM이 실행하지 않도록 하는 GC입니다.
기존 GC들은 STW로 인해 어플리케이션의 성능에 영향을 주고 있습니다. 이러한 문제를 해결하기 위해 ZGC는 다음과 같은 목표로 태어났습니다. ZGC는 JDK 11 이후 버전부터 이용이 가능합니다.
✔ ZGC : a good fit for server applications, where large heaps are common, and fast application response times are a requirement.
목표
G1보다 낮은 Latency를 가지고 G1에 뒤쳐지지 않을 처리량을 가지는 GC를 개발한다.
주요 원리
colored pointers, load barriers를 함께 사용하여 GC를 위한 기능/최적화 기반을 마련합니다.
ZGC에서는 메모리를 ZPages라 불리는 영역으로 나누고 동적 사이즈로 2MB의 배수가 동적으로 생성 및 삭제될 수 있습니다.
대부분의 GC 같은 경우, GC 가 발생시 영역이 변경되며 기존 객체가 새로운 빈 공간을 찾아 재할당 되는 과정을 거치고 있습니다. 하지만 ZGC 같은 경우는 이러한 빈 공간을 탐색하는 시간이 너무 오래 걸리기 때문에 새로운 영역을 할당해 그 곳에 객체를 이동시키는 전략을 이용합니다. 이를 Compact라 부릅니다.
하지만 이 때 기존 객체와 새로운 영역에 할당된 새로운 객체간 값의 동기화가 정상적으로 이루어지지 않을 수 있다는 문제가 존재합니다.
다음은 하나의 객체가 GC를 통해 재할당 되는 과정을 보여줍니다.
이를 보완하기 위해 ZGC는 다음과 같은 세가지 전략을 이용하고 있습니다.
JIT에 의해 Load Barrier가 주입되는데, 이를 통해 ZGC는 어플리케이션 진행과 동시에 압축을 수행할 수 있습니다. 만약 어플리케이션 쓰레드가 힙 메모리에 접근할 경우 다음 두 가지를 진행합니다.
ZGC는 이 colered pointer를 이용해 해당 color가 bad color인지 체크하고 bad color일 경우 객체를 상황에 따라 mark/relocate/remapping
을 진행합니다.
이제 ZGC에서 이용하는 세 가지의 새로운 개념에 대해 알아보았습니다.
그럼 ZGC는 어떻게 Compact의 문제점을 다음 세 가지 전략을 추가 개선하였을까요? 과정은 다음과 같습니다.
multi mapping
을 진행합니다.(color mark 간 overhead를 방지하기 위해 사용)relocation
및 remapping
여부를 확인하며 실제 객체와의 동기화 작업을 지속적으로 수행합니다.remapping
된 실제 객체를 제외한 나머지 메모리를 회수합니다.ZGC의 GC 순서는 크게 2가지, 작게는 9가지로 나뉘는 순서를 가지고 있습니다.
Pause Mark Start -> Concurrent Mark -> Pause Mark End
-> Concurrent Process Non-Strong References -> Concurrent Reset Relocation Set
-> Pause Verify -> Concurrent Select Relocation Set -> Pause Relocate Start Concurrent Reloate
대충 순서도 확인했으니 이제 어떻게 동작하는지 확인해보겠습니다.
GC Thread
Application Thread
JDK 버전별 각 GC의 성능을 비교한 자료입니다. 자세한 내용은 GC progress from JDK 8 to JDK 17 에서 참고하면 좋을 것 같습니다. 현재 버전별 default GC는 다음과 같습니다.
JDK 버전별 GC
다양한 GC가 존재하지만 현재 보편적으로 사용되는 jdk 8은 Parallel GC를 default로, jdk 11은 G1 GC를 default로 사용하고 있습니다.
이러한 점을 고려해 버전 업그레이드시 G1 GC에 대한 이해도를 보다 더 높여야 겠다는 생각을 가지게 되었습니다.
G1 GC에 대해 좀 더 알아보고 싶은 분들은 다음 링크를 추가로 확인하면 좋을 것 같습니다.
하지만 다음과 같은 상황에서는 해당 GC 도입을 고려해볼만 하다고 생각합니다.
상황별 GC 선택
JEP 318: Epsilon: A No-Op Garbage Collector - Experimental
No-Op Experimental Garbage Collector
JEP 363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector