[GC] 2. G1GC tuning

rin·2021년 10월 2일
4

https://www.oracle.com/technical-resources/articles/java/g1gc.html

IHOP

Initiating Heap Occupancy Percent : 최초 마킹 발동 기준이 되는 값, old gen 사이즈에 대한 백분율이다.

Adaptive IHOP

  • 수집한 통계 데이터(마킹에 소요되는 시간 및 마킹 주기)를 기반으로 최적의 IHOP 값을 찾아내 알아서 설정한다.
  • -XX:-G1UseAdaptiveIHOP 옵션으로 on/off가 가능하다.
    • on : -XX:InitiatingHeapOccupancyPercent 옵션을 주는 경우, 통계값이 충분하지 않은 초기 상태에서 해당 값을 초기값으로 사용한다.
    • off : 통계를 계산하지 않으므로 이런 경우 -XX:InitiatingHeapOccupancyPercent로 지정한 IHOP 값을 계속 사용한다.
  • -XX:G1HeapReservePercent로 설정된 값 만큼의 버퍼를 제외하고 시작 Heap 점유율을 설정한다.
    • 이는 old gen 점유율이 최대 old gen 사이즈에서 -XX:G1HeapReservePercent 값을 뺀 크기가 된 경우 space-reclamation phase의 첫 mixed GC가 시작되도록 하기 위함이다.

Humongous Object

  • 위 구조에서 H로 표시된 객체이다.
  • 말 그대로 크기가 큰 Region
  • -XX:G1HeapRegionSize의 영향을 받는다. 기본값을 사용하고 있다면, 알아서 자동으로 결정된다.
  • 연속된 영역에 대해 순차적으로 공간을 차지하도록 할당된다.
  • 마지막 꼬리 영역의 잉여공간은 사용하지 않는다. 즉, 큰 객체가 회수될 때 까지는 빈 공간으로 유지된다.
  • ❗️ 큰 객체가 할당되면, G1은 IHOP을 확인하고 초과된 상태일 시 강제로 즉시 young collection을 시작한다.
  • 큰 객체는 Full GC 중에도 옮겨지지 않으므로 조각화가 발생할 수 있다.
    • 공간이 충분한데도 메모리 부족 상태가 발생할 가능성이 있다. 👉 Full GC가 느려질 수 있다.

옵션

https://docs.oracle.com/en/java/javase/11/gctuning/garbage-first-garbage-collector.html#GUID-082C967F-2DAC-4B59-8A81-0CEC6EEB9016

  • -XX:InitiatingHeapOccupancyPercent=45, -XX:+G1UseAdaptiveIHOP
    위에서 설명한 IHOP 관련 설정
  • -XX:G1HeapWastePercent=10
    얼마나 많은 region이 낭비되어도 가능한지를 결정한다. (Heap을 낭비해도 좋다고 판단하는 값) Mixed cycle의 종료 시점을 결정한다.
  • -XX:G1MixedGCCountTarget=8
    한번의 Mixed GC 때 처리할 Region의 개수. 값이 적을 수록 Mixed GC가 여러 주기에 거쳐 가비지를 수거하므로 중단 시간은 길어지나 빠르게 region를 비워주는 이점이 있다.
    • -XX:MaxGCPauseMillis 값으로 제한된 시간 내에 처리되어야 한다. 처리 불가한 경우 MaxGCPauseMillis 플래그가 우선된다.
  • -XX:G1MixedGCLiveThresholdPercent=85
    Old Region 내 라이브 객체의 점유율이 이 옵션 이상이라면 GC 대상에서 제외(space-reclamation 단계에서 수집되지 않음)한다.
  • -XX:MaxGCPauseMillis=200
    최대 일시 정지 시간 목표. 기본값은 200ms.
  • -XX:GCPauseTimeInterval=<ergo>
    일시 정지 시간 "최대 간격" 목표
    이 값은 기본값이 없어서, 최악의 경우 G1이 GC를 끊임없이 계속 수행할 수도 있다.
  • -XX:ParallelGCThreads = <ergo>
    일시 정지 중 parallel 작업에 사용되는 최대 스레드 갯수.
    사용 가능한 프로세서 수가 8보다 작으면 그대로 지정한 값을 사용하고, 그 외의 경우는 5/8만큼의 스레드를 추가로 사용한다.
    예를 들어 사용 가능한 프로세서 수가 13 개라면 8+(13−8)×5/8=11.125 이므로, 11 개의 스레드를 사용한다.
    일시 정지 상태로 들어갔을 때 사용되는 최대 스레드의 수는 최대 토탈 heap 사이즈에 의해 제한을 받는다.
    • -XX:HeapSizePerGCThread 옵션으로 지정된 GC 스레드가 담당할 heap 사이즈의 최대값에 영향을 받는다.
  • -XX:ConcGCThreads=<ergo>
    동시 작업에 사용하는 최대 스레드 수.
    이 값은 -XX:ParallelGCThreads를 4로 나눈 값이다.
    Old Region 내 라이브 객체의 점유율이 해당 옵션 값 이상이라면 GC 대상에서 제외한다. (space-reclamation 단계에서 수집되지 않는다.)
  • -XX:G1HeapRegionSize=<ergo>
    영역 하나의 사이즈. 기본적으로는 최대 heap 사이즈의 1/2048 만큼의 계산된 사이즈를 갖는다.
    굳이 설정을 한다면 1 ~ 32MB 정도로 설정할 수 있으며, 2의 거듭제곱 값이어야 한다.
  • XX:G1NewSizePercent=5, -XX:G1MaxNewSizePercent=60
    young gen의 총 사이즈는 이 두 값 사이에서 변화한다.

