[Java] G1 GC 에 대해

땡글이·2023년 4월 21일
2

Java

목록 보기
5/6

GC(Garbage Collector)는 자바의 메모리를 자동으로 관리해주기 위해 JVM의 실행엔진(Execution Engine)에 위치한 소프트웨어입니다.

그런데 GC는 메모리를 관리하기 위해 CPU 자원을 사용하므로, GC가 수행될 때는 애플리케이션의 일시적인 정지(stop-the-world)가 발생할 수 있습니다.

또한, stop-the-world 시간과 처리율(Throughput) 사이에는 상충관계가 발생할 수 있습니다. 즉, stop-the-world가 짧으면 GC 스레드가 차지하는 메모리가 커져 어플리케이션의 처리율이 좋지 않아지고, stop-the-world가 길면 GC 스레드가 차지하는 메모리가 작아서, 어플리케이션의 처리율이 좋아지는 것입니다.

  • 처리율 증가 → stop-the-world 증가
    • 처리율을 증가시키기 위해서 Heap(대부분 Young) 영역의 크기를 키우면, 스캔하고 관리해야하는 메모리의 크기가 커져 GC 작업 시간(stop-the-world)이 증가하게 된다.
  • stop-the-world 감소 → 처리율 감소
    • stop-the-world를 최소화시키면, 짧은 GC가 자주 발생하게 되며 처리율이 떨어지게 된다.

여기서, 처리율은 Garbage Collection 에 소요되지 않은 총 시간의 백분율입니다.
공식문서 : "Throughput is the percentage of total time not spent in garbage collection considered over long periods of time"

G1 GC의 개념

공식문서에 따르면, G1(Garbage-First) GC 도 다른 GC와 마찬가지로 Heap 영역을 Young 영역과 Old 영역으로 나누어 관리합니다. 그렇게함으로써 메모리 확보를 Young 영역에 집중해서 효율적으로 구현할 수 있고, Old 영역에서 간헐적으로 메모리를 확보함으로써 stop-the-world 시간을 최소화시킵니다.

Young 영역과 Old 영역으로 나누는 이유는, weak generational hypothesis 가정에 의한 것입니다. 자세한 내용은 해당 글을 참고해주세요.

G1 GC는 일부 작업은 처리율을 향상시키기 위해 stop-the-world를 발생시키고, 전체 Heap 영역을 대상으로 한 marking 작업과 같이 시간이 오래 걸리는 작업은 어플리케이션과 병렬로 처리합니다.

G1 GCstop-the-world 를 짧게 유지하기 위해 G1은 메모리 확보(space reclamation)을 단계적으로 그리고 병렬로 점진적으로 수행합니다. 즉, 한 번에 모든 메모리를 회수하는 것이 아니라 점진적으로 회수한다는 의미입니다.

G1 GC는 이전 GC 실행 결과와 Heap 상태를 기반으로 자동으로 IHOP(Initiating Heap Occupancy Percent)를 수정합니다. G1 GCIHOP 를 기준으로 GC가 동작하기 때문에, IHOP 를 어플리케이션의 상태에 맞게 최적으로 수정해나간다면 G1 GC가 올바른 시점에서 Minor GCMixed GC를 실행하고, 얼마나 많은 Heap 메모리를 할당해야 하는지 등을 최적으로 선택하게 됩니다.

G1 GC는 Garbage-First Collector 라고도 불리는데, 이것은 garbage(unreachable objects)들로만 이루어진 region 부터 공간을 확보한다는 의미에서 붙여진 이름입니다.
즉, 위의 내용과 비슷한 맥락으로 G1 GC는 항상 효율적으로 GC를 실행한다는 의미를 가집니다.

G1 GC를 적용하기에 좋은 상황들

G1(Garbage-First) GC 는 대용량 메모리가 있는 다중 프로세서 시스템을 대상으로 합니다. 구성할 필요가 거의 없이 높은 처리량을 달성하면서 높은 확률로 일시 중지 시간(stop-the-world) 목표를 충족하려고 시도합니다.

G1 GC은 다음과 같은 기능을 갖춘 현재 대상 애플리케이션 및 환경을 사용하여 대기 시간과 처리량 간에 최상의 균형을 제공하는 것을 목표로 합니다.

공식문서에서는 G1 GC 는 다음과 같은 상황들에서 사용되는 것이 좋다고 얘기합니다.

  • 힙 크기는 최대 수십 GB 이상이며 Java 힙의 50% 이상이 live object로 채워질 때
    • live data : 실제 어플리케이션에서 사용되기 위해 Root space로부터 참조되는 object
  • 시간이 지남에 따라, 객체 allocation 및 promotion 비율이 크게 달라질 때
    • allocation : Heap 영역에 객체가 할당되는 것
    • promotion : Heap 영역에서 GC가 동작해도 살아남아 유지되어 다른 메모리로 복사되는 것
  • 수백 밀리초보다 길지 않은 예측 가능한 일시 정지 시간(stop-the-world)를 목표로 할 때
    • 파라미터로 주어진 일시 정지 시간을 넘지 않으려 노력하지만, 항상 지켜지진 않는다.

