본문 바로가기
[개발] Java & Kotlin

JVM GC에 대해 알아보자. (G1GC)

by 비어원 2025. 9. 21.
728x90

G1GC

G1GC는 대용량 메모리로 확장되는 멀티 프로세서 머신을 대상으로 설계되었다. 별도 구성 없이 높은 처리량을 달성하는 동시에 높은 확률로 적은 STW(Stop the world) 시간을 목표로 충족하려고 시도한다. G1GC는 아래와 같은 특징을 갖춘 애플리케이션을 대상으로 latency와 처리량 간의 최적의 균형을 제공하는 것을 목표로 한다.

  • 최대 수십GB 이상에 달하는 heap, heap의 50% 이상이 라이브 데이터로 차지된다.
  • 시간이 지남에 따라 크게 달라질 수 있는 객체 할당 및 프로모션 비율
  • heap에 상당히 많은 조각화가 있다.
  • 몇백 ms를 넘지 않는 예측 가능한 일시 정지시간을 목표로 하여 긴 가비지 수집 일시 중지를 방지한다.

 

메모리 구조

G1은 다른 GC와 마찬가지로 Heap을 Young과 Old 영역으로 분리한다. 하지만 메모리 영역을 크기가 동일한 작은 영역 세트로 분할하여 관리한다. 이 영역을 region이라고 한다.

img

 

위의 그림에서 Young 영역은 Eden(빨간색), Survivor(빨간색+S) 영역으로 나뉘고, Old 영역은 파란색으로 표시된다. G1의 region은 기본적으로 힙 할당량에 따라 다르게 설정되며, 직접 지정하는 것도 가능하다.

regionSize = pow(2, floor(log2(heapSize / 2048)))

-XX:G1HeapRegionSize 로 직접 지정 가능
  • 옵션을 주지 않는다면 기본적으로 위의 수식대로 결정되며 4G 미만인 경우 1MB, 8G 미만인 경우 2MB 등, 2의 제곱꼴로 크기가 결정된다.

여기서 region은 G1에서 메모리 할당과 회수의 단위로 관리한다. region은 비어있을 수도 있고, young 또는 old에 할당될 수도 있다. 메모리 요청이 들어오면 관리자는 비어있는 영역을 나눠주고, GC를 수행하는 경우 도달 불가능한 메모리를 반환하여 애플리케이션에 메모리를 할당할 수 있도록 한다.

 

메모리 요청이 들어오면 메모리는 기본적으로 Eden 영역에 할당된다. 하지만, 거대 객체는 예외적으로 Old 영역으로 할당한다고 한다.

메모리 공간이 연속적인 Serial, Parallel GC와는 다르게 G1GC는 Region 단위로 나눠서 메모리 공간을 비연속적으로 관리한다. 이렇게 관리하는 경우에는 Serial, Parallel GC에서 발생하는 메모리 단편화가 줄어들기 때문에 이 방식에 비해 효율적일 수 있다.

 

GC

G1GC는 가비지 수집과 메모리 확보를 위해 두 단계를 번갈아가면서 작동한다.

  1. Young Only 단계에서는 현재 사용 가능한 메모리를 old generation의 객체로 점진적으로 채우는 가비지 수집을 포함한다.
  2. Space Reclamation 단계에서는 G1이 young generation을 처리하는 것 외에도 old generation에서 점진적으로 공간을 회수하는 단계이다.

img

 

Young only 단계

 

이 단계에서는 Young에 있는 메모리를 Old로 승격시키는 normal young collection을 진행한다. 여기서 Young에 있는 도달 가능한 객체는 Survivor나 Old로 복사되며, 복사 이후에는 해당 영역은 정리가 된다. 이 과정에서 STW가 발생하게 된다.

 

 

Old 영역의 메모리가 특정 임계값(InitiatingHeapOccupancyPercent, default: 45%) 이상 도달할 때 까지 진행하며, 임계값을 넘으면 concurrent start young collection을 스케줄링 한다.

 

Young only (Concurrent start young collection)

Concurrent start young collection은 총 3단계로 진행된다.

  1. Concurrent Start: Normal young collection과 함께 마킹을 시작한다. 마킹 단계에서는 Old 영역에서 도달 가능한 객체를 표시하여 Space Reclamation 단계에서 객체가 정리되지 않도록 보호한다. 마킹 단계는 두 가지 STW인 Remark와 Cleanup으로 끝난다.
  2. Remark: 마킹을 마무리하고 전역 참조 프로세싱과 클래스 언로딩을 수행하고 완전히 비어있는 영역을 회수하고 내부 데이터 구조를 정리한다.
  3. Cleanup: 이 단계에서는 space-reclamation 단계를 실제로 수행할지 여부를 결정한다. 만약 space-reclamation 단계를 진행하면, young-only 단계는 단일의 prepared mixed young collection 으로 완료된다.

 

Space Reclamation

이 단계에서는 Young 외에도 Old 영역의 활성 객체를 비우는 여러개의 Mixed collection으로 구성된다. 이 단계는 G1이 더 많은 Old 영역을 비우는 것이 충분한 여유공간을 확보하지 못할 것으로 판단할 때 종료된다.

 

 