튜닝

https://docs.oracle.com/en/java/javase/11/gctuning/garbage-first-garbage-collector-tuning.html#GUID-90E30ACA-8040-432E-B3A0-1E0440AB556A

G1에 대한 일반 권장 사항

가장 일반적인 권장 사항은 기본 설정으로 G1을 사용하면서, 적절한 일시 중지 시간 목표를 설정하고 필요한 경우 -Xmx 옵션을 통해 최대 Java 힙 크기를 설정하는 것이다.

❗️ 기본 구성에서 G1의 목표는 높은 처리량에 비해 비교적 적고 균일한 일시 중지를 제공하는 것이다.

  • 높은 처리량을 선호하는 경우 : -XX:MaxGcPauseMillis 옵션(최대 일시 정지 시간 목표값) 혹은 더 큰 힙을 사용하게 함으로써 일시 중지 시간 목표를 완화해야한다. (일시 중지 시간이 길어질 것)
  • 지연 시간 최소화를 원하는 경우 : 일시 중지 시간 목표를 조정한다.

주의 사항

  • young gen.은 G1이 설정된 일시 중지 시간을 충족할 수 있도록 만드는 주요 수단이므로 -Xmn, -XX:NewRatio 등과 같은 옵션을 사용해 young gen.의 크기를 특정 값으로 제한해서는 안된다.
    • young gen.의 크기를 특정 값으로 설정하는 경우, 일시 중지 시간 제어가 무시된다.(비활성화)

다른 수집기에서 G1으로 변경하는 경우

일반적으로 다른 수집기(특히 Concurrent-Mark-Sweep GC)에서 G1으로 이동할 때는

  1. GC에 영향을 미치는 모든 옵션을 제거한다.
  2. 일시 중지 시간 목표와 -Xmx, -Xms(optional)를 사용해 전체 힙 크기만 설정한다.

이는 다른 수집기에 영향을 미치는 옵션들은 G1에 영향이 없거나, G1의 처리량과 일시중지 목표를 충족함에 있어 문제를 일으킬 수 있기 때문이다.

G1 성능 향상

G1은 추가 옵션 지정없이도 전반적으로 우수한 성능을 제공할 수 있도록 설계되었다.
하지만, 기본 heuristics(휴리스틱, 추론)이나 구성이 차선의 결과를 제공하는 경우가 있기에 이를 진단하고 개선할 수 있는 몇 가지 방법을 제공한다.

진단을 위해 G1은 포괄적인 로깅을 제공한다. -Xlog:gc*=debug옵션을 우선 사용하고, 필요한 경우 출력을 상세 조정할 수 있다. 로그는 GC 활동 및 그 외의 다양한 개요를 제공한다. (수집 유형 및 일시 중지의 특정 단계에서 분류별로 소요된 시간 등이 포함된다.)