G1 GC의 특징

G1 GC는 어떻게 동작되고, 어떤 특징들을 가지는지 확인해보겠습니다.

Heap Layout


위의 그림처럼 G1 GC는 힙 영역을 같은 크기의 region으로 나누고 관리합니다. 나눠져서 관리되는 region은 메모리 회수와 할당의 단위가 됩니다. 위의 그림에서 region 마다 다른 색깔을 띄고 있는데 각각은 다음과 같은 의미를 띕니다.

  • 빨강색 : Young 영역(Eden)
  • 빨강색 + S : Young 영역 (Survivor)
  • 파랑색 : Old 영역
  • 파랑색 + H : Old 영역 (여러 개의 region이 필요한 사이즈가 큰 객체(humongous object)
  • 회색 : 비어있는 부분들. 즉, 언제든지 할당가능한 영역

하나의 region 크기에도 저장되지 못할 정도로 큰 객체들(humongous objects)은 처음 객체가 저장될 때에도 Young 영역이 아닌, Old 영역으로 바로 저장됩니다.
공식 문서 : "...An application always allocates into a young generation, that is, eden regions, with the exception of humongous objects that are directly allocated as belonging to the old generation."

Garbage Collection Cycle

위의 그림은 G1 GC가 메모리를 회수할 때의 2가지 Garbage Collection 단계를 나타낸 그림입니다.

  • Young-only
  • Space Reclamation

간단히 말하면, Young-only 단계는 Old 영역으로 객체를 사용가능한 메모리에 할당시켜주는 단계이고, Space Reclamation 단계는 G1 GC가 Young 영역에 대해서 GC를 처리하는 것 이외에도 Old 영역에서의 GC를 동작시킴으로써 메모리를 회수하는 단계입니다.

Young-only 단계

Young-only 단계에서는 Young 영역에 저장된 객체들 중 Root space로부터 참조되는 객체들을 Old 영역으로 promotion 하게 됩니다. 또한, 메모리가 회수될 객체들은 marking 해두고, Space Reclamation 단계를 실행할지 여부를 결정짓게 됩니다.

Young-only 단계와 Space Reclamation 단계 사이의 전환은 Old Generation 점유가 특정 임계값인 Initiating Heap Occupancy Percent 임계값에 도달할 때 시작됩니다. 이 때 G1 GC가 동작하게 됩니다.

  • IHOP(Initiating Heap Occupancy Percent) 임계값이란, 힙 영역이 일정 수준 이상으로 채워졌을 때, 힙 영역에서 Mixed GC를 실행시키기 위한 threshold 값입니다. 디폴트 값은 45% 입니다.

Mixed GC ?

G1 GC에 대한 설명에서 Mixed GC라는 단어가 나왔습니다. 이는 Young 영역과 Old 영역을 구분짓지 않고 GC가 동작한다고 해서 붙은 이름입니다. 아래 설명을 보겠습니다.

  • Major GC : Old 영역에서 수행되며, 전체 Heap을 스캔하여 더 이상 참조되지 않는 객체를 제거합니다. Major GC는 일반적으로 실행 시간이 길어질 수 있고, 어플리케이션의 일시 중지를 초래할 수 있으므로 최대한 자주 수행하지 않는 것이 좋습니다.
  • Mixed GC : Young 영역과 Old 영역을 동시에 수집하며, 일부 라이브 객체만을 수집하여 Young 영역으로 이동하거나 Old 영역에서 제거합니다. 따라서 일시 중지 시간이 짧고, 최적화된 방식으로 객체를 처리하여 성능을 향상시킬 수 있습니다.

즉, Mixed GC 는 일부를 대상으로 GC가 동작하는 것이고, Major GC(Full GC)는 Heap 전체를 대상으로 GC가 동작하는 것입니다. 물론 G1 GC에서도 다른 GC들과 같이 Major GC(Full GC) 가 동작하지만, Mixed GC도 동작함으로써 stop-the-world 시간을 최소화시키고자 합니다.

Concurrent Start

앞에서 G1 GC는 Old 영역에서의 점유가 특정 임계값인 IHO(Initiating Heap Occupancy) 에 도달할 때 동작합니다. Concurrent Start young collection 은 일반 젊은 수집을 수행하는 것 외에도 마킹 프로세스를 시작합니다. Marking 은 다음의 Space Reclamation 단계에서 보관할 객체를 특정하기 위해 Old 영역에서 현재 reachable(라이브)한 모든 객체를 결정합니다.

단, Marking 이 완전히 완료되지 않은 상태에서도 Normal Young Collection 이 발생할 수 있습니다. Marking 은 다음의 두 가지 특별한 stop-the-world 발생한 뒤, 완료됩니다.

  • Remark
  • Cleanup

Remark

Remark 단계에서는 Marking 자체를 마무리하고, 전역 참조 처리클래스 언로드를 수행하고 완전히 빈 영역을 회수하고 내부 데이터 구조를 정리(compact)합니다.

전역 참조 처리 = Root space로부터 unreachable 한 object GC
클래스 언로드 = 클래스로더가 Metaspace 영역으로 로딩해둔 클래스 관련 메타데이터를 GC

그리고 G1 GC는 Remark 단계와 Cleanup 단계 사이에서 나중에 Old 영역에서 여유 공간을 동시에 회수할 수 있도록, 정보(information)을 계산합니다. 이는 Cleanup 단계에서 완료됩니다.

정보(information)을 계산한다?
공식문서에서는 "G1 calculates information" 라고 나와있는데, 이는 G1 GC의 동작방식을 이해하면 간단하다.
G1 GC는 이전 GC의 동작과 Heap 영역의 상태를 고려해서 IHOP 값을 수정하며 stop-the-world와 처리율 간의 밸런스를 점진적으로 맞춰나갑니다. 이를 Adaptive IHOP 라고 부릅니다.
그렇기에 G1 GC는 동작하면서 여러 정보들을 업데이트하고, 동적으로 IHOP 값을 수정해나갑니다.

Cleanup

이 단계에서는, 앞서 말한 것처럼 이전 GC의 동작과 Heap 영역의 상태를 고려해 정보(information)을 업데이트하고,이 단계 뒤에 Space Reclamation 단계가 실행될 것인지를 결정합니다. 지워야될 객체가 없다면 Space Reclamation 단계를 생략할 것이고, 만약 지워야될 객체가 있어서 Space Reclamation 단계가 동작한다면 Young-only 단계는 Single Prepare Mixed Young Collection 으로 전환됩니다.

Space Reclamation 단계

G1 GC의 Space Reclamation 단계에서는 Young 영역과 Old 영역을 구분짓지 않고, 힙 영역에 저장되어 있는 객체들을 상대로도 메모리를 회수해가는 과정입니다.

다만, Space Reclamation 단계에서 Old 영역에 여유 공간이 충분하다면 지울 객체가 있더라도 더이상 메모리를 회수하지 않고, 종료됩니다.

그리고 앞에서 G1 GC는 병렬로 어플리케이션과 같이 동작하며 객체들의 참조 여부를 체크한다고 했습니다. 그렇기에 G1 GC는 어플리케이션이 동작하는 도중에 만약 여유 공간이 충분하지 않다면, Stop-the-world를 발생시키고, Major GC(Full GC)를 발생시켜 메모리를 압축시키고 여유 공간을 확보합니다.

  • 즉, G1 GCMixed GC 로 메모리를 최적화하다가, GC가 동작함에도 여유 공간이 부족하면, Major GC(Full GC)가 동작함으로써 여유 공간을 확보한다.

G1 GC의 디테일한 특징들

위에서는 G1 GC의 특징들에 대해 포괄적으로 알아봤다면 이제는 조금 디테일하게 알아보고자 한다.

Java Heap Sizing

G1 GC는 아래의 파라미터 값들을 고려해 Heap을 리사이징합니다.

  • -XX:InitialHeapSize : 최소 Heap 사이즈
  • -XX:MaxHeapSize : 최대 Heap 사이즈
  • -XX:MinHeapFreeRatio : 최소 Heap 사이즈 비율
    • ex) Heap 영역에서 사용 가능한 공간이 40% 보다 낮아지면, Generation의 크기를 키워서(비어있는 영역을 새로운 Young/Old 영역으로 할당), 40% 이상을 유지한다.
  • -XX:MaxHeapFreeRatio : 최대 Heap 사이즈 비율
    • ex) Heap 영역에서 사용가능한 공간이 70% 보다 커지면, Generation의 크기를 줄여서(비어있는 Young/Old 영역을 release), 70% 이하를 유지한다.

