봉황대 in CS

[JVM] G1 Garbage Collector의 기본 개념과 동작 과정 (+ Serial, ParNew, CMS Collector) 본문

Java

[JVM] G1 Garbage Collector의 기본 개념과 동작 과정 (+ Serial, ParNew, CMS Collector)

등 긁는 봉황대 2025. 4. 8. 18:42
반응형

* 본 글은 ‘JVM 밑바닥까지 파헤치기’ 책과 Oracle 공식 문서들을 바탕으로 작성하였습니다. (참고 문서의 링크는 하단에 첨부)

 

이전 포스팅에서는 Garbage collection의 기본 개념과 JVM의 메모리 할당 전략에 대해서 알아보았다. 이번 글에서는 GC를 수행하는 주체인 Garbage collector들에 대해서 작성하고자 한다. 각 컬렉터들이 GC와 관련하여 풀고자 하는 문제들은 무엇이 존재하며 이들을 어떤 방식으로 해결하고자 하는지를 중점으로 알아보자.

 

Oracle JDK 7 ~ 11 Garbage Collectors


G1 컬렉터 설명 전, 이쪽에 묶인 가비지 컬렉터들은 세대(generation) 단위로 영역을 구분하며, 그들의 크기와 수가 고정되어 있는 Heap memory layout을 사용한다.

 

 

Serial & Serial Old Collector

가장 오래된 컬렉터들이다. Serial collector는 Young generation 용으로 Mark and copy 알고리즘을 사용하고, Serial old collector는 Old generation 용으로 Mark and compact 알고리즘을 사용한다. 이름에서 알 수 있듯이 GC가 단일 스레드로 수행된다. 그리고 GC가 완료될 때까지 모든 Application thread(User thread)들은 멈춰있어야 한다. 즉, Stop-the-world 현상이 발생하여 Application의 실행에 큰 영향을 미치게 된다.

 

 

이후 등장한 몇몇 가비지 컬렉터들은 이 Stop-the-world 시간을 줄이기 위한 목적으로 발전해 왔다.

 

ParNew Collector

Serial collector를 병렬화(parallelize)한 버전이다. 여러 thread를 통해 GC를 수행하는 것이니 Serial collector 보다는 자원을 효율적으로 사용한다. 그러나 여전히 Application thread과는 동시에(concurrently) 수행시키지 못하므로 Stop-the-world 문제는 여전하며, 여러 GC thread가 상호작용해야 하는 것에서 발생하는 overhead 또한 존재한다. 그러나 이후 소개할 CMS collector와 유일하게 조합할 수 있는 놈이었기 때문에 Young generation 용 컬렉터로 많이 사용되었다.

 

 

CMS Collector

Concurrent Mark Sweep(CMS) collector의 목적은 Stop-the-world 시간을 최소화하여 Application의 응답 지연 시간(response latency)을 줄이는 것이다. 이름에서 알 수 있듯이 Mark and sweep 알고리즘을 사용하는데, GC thread는 Mark 단계와 Sweep 단계를 Application thread와 동시에 수행한다. 동작 과정은 총 4가지 단계로 구성된다.

 

 

1. Initial mark pause, 최초 표시 단계

GC root와 직접 연결된 객체들을 살아있다고 표시한다. Stop-the-world 방식으로 진행되나, 매우 빠르게 끝난다. 기본적으로는 단일 thread로 수행된다.

 

2. Concurrent tracing phase, 동시 표시 단계

첫 번째 단계에서 표시한 객체들부터 시작해서 객체 그래프 전체를 탐색하며 Marking을 진행한다. 가장 오래 걸리는 단계이지만 Application thread와 동시에 수행한다. 이때 원래는 Application이 사용하고 있었던 processor resource를 GC thread가 사용하게 되어 Application의 throughput은 감소한다. (One or more concurrent garbage collector threads may be using processor resources that would otherwise have been available to the application.)

 

3. Remark pause, 재표시 단계

두 번째 단계 도중에 Application thread가 참조를 변경하여 누락된 객체들을 찾아 바로잡는다. Stop-the-world 방식으로 진행되고 최초 표시 단계보다는 길게 동작한다.

 

4. Concurrent sweeping phase, 동시 쓸기 단계

앞 3개의 단계를 통해 도달 불가능(Unreachable)이라고 판단한 객체들을 제거(Sweep) 한다. 살아있는 객체들을 옮기지 않기 때문에 Application thread와 동시에 진행할 수 있다.

 

동시성을 지원하는 최초의 가비지 컬렉터이나, 새롭게 발생하는 문제도 존재한다. 첫 번째, Application과 GC를 동시에 실행시키기 때문에 충분한 자원(메모리 등)이 필요하다. 또한, 위에 언급하였듯이 두 작업이 동시에 실행되는 단계에서는 Application의 throughput이 감소하게 된다. 두 번째, Mark and sweep 알고리즘을 토대로 하기 때문에 Memory fragmentation이 발생한다. 따라서 큰 객체를 할당하려고 했을 때, 메모리에서 이를 할당할 수 있는 연속된 공간을 찾지 못했다면 Full GC를 수행해야만 한다.

 