1️⃣ Full GC

Full GC는 일반적으로 오랜 시간이 소요된다.

  • old gen.에서의 높은 힙 점유률로 인한 Full GC는 로그에서 Pause Full(Allocation Failure)로 표시된다.
  • 일반적으로 to-space exhausted 태그로 표시되는 evacuation 실패가 선행된다.

Full GC가 발생하는 이유는 (1) 어플리케이션이 신속하게 회수 가능한 수 이상으로 객체를 너무 많이 할당했기 때문이다. 종종 Concurrent marking을 끝내지 못하고 급히 "space-reclamation" 페이즈를 시작하는 경우가 있다.

(2) 또한 G1의 할당 방식 때문에 다수의 큰 객체가 할당될 때, 예상보다 더 큰 메모리를 차지하게 됨으로써 Full GC가 발생활 확률은 더욱 복잡해질 수 있다.

👉 해결책

  • 목표 : Concurrent marking이 제시간(목표한 시간)에 종료될 수 있도록 만든다.
  • 방법 : (1) old gen.의 할당 비율을 줄이거나 Concurrent marking을 완료하는데 더 많은 시간을 할당한다.

👉 사용가능한 옵션

1) Heap region 사이즈 조정

  • gc_heap=info 로깅옵션 : Java 힙에서 큰 객체가 차지하는 region 수를 확인한다. (Humongous Region)
  • Humongous region: X->Y 의 Y는 큰 객체가 차지하는 영역의 양을 나타낸다. 이 값이 old region 보다 크다면 이 객체(humongous region에 포함된 큰 객체)의 수를 줄이는 것이 베스트이다. -XX:G1HeapRegionSize 옵션을 사용해 각 region의 크기를 늘리면 기존 humongous region에 들어갈 수 밖에 없었던 객체가 일반 young region(eden)에 포함될 수 있다.
  • 현재 지정된 heap region의 크기는 로그 시작 부분에서 확인할 수 있다.

2) Java Heap 크기를 조정

  • -XX:G1HeapRegionSize를 명시하지 않는 경우, 각 Region size는 전체 Heap size에 의존하므로 결과적으로 1)안과 동일하다.
  • 단, Heap 크기가 증가한단 것은 마킹에 사용되는 시간 또한 길어짐을 의미한다.

3) 명시적으로 -XX:ConcGCThreads 옵션을 설정해 Concurrent marking에 사용되는 thread 수를 증가시킨다.

4) G1이 더 일찍 마킹하도록 한다.

  • 해제되지 않은 객체가 많은 경우라고 볼 수 있기 때문에, GC가 더 자주 일어나도록 하면 된다.
  • 기본적으로 G1GC는 -XX:+G1UseAdaptiveIHOP 옵션을 사용해 이전 어플리케이션 동작을 기반으로 IHOP 임계값을 결정하므로 어플리케이션 동작이 변경되는 경우 예측이 틀릴 수 있다.
  • -XX:G1ReservePercent를 사용해 adaptive IHOP 계산에 사용되는 버퍼를 증가시킴으로써 "space-reclamation"을 시작할 시점에 대한 목표치를 낮춘다.
  • 혹은 -XX:-G1UseAdaptiveIHOP-XX:InitiatingHeapOccupancyPercent를 사용해 수동으로 adaptive IHOP 계산을 비활성화 시킬 수 있다.

Full GC에 대한 할당 실패 외에 어플리케이션 혹은 일부 외부 도구에 의해 Full heap collection이 유발되는 경우가 있을 수 있다.
System.gc()에 의해 발생하나 어플리케이션 코드를 수정할 방법이 없는 경우, -XX:+ExplicitGCInvokesConcurrent를 사용해 Full GC의 영향을 감소시키거나 -XX:+DisableExplicitGC 설정으로 VM이 이를 무시하도록 할 수 있다.