애플리케이션이 활성 정보를 수집하는 동안 메모리가 부족해지면 G1은 즉시 Full GC (Major GC)를 수행한다.

 

성능 테스트

Serial GC와 마찬가지로 같은 환경에서 성능 테스트를 진행해보았다. (아래 글 Serial VS Parallel 참조)

 

2025.09.20 - [[개발] Java & Kotlin] - JVM GC에 대해 알아보자. (Serial GC & Parallel GC)

 

JVM GC에 대해 알아보자. (Serial GC & Parallel GC)

Garbage CollectorGarbage Collection는 JVM에서 자바의 메모리를 관리하는 방법으로, 애플리케이션에서 더 이상 참조되지 않는 객체의 메모리를 회수하는 자동 메모리 관리 방식이다. 자바 애플리케이션

beer1.tistory.com

 

 

 

G1GC는 17분 49초가 걸렸다.

 

G1GC가 큰 용량의 힙사이즈에 대한 가비지 수집에 최적화 되어있어서 Parallel GC보다는 느린 듯 하다. 메트릭을 보면 평균 STW 시간이 약 34ms 정도 되는 것을 확인할 수 있다.

 

성능 테스트 결과 (Heap = 1Gi, 요청 = 20만)

 

Serial, Parallel, G1의 성능 테스트 결과는 다음과 같다. 평균 STW 시간이 G1GC가 가장 많이 걸린다.

GC 종류 Serial Parallel G1
총 소요시간 17m 56s 14m 42s 17m 49s
평균 STW 시간 12.1ms 8.43ms 33.9ms
GC 횟수 20 40 12

 

G1GC가 큰 용량의 힙사이즈에 대한 가비지 수집에 최적화 되어있다고 해서 힙 용량을 더 크게해서 테스트를 해보면 좋을 것 같다.

성능 테스트 결과 (Heap = 4Gi, 요청 = 30만)

Heap 용량을 4G로 늘리고, 요청을 30만개로 늘려서 테스트해본 결과는 다음과 같다.

GC 종류 Serial Parallel G1
총 소요시간 28m 30s 34m 02s 30m 50s
평균 STW 시간 127ms  131ms 63.5ms
GC 횟수 8 10 15

 

Serial GC

 

의외로 Serial GC가 총 소요시간이 28분 30초로 가장 빨랐다. 하지만 평균 STW 시간은 127ms로 G1보다 많이 느리다.

 

 

 

Parallel GC

Parallel GC는 총 소요시간이 34분 2초가 걸렸다. 평균 STW 시간은 131ms로 Serial 보다 느린데, 메트릭 그래프를 보면 Survivor 영역이 동적으로 늘어난 것을 확인할 수 있다.

 

 

 

이유를 찾아보니 Serial과 Parallel GC에서 Survivor 영역을 관리하는 방식이 다르다고 한다.

 

Serial GC는 Survivor 영역을 고정적으로 관리하는 반면, Parallel GC는 Throughput을 최대화하기 위해 Survivor를 동적으로 조정한다고 한다. Survivor 영역에서 살아남는 객체가 많은 경우 Survivor 영역을 더 크게 잡아서 객체 Copy 비용을 줄이는 것을 기대한다.

 

테스트 웹서버 특성상, Queue에 객체를 넣어서 Queue에 있는 객체는 Old로 넘어갈 확률이 높기 때문에 시간이 지나면 Survivor 영역이 꽉 차서 GC가 Survivor 영역을 늘린다. 이 순간에는 STW 시간이 늘어나더라도 장기적으로 봤을 때 Throughput을 높이기 위해서 Survivor 영역을 늘린다고 할 수 있다.

 

그래프를 더 자세히 보면, Survivor 영역이 늘어나기 전에는 STW 시간이 100ms 정도인 것을 확인할 수 있으며, 이 때는 Serial 보다 더 빠르다는 것을 알 수 있다. Survivor 영역이 늘어난 경우 Eden 영역이 줄어드는데, 진행한 테스트에서는 단순 같은 요청을 반복하며, 요청할 때마다 같은 크기의 메모리를 사용하기 때문에 요청에 비례해서 eden 영역에 메모리가 쌓인다. 그래서 eden 영역이 줄어들면 GC가 더 자주 일어나고, Survivor 영역이 많이지고, 이 영역의 객체는 모두 Queue에 쌓인 객체이며 보통은 Old로 향하기 때문에 Old로 승격되는 객체가 더 많아지고 그 영향으로 STW 시간이 더 길어진 것으로 보인다.

 

일반적인 웹 애플리케이션의 경우에는 대부분 Eden에서 단명하며, Old로 잘 가지 않기 떄문에 Serial GC보다는 더 빠를 가능성이 크다.

G1 GC

G1 GC는 총 소요시간이 30분 50초가 걸렸고, Serial GC와 비슷하다. 하지만 평균 STW 시간이 63.5ms로 나머지 두 GC에 비해 빠르다. 힙 용량이 클수록 STW 시간이 G1 GC가 더 효율적인 것을 알 수 있다. (튜닝하지 않았을 경우)

 

 

 

728x90

댓글