Adaptive IHOP (Initiating Heap Occupancy Percent)

IHOP란, Old 영역에서 할당되어 사용 중인 percentage를 의미합니다. 즉, Old 영역 중 40% 가 객체가 저장되어 있다면, IHOP는 40이 되는 것입니다.

  • 실제 default IHOP는 45입니다.

위에서도 언급했듯이, IHOP(Initiating Heap Occupancy Percent)는 Young-only 단계의 Concurrent Start(객체 initial marking) 를 trigger해주는 임계값입니다.

그리고 이것 또한 언급했던 내용입니다. G1 GC는 marking 하는 동안, marking에 소요되는 시간과 Old 영역에서 할당되는 메모리 양을 관찰하여 최적의 IHOP를 자동으로 결정합니다. 이를 Adaptive IHOP 라고 합니다.

Adaptive IHOP 기능은 -XX:G1UseAdaptiveIHOP 옵션으로 on/off가 가능합니다.

  • on : -XX:+G1UseAdaptiveIHOP
  • off : -XX:-G1UseAdaptiveIHOP

즉, Adaptive IHOP 기능을 사용하고 있다면, -XX:InitiatingHeapOccupancyPercent 값은 관찰 데이터가 부족할 때에만 유효한 IHOP 값이 되는 것입니다.