❗️NOTE 대형 객체에 의한 문제
-XX:G1HeapRegionSize을 사용해 대형 객체의 수를 조절할 수 있다.
하지만 극단적인 경우, G1이 객체를 할당하는데 필요한 연속 공간이 불충분 할 수 있다. Full GC를 통해 충분한 연속 Region을 회수할 수 없는 경우 VM은 종료된다.

가능한 방법은 humongous 객체 할당량을 줄리거나 heap size를 늘리는 수 뿐이다.

2️⃣ 지연시간 조정

일시 중지 시간이 너무 긴 경우

1) 비정상적인 시스템 혹은 Real-Time 어플리케이션

로그에서는 일시 중지 시간이 어디서 사용되었는지에 대한 분석을 확인 할 수 있다.

  • User Time : VM 코드에서 보낸 시간
  • System Time : OS에서 보낸 시간
  • Real Time : 일시 중지 중 경과된 절대(absolute) 시간

시스템 시간이 상대적으로 높은 경우 대부분 환경이 원인이다. 이런 경우 아래와 같은 조치를 취할 수 있다.

  • VM이 OS 메모리로부터 메모리를 할당하거나 반환하는 경우.
    • -Xms, -Xmx 옵션을 사용해 최소/최대 힙 크기를 동일한 값으로 설정
    • 해당 작업을 VM 시작단계로 이동시키기 위해 -XX:+AlwaysPreTouch 옵션을 사용해 모든 메모리를 미리 할당/반환 시킨다.
  • Linux의 THP(Transparent Huge Pages) 기능에 의해 작은 Page를 큰 Page로 병합하다가 임의의 프로세스가 중단되는 경우
    • OS 설정을 통해 해당 기능을 비활성화 할 필요가 있을 수 있다.
  • 로그가 기록되는 하드 디스크의 모든 I/O 대역폭을 간헐적으로 차지하는 일부 백그라운드 작업에 의해 로그 쓰기가 중단되는 경우.
    • 로그 작성을 위한 별도의 디스크를 사용하는 것을 추천한다.

2) Real 시간이 User와 System 시간을 합친 것보다 큰 경우

  • VM이 과부하 가능성이 있는 시스템에서 충분한 CPU 시간을 얻지 못함

3) Reference Object 처리 시간이 너무 오래 걸리는 경우

  • Reference Object는 -XX:ParallelGCThreads 값을 기준으로하여 싱글 스레드로 작동한다.
  • 사용가능한 모든 스레드를 사용하도록 -XX:ReferencesPerThread를 0으로 설정하거나 -XX:-ParallelRefProcEnabled로 병렬화를 비활성화 할 수 있다.

4) Young-Only 단계에서 Young-Only Collection 시간이 오래걸리는 경우
일반적으로 Young Colelction은 Young Gen.의 크기. 즉, 복사가 진행될 CSet 내 라이브 객체 수에 비례하는 시간이 소요된다.

  • CSet의 Evacuate 단계, 특히 sub-phase인 객체 복사 시간이 지연되는 경우
    • -XX:G1NewSizePercent를 작게 설정해 Young gen.의 최소 크기를 줄여준다.
  • 어플리케이션의 성능, 특히 컬렉션에 남아 있는 객체의 양이 급변하는 경우 Young Gen.의 크기 조정과 관련된 다른 문제가 발생할 수 있다.
    • -XX:G1MaxNewSizePercent를 작게 설정해 Young gen.의 최대 크기를 줄여준다.
    • 일시 중지 중 처리해야 하는 개체 수가 제한된다.

5) Mixed Collection 시간이 오래걸리는 경우
Mixed Collection은 Old gen. Region을 회수하는데에 사용된다. 즉 Mixed Collection에는 Young gen.과 Old gen. Region이 모두 포함된다.

로그 출력 시 gc+ergo+cset=trace를 활성화하여 관련된 정보를 얻을 수 있다.
Old gen. 시간이 긴 경우

  • -XX:G1MixedGCCountTarget을 높게 설정해 Old gen내 Region 처리 개수를 늘린다.
  • -XX:G1MixedGCLiveThresholdPercent를 사용해 GC 대상을 줄인다.
    • 후보군 Collection에 포함되지 않도록 조정한다.
  • -XX:G1HeapWastePercent 값을 높게 설정해 G1이 높은 메모리를 차지하는 Region은 수집하지 않도록 한다.

