Java G1 GC와 Humongous Objects and Humongous Allocations

A. 서론
DevOps가 바라본 SpringBoot (+ JVM) [A-1] 작성 이후,
Toss Slash 21, SRE 사례 소개하기 [A-2]를 보면서,
G1 GC Humongous Objects and Humongous Allocation 이슈를 알게 되었습니다.
💡
SpringBoot에서 대량의 매트릭을 수집하면서, G1GC Humongous Objects and Humongous Allocation 관련해서 발생하면서 큰 용량의 메모리 처리에서 Timeout이 발생함
G1 GC Humongous Objects and Humongous Allocation 이전에
G1 GC을 이해하고 싶었고 이를 위해서 CMS GC, Mark Sweep, Reference Counting까지 단계적으로 존재를 알게 되었습니다.
위 이슈를 이해하기 위해 역순으로 짚어나가면서 공부한 기록을 남기고자 합니다.
🚫
이 문서는 "추상화"에 집중한 문서이며, 다양한 참고자료를 교차검증하면서 G1 GC Humongous Objects and Humongous Allocation 이슈를 이해하는데 집중하고자 합니다.
A.0. 최소한의 메모리 상식
대다수 프로그래밍 언어는 필요한 정보를 메모리에 할당*하고 참조*하여 사용합니다.
할당* : 물리 메모리의 특정한 장소에 데이터를 넣는 행위
참조* : 물리 메모리의 특정한 장소를 가리키는 주소지를 바라보는 행위
메모리에 할당된 정보가 사용하지 않는 시점에는 이를 회수*하여 공간을 확보합니다.
회수* : 물리 메모리의 할당*을 해제하고 이 공간은 할당 가능한 공간이 됨
일부 저수준 언어(C/C++)는 개발자가 할당, 회수 과정을 직접적으로 제어해야 합니다.
하지만 대부분의 고수준 언어(Java/Node/Golang)은 GC*에 이를 위임하게 됩니다.
GC* : Garbage Collector가 메모리 할당, 참조, 회수 등의 관리 작업을 대신함
이 과정에서 개발자는 메모리 누수의 위험이 줄어들고 편의성이 증가되지만
GC 작업 및 작업 과정에서 다양한 오버헤드가 발생하여 어플리케이션 성능이 저하됩니다.
A.1. Java Root Space란 무엇인가?
언어마다 다르겠지만 Java에서는 stack, native method, method area 등이 루트 공간에 해당합니다.
A.2. Reference Counting 방식 [A.2-1]
참조 횟수(reference count)가 0인 경우에 회수하는 방식입니다.
참조 횟수는 루트 공간*에서의 객체 참조와 객체 간 참조의 합으로 연산합니다.
하지만 순환 참조(circular reference)가 발생하면, 참조 횟수는 0이 될 수 없습니다.
붉은 영역은 사용하지 않으나 할당된 상태인 메모리 누수(memory leak) 부분입니다.
A.3. Mark & Sweep 방식 [A.2-1]
루트 공간에서의 접근이 불가능(unreachable)한 경우 회수하는 방식입니다.
대상을 찾는 Mark, 대상을 지우는 Sweep, 정돈하는 Compact가 있습니다.
(프로그래밍언어의 GC 구현체에 따라서 Compact는 생략되는 경우도 있습니다.)
Mark : 루트 공간에서 접근 가능한 친구를 탐색
Sweep : 마킹 되지 않은 친구들을 제거
Compaction : 메모리가 흩어져있는 파티셔닝 현상을 정렬함으로써 완화
Mark & Sweep은 메모리 관리 전략(방법) 중 하나입니다.
다양한 프로그래밍 언어*에서 Mark & Sweep을 응용해서 사용하고 있습니다.
💡
Java, Node.js, Golang GC는 모두 Mark & Sweep을 사용한다. Java는 JDK 9 전에는 CMS(Concurrent Mark Sweep)을 사용하다가 현재에는 G1(Garbage First Mark Sweep) 방식을 사용하고 있다. Node.js, Golang은 모두 CMS(Concurrent Mark Sweep)을 사용하고 있으나, 최신 버전에서는 달라질 수 있으므로 확인이 필요하다.
바로 이어서 CMS GC, G1 GC에 대해서 배우기 전에,
Mark & Sweep의 특징과 두가지 스레딩 방식을 알고 넘어가는 것이 좋습니다.
이러한 Mark & Sweep 방식은 2가지 특징이 존재하며,
이 중에서 대기(wait)가 일어나는 현상을 StW(Stop the World)라고 부르며,
StW가 일정 시간(duration)보다 길어지면 서비스 자체에 장애가 발생할 수 있습니다.
의도적으로 GC를 실행시켜야 한다.
GC 실행과 어플리케이션 실행이 병행하며,
GC 실행이 되는 동안 어플리케이션이 대기(wait)하는 현상이 발생한다.
Mark & Sweep 방식은 스레딩 구조에 따라서 2가지로 구분되며,
Java에서는 Parellel GC를 기본값으로 사용하고 있습니다.
Serial GC(signle-thread) : 어플리케이션 대기 시간이 길다.
Parellel GC(multi-thread) : 어플리케이션 대기 시간이 짧다.
💡
Seiral GC는 sigle-thread java + small heap 환경에서 최초 사용 되었으나multi-thread java + large heap 환경으로 인해 Parrel GC가 도입되고 기본 GC로 사용되고 있다.
A.4. CMS GC 방식 (작성 필요)
CMS*(Concurrent Mark Sweep) GC 방식
JDK 9 @deprecated JDK 14 @removed
Initial Mark : GC Root에서 참조하는 객체 마킹 (StW)
Concurrent Mark : 이전 단계에서 참조한 모든 객체를 마킹 (Non StW)
ReMark : 이전 단계에서 참조된 객체를 다시 찾아, 추가 및 참조 해제된 객체를 확정 (StW)
Concurrent Sweep : 도달할 수 없는 객체들을 삭제 (Non StW)
💡
CMS GC 작동 원리에 대해서 간단하게만 알아두는 정도로... 나머지는 전부 추상화
A.5. G1 GC 방식 (작성 필요)
G1 (Garbage Firsrt) GC 방식
G1 GC에서 이루어지는 가비지 컬랙팅은 크게 3가지로 구분됩니다.
Minor GC
Young Generation 내부에서 작동
Eden 및 꽉찬 Survivor에 있는 객체를 텅빈 Survivor로 옮기고 나머지 청소
Promotion
Young Generation 내부에서 작동 시작
Survivor 사이를 오가는 객체들의 age-bit가 일정 수준이 넘어가면 해당 객체를 Old Generation으로 이동
Major GC(Full GC)
Old generation의 공간이 가득 찼을때 Mark Sweep 방식으로 지워짐
A.6. G1 GC Humongous Objects & Humongous Allocation
G1 GC에는 Heap Region이 있고
객체가 Heap Region size의 1/2 이상의 메모리가 필요한 경우
Heap Region에 바로 할당할 수 없고 Old Generation 영역에 G1 GC Humongous Objects & Humongous Allocation
이렇게 Humongous Objects들이 대량으로 생성되면서,
Old Generation 공간이 부족해져서 Major GC(Full GC)가 발생하고
결과적으로 Timeout 문제까지 연결될 수 있습니다.
객체의 크기 줄이기
Heap Region Size 늘리기
Stream 방식 활용
A.7. G1 GC 분석하기
💡
G1 GC 분석을 경험하고 나서 그 경험을 살려서 이 글을 이어서 쓸 것 같습니다.
기본값 설정 확인하기
java -XX:+PrintCommandLineFlags -version
-XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=9 -XX:InitialHeapSize=268435456 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=4294967296 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedOops -XX:+UseG1GC
Eden, S0, S1, Old Menegartion 횟수 및 시간 확인하기
jstat -gcutil -t 8844 1000 10
Eden, S0, S1, Old Menegartion 용량 확인하기
jstat -gccapcity -t 8844 1000 10