다만, Adaptvie IHOP 기능을 끈다면, -XX:InitiatingHeapOccupancyPercent을 주면 계속 해당 IHOP 값을 유지하며, Old 영역의 점유율이 해당 값을 넘기게 되면 Concurrent Start 단계가 trigger 됩니다.

IHOP 값은 어떤 기준으로 바뀌는가?

예를 들어, Old 영역에도 객체에게 할당할 메모리가 없어서 Full GC(Major GC)가 발생했다고 가정해보겠습니다.
그렇다면, Adaptive IHOP 기능이 켜져있다면, IHOP 값은 내려가게 됩니다.
왜? IHOP 값이 내려가게 되면 조금 더 빨리 marking 작업이 이뤄지고 release 작업이 이뤄질 것입니다.

G1 GC는 GC가 동작하는 과정에서 할당할 메모리 공간이 충분하다면, Full GC(Major GC)가 발생하지 않고, Minor GCMixed GC로 동작합니다.

  • Full GC는 큰 Old 영역을 대상으로 GC가 동작하기에 오랜 시간이 소모되는 작업이라 어플리케이션의 성능에 큰 저하를 줍니다.
  • 다만, Mixed GC는 Young 영역과 Old 영역을 구분짓지 않고 GC가 동작하지만, 모든 영역을 스캔하는 것이 아니라 일부의 객체들만 수집해서 메모리를 회수해가기에 Full GC에 비해 시간이 덜 소모됩니다.
  • Minor GC는 Young 영역에서 발생하는 GC라서 당연히 관리하는 메모리 크기가 작아 가장 빠른 시간 내에 완료되는 GC입니다.

그렇기에 Adaptive IHOP 또한 그 점을 고려해서 Full GC가 발생하지 않도록 계속 IHOP 값을 수정해나갑니다.

Marking (feat. SATB)

G1 GC의 MarkingSATB(Snapshot-At-The-Beginning) 알고리즘을 통해 구현되어 있습니다.

SATB 알고리즘은 marking cycle이 시작할 때, Heap 메모리에 있는 살아 있는 객체의 set을 logical snapshot으로 저장합니다.

  • logical snapshot은 marking cycle 시작 시점에 기존의 Heap 상태를 복사한 것으로, 이후에 변경되는 객체의 정보를 포함하지 않습니다.

SATB 알고리즘은 새로 추가된 객체나 수정된 객체를 추적하기 위해 어플리케이션 스레드가 write 하는 모든 포인터(write barrier)를 가로채어 처리합니다. 이때, SATB 알고리즘remembered set이라는 자료구조를 사용합니다.

  • remembered set은 어플리케이션 스레드가 write 하는 포인터를 가지고 있는데, 이 포인터가 가리키는 객체가 heap에서 다른 영역으로 이동하거나, 아예 사라져도 그 포인터가 가리키는 객체를 살아 있는 객체로 추적할 수 있게 합니다.

즉, SATB 알고리즘은 logical snapshot의 일부인 객체들을 기록하거나 marking 하기 위해서 미리 작성한 barrier를 사용합니다. remembered set에는 객체가 가리키는 포인터 정보가 담겨져 있으며, 새로운 객체나 수정된 객체를 추적할 수 있게 합니다.

이를 통해 더 높은 성능을 발휘하며, 어플리케이션 스레드를 중지하지 않고도 marking 작업을 수행할 수 있습니다.

Reference

https://docs.oracle.com/en/java/javase/12/gctuning/garbage-first-garbage-collector.html
https://docs.oracle.com/en/java/javase/12/gctuning/garbage-first-garbage-collector-tuning.html
https://luavis.me/server/g1-gc
https://johngrib.github.io/wiki/java-g1gc
https://velog.io/@hanblueblue/GC-2.-G1GC-tuning
https://marknienaber.medium.com/jvm-tuning-with-g1-gc-76f27535f054

profile
꾸벅 🙇‍♂️ 매일매일 한발씩 나아가자잇!

0개의 댓글