Young Gen. 시간이 긴 경우 (4)를 참조하도록 한다.

6) RSet 업데이트 및 스캔 시간이 긴 경우

G1은 단일 Old gen region을 피하기 위해 cross-region references의 위치를 추적한다. (한 region에서 다른 region를 참조) 이를 해당 region의 Reference set(RSet)이라고 부른다.

Region 내용이 이동될 때(Evacuation), RSet은 업데이트된다. 단, 이는 효율을 위해 즉시 업데이트되지 않고 일괄 처리된다. 이 때 어플리케이션에 따라 상당한 시간이 소요될 수 있다.

  • -XX:G1HeapRegionSize로 heap region의 크기를 조정
    • cross-region references 수와 RSet의 크기에 영향을 준다.
    • region이 클 경우, cross-region references가 적은 경향이 있으므로 상대적으로 처리에 소요되는 작업량이 감소된다. 단, 각 region이 더 많은 라이브 객체를 가지므로 다른 단계의 지연 시간이 증가할 가능성이 있다.
  • -XX:G1RSetUpdatingPauseTimePercent로 RSet update에 사용되는 시간을 강제화 할 수 있다.
    • 해당 옵션값이 작을 수록 G1은 더 많은 RSet 업데이트 작업을 동시에 수행시킨다.

RSet을 스캔하는 시간은 G1이 메모리에 저장되는 RSet storage 크기를 작게 유지하기 위해 수행하는 압축정도에 의해 결정된다. 즉, RSet이 더욱 작게 (압축되어) 저장될수록 값을 검색하는데에 더 많은 시간이 걸린다.

gc+remset=trace 수준의 로깅과 함께 -XX:G1SummarizeRSetStatsPeriod 옵션을 사용할 시 이러한 압축이 발생하는지 확인 할 수 있다. ([Before GC Summary] 섹션 이전 Did <X> coarsenings 라인의 X값은 높은 값을 보여줄 것이다.)

  • -XX:G1RSetRegionEntries을 높임으로써 압축되는 양을 감소 시킬 수 있다.
  • 단, 위와 같은 로깅을 사용할 경우 데이터 수집에 상당한 시간이 걸릴 수 있으므로 ❗️ 프로덕션 환경에서는 사용해서는 안된다.

3️⃣ 처리량 조정

G1은 기본적으로 처리량과 대기 시간 간의 균형을 유지하려고 하지만 어플리케이션에 따라 높은 처리량이 바람직한 경우도 있다.

  • -XX:MaxGCPauseMillis를 사용해 최대 일시 중지 시간을 늘린다. (일시 중지 빈도는 감소한다.)
    generation 크기 조정 휴리스틱은 일시 중지 빈도를 직접 결정하는 young gen 크기를 자동으로 조정한다.

space-reclamation phase에서 예상되는 동작이 이뤄지지 않는 경우 -XX:G1NewSizePercent 옵션으로 young gen의 최소 크기를 늘리면 된다.

  • -XX:G1MaxNewSizePercent로 young gen.의 최대 크기를 늘려 처리량을 증가 시킨다.
  • 동시 작업을 위한 RSet 업데이트에 CPU 리소스가 많이 소모되므로 동시 작업량을 줄여 처리량을 증가 시킨다.
    • -XX:G1RSetUpdatingPauseTimePercent를 높은 값으로 설정하면 동시 작업이 감소, 일시 중지 시간 증가
    • 극단적인 경우 RSet 업데이트를 비활성화하고, 다음 GC로 미룰 수 있다.
      • -XX:-G1UseAdaptiveConcRefinement, -XX:G1ConcRefinementGreenZone=2G, -XX:G1ConcRefinementThreads=0
  • heap 사이즈 조정 작업을 끄거나 최소화한다. 아래 두 방법을 사용하면 높은 확률로 일관된 일시 정지 시간을 얻을 수 있다.
    • -Xms, -Xmx를 같은 값으로 설정
    • -XX:+AlwaysPreTouch를 설정
profile
🌱 😈💻 🌱

0개의 댓글