G1 Garbage Collector


Garbage-First(G1) garbage collector는 Java 9 이상부터 default garbage collector로 채택된 놈이다. G1 컬렉터의 목적은 목표 일시 정지 시간(= Stop-the-world 시간)을 높은 확률로 달성하는 동시에 높은 throughput을 달성하는 것이다. 즉, Application의 latency와 throughput 사이의 최적의 균형을 맞추는 것이다. (It attempts to meet garbage collection pause-time goals with high probability while achieving high throughput with little need for configuration. G1 aims to provide the best balance between latency and throughput using current target applications and environments.)

 

G1 컬렉터의 목적을 달성하기 위해서는 정지 시간 예측 모델(Pause prediction model)이 필요하다. GC에 쓰는 시간이 사용자 설정값 M msec을 넘지 않도록 통제하기 위함이다. 이를 구현하기 위해서는 제한된 시간 내에 어느 부분의 메모리를 회수하는 것이 가장 효율적인지를 알아야 한다. 따라서 이후에 설명할 G1 컬렉터의 Space-reclamation phase에서는 메모리 회수 영역을 고르는 기준이 어느 특정 영역(generation)에 속하지 않는 Mixed GC를 수행한다. 즉, 힙 메모리 전체를 봤을 때 어느 영역에 쓰레기가 가장 많은지(GC로 회수할 수 있는 공간의 크기) 그리고 메모리 회수 시 이득을 가장 많이 볼 영역이 어디인지(GC에 드는 시간도 포함)가 메모리 회수 영역의 기준이 된다.

 

유의해야 할 점은 목표 일시 정지 시간을 장기적으로, 높은 확률로, 최대한 만족하려고 하는 것이지 항상, 무조건적으로 지키지는 않는다는 것이다. (The Garbage-First collector is not a real-time collector. It tries to meet set pause-time targets with high probability over a longer time, but not always with absolute certainty for a given pause.)

 

Heap Memory Layout

G1 컬렉터는 이전 컬렉터들과 동일하게 세대(generation) 단위 컬렉션 이론을 기본으로 하지만, 아예 다른 Heap memory layout을 가지고 있으며 이것이 G1 컬렉터의 목표를 달성하게 해주는 중요한 역할을 한다.

 

G1 컬렉터는 Heap을 'Region'이라는 단위로 쪼갠다. Region은 메모리 할당과 회수의 단위가 되며, 어느 한 generation에 고정되어 있지 않다. 즉, Young generation의 Eden space 또는 Survivor space가 될 수도 있고, Old generation 영역으로 쓰일 수도 있다는 것이다. 그리고 커다란 객체를 저장하기 위한 'Humongous region'도 새롭게 둔다. 이후에 더 자세히 설명할 부분이지만, 한 region의 절반 이상을 넘는 객체들을 'Humongous object'라고 부른다. 만약 메모리 할당 요청이 온다면 Memory manager는 할당을 진행할 수 있는 Free region을 내어놓는데, 이 region이 어느 generation에 속하는 것인지를 먼저 지정한 후에 return 한다. (As requests for memory comes in, the memory manager hands out free regions. The memory manager assigns them to a generation and then returns them to the application as free space into which it can allocate itself.)

 

 

GC log를 출력해 보면 이 부분을 확인할 수 있다. GC log을 보기 위해서는 프로그램 실행 시 환경변수 -Xlog:gc*를 추가해 주면 되는데, Intellij 기준으로는 VM options에 아래처럼 넣어주면 된다. 실행한 Java version은 17이다.

 

찍힌 Log를 보면 이전에 컬렉터 관련 설정을 아무것도 하지 않았는데도 G1 컬렉터를 사용하고 있다고 찍혀있다. Java 17 기준으로는 G1 컬렉터가 default이기 때문이다. 그리고 Heap Region Size: 2M라고 찍혀있다. 각 region은 2 megabyte의 크기를 가지고 있다는 것이며, 이 설정을 기준으로는 1 megabyte 이상인 객체들은 Humongous object로 분류된다.

 

 

[참고]

Heap initial capacity : JVM 시작 시점의 Heap 크기 ( Heap은 메모리 압박량에 따라 그 크기가 동적으로 조절된다.)

Heap min capacity : 최소 Heap 크기

Heap max capacity : 최대 Heap 크기

Parallel workers : Stop-the-world 작업을 병렬로 수행하는 스레드의 개수

Concurrent workers : Stop-the-world 없이 실행되는(= Application과 동시에 실행되는) GC 작업을 담당하는 스레드의 개수

 

GC Cycle

크게 2가지 phase로 나뉜다.

 

출처 : https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html

 

[ Young-only phase ]

- Young-only collection pause (파란색 작은 원) : Young generation에 대해 GC가 수행된다. (= Minor GC) 여기서 살아남은 놈들은 그의 나이에 따라 Survivor space 또는 Old generation으로 승격된다.

- Concurrent start → Remark pause → Cleanup pause : Old generation의 메모리 사용률이 Initiating heap occupancy threshold에 도달했을 때, 즉 일정 수준 이상으로 찼을 때 이 단계가 수행된다. CMS collector의 동작 과정과 비슷하다. Application thread와 동시에 marking을 진행하고, 참조 변경된 객체 추적한다. 단, Old generation 영역에 대해서 진행하며, Cleanup pause에는 완전히 사용되지 않는 Old generation region들만 회수한다. (이 과정 중간중간에 Minor GC가 발생할 수 있기 때문에 Young-only phase에 포함된다.) 그리고 Remark와 Cleanup pause 시점 사이에 Old generation 영역에서 공간을 얼마만큼 회수할 수 있을지를 계산하고, 이 결과값을 통해 Space reclamation phase로 넘어갈지를 Cleanup pause 시점에 결정한다.

 

[ Space reclamation phase ]

- Mixed collection pause (분홍색 작은 원) : Young generation 영역과 일부 Old generation 영역을 함께 회수하는 Mixed GC가 발생한다. 만약 더 이상 Old generation을 회수해도 충분한 이득을 얻을 수 없다고 판단된다면 Space reclamation phase를 종료하고, Young-only phase로 주기가 다시 시작된다.

 

+) Full GC pause : 만약 Marking 과정 중에 Application이 모든 메모리를 사용하여 고갈되었거나, 충분한 연속된 공간을 찾을 수 없어 객체 할당이 불가능하다면 다른 컬렉터들처럼 In-place stop-the-world full heap compaction을 진행한다.

 

Humongous Objects

위에서 언급한 것과 같이, G1 컬렉터는 Region 크기의 절반 이상을 가지는 객체들을 'Humongous object'라고 판단한다. 그리고 Old generation 영역에 존재하는 'Humongous region'에 이 객체들을 저장한다. 이들을 다른 객체들과는 다르게 다루기 때문에 예상치 못한 문제가 발생할 수 있다. 따라서 거대한 객체를 할당해야 하는 경우에는 아래 특징들을 유의해야 한다.

 

1. Humongous object가 사용하지 않는 부분은 계속 낭비된다.

Humongous region은 Old generation 영역에 속하며 메모리 상에서 연속적인 region들이다. Humongous object들은 항상 첫 번째 region 시작점에 위치하며, 마지막 region에서 남는 공간들은 이 객체가 회수될 때까지 다른 곳에 사용되지 못한다.

 

2. Humongous object는 할당 후 다른 위치로 이동되지 않는다.

G1 컬렉터는 Humongous object에 대해서는 도달 가능 여부만을 판단하고, 도달 불가능할 경우에 할당을 해제하기만 한다. 이것 외의 다른 작업은 하지 않는다. (G1 only determines their liveness, and if they are not live, reclaims the space they occupy. Objects within humongous regions are never moved by G1.) 즉, Full GC가 발생했을 때에도 Humongous object를 이동시키지 않기 때문에 Memory fragmentation 문제가 발생한다. 만약 충분한 공간이 있음에도 불구하고 연속된 메모리 공간이 없어 할당이 불가능하게 된다면 결국 OOM(out-of-memory)가 터지게 된다.

 

3. Humongous object를 회수하는 시점은 Cleanup pause와 Full GC 밖에 없다.

예외로, Primitive type(e.g., boolean, integer 등)을 가진 배열에 대해서는모든 pause 시점에 회수할 수 있다.

 

4. Humongous object의 할당은 GC로 인한 pause를 자주 발생시킬 수 있다.

Humongous object를 할당할 때마다 G1 컬렉터는 Initiating heap occupancy threshold를 확인한다. Old generation 영역의 메모리 점유율을 확인하여 만약 임계치를 넘었다면 'Concurrent start → Remark pause → Cleanup pause' 과정을 시작하게 된다. 이는 모든 메모리가 고갈되어 Full GC가 발생하는 것을 방지하기 위해 GC를 시작하는 것인데, 만약 GC로 인한 pause가 너무 자주 발생하게 된다면 실행하는 Application의 latency에 큰 영향을 미칠 것이다.

 

참고


https://docs.oracle.com/en/java/javase/12/gctuning/concurrent-mark-sweep-cms-collector.html

https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html

 

 

반응형

'Java' 카테고리의 다른 글

[JVM] JVM의 메모리 할당 전략과 Garbage Collection  (0) 2025.04.04
Comments