Garbage Collector #1
공식문서로 Garbage Collector를 배워볼까요?
#1. Introduction to Garbage Collection Tuning
다양한 애플리케이션, 즉 데스크톱의 작은 애플릿부터 대형 서버에서 실행되는 웹 서비스까지, Java Platform, Standard Edition(Java SE)을 사용합니다.
이러한 다양한 배포 환경을 지원하기 위해, Java HotSpot VM은 여러 개의 가비지 컬렉터를 제공하며, 각 컬렉터는 서로 다른 요구사항을 충족하도록 설계되었습니다.
Java SE는 애플리케이션이 실행되는 컴퓨터의 종류에 따라 가장 적절한 가비지 컬렉터를 자동으로 선택합니다.
그러나, 이 선택이 모든 애플리케이션에 최적화되어 있지는 않을 수 있습니다.
엄격한 성능 목표나 특정 요구사항이 있는 사용자, 개발자 및 관리자들은 원하는 성능 수준을 달성하기 위해
가비지 컬렉터를 명시적으로 선택하고 특정 매개변수를 조정해야 할 수도 있습니다.
이 문서는 이러한 작업을 돕기 위한 정보를 제공합니다.
먼저, 일반적인 가비지 컬렉터의 특징과 기본적인 튜닝 옵션을 Serial(단일 스레드), Stop-The-World(모든 애플리케이션 실행을 멈추는 방식) 가비지 컬렉터를 기준으로 설명합니다. 그 후, 다른 가비지 컬렉터의 특정 기능과, 컬렉터를 선택할 때 고려해야 할 요소를 제시합니다.
What Is a Garbage Collector?
가비지 컬렉터(GC)는 애플리케이션의 동적 메모리 할당 요청을 자동으로 관리합니다.
가비지 컬렉터는 다음과 같은 작업을 통해 자동으로 동적 메모리를 관리합니다.
- 운영 체제로부터 메모리를 할당받고 다시 반환합니다.
- 애플리케이션이 요청할 때 해당 메모리를 제공합니다.
- 해당 메모리 중 어떤 부분이 여전히 애플리케이션에서 사용 중인지 확인합니다.
- 사용되지 않는 메모리를 회수하여 애플리케이션이 재사용할 수 있도록 합니다.
Java HotSpot 가비지 컬렉터는 이러한 작업의 효율성을 높이기 위해 다양한 기술을 사용합니다.
- 세대별 스캐빈징(generational scavenging)과 객체 수명 주기(aging)를 함께 사용하여,
힙(heap)에서 회수 가능한 메모리가 많을 가능성이 높은 영역을 집중적으로 처리합니다. - 여러 개의 스레드를 사용하여 적극적으로 연산을 병렬 처리하거나,
일부 장시간 실행되는 작업을 애플리케이션과 동시에 백그라운드에서 수행합니다. - 살아있는 객체(live objects)를 압축(compacting)하여 더 큰 연속적인 빈 메모리를 확보하려고 시도합니다.
Why Does the Choice of Garbage Collector Matter?
가비지 컬렉터(GC)의 목적은 애플리케이션 개발자가 수동으로 동적 메모리를 관리할 필요가 없도록 하는 것입니다.
즉, 개발자는 메모리 할당과 해제(deallocation)를 일일이 맞추거나, 할당된 동적 메모리의 수명을 신경 쓸 필요가 없습니다.
이 방식은 메모리 관리와 관련된 특정 오류를 완전히 제거할 수 있지만, 그 대가로 런타임 오버헤드가 추가됩니다.
Java HotSpot VM은 여러 가지 가비지 컬렉션 알고리즘을 제공하며, 사용자가 선택할 수 있게 해줍니다.
어떤 애플리케이션에서는 가비지 컬렉터 선택이 전혀 중요하지 않을 수 있습니다.
즉, 애플리케이션이 가비지 컬렉션이 발생하는 동안 적당한 빈도와 지속 시간의 일시 중지(pause)를 허용할 수 있다면, 성능 문제는 크지 않을 수 있습니다.
하지만, 대규모 데이터(수 GB 이상)를 다루거나, 많은 스레드를 사용하거나, 높은 트랜잭션 처리율이 요구되는 애플리케이션에서는 상황이 다릅니다. 이러한 애플리케이션에서는 가비지 컬렉션이 성능에 큰 영향을 미칠 수 있으며, 적절한 GC 선택이 필수적입니다.
Amdahl의 법칙(Amdahl’s Law) 에 따르면, 병렬 처리가 가능한 문제라 하더라도, 순차적으로 실행되는 부분이 존재하기 때문에 완벽하게 병렬화할 수 없습니다. 즉, 완벽한 병렬 처리는 불가능하며, 일부 작업은 항상 순차적으로 실행될 수밖에 없습니다.
Java 플랫폼에서는 현재 4가지 GC 방식을 지원하며, 이 중 Serial GC를 제외한 모든 GC는 병렬 처리를 활용하여 성능을 개선합니다.
따라서 GC로 인해 발생하는 오버헤드를 가능한 한 줄이는 것이 매우 중요합니다.
아래 그래프(Figure 1-1)는 이상적인 확장성(scalability)을 갖춘 시스템을 모델링하지만, GC가 유일한 병목 요소인 경우를 가정합니다.
- 빨간색 선
- 단일 프로세서 시스템에서 GC가 전체 실행 시간의 1%를 차지하는 애플리케이션입니다.
- 하지만 32개 프로세서로 확장하면, GC로 인해 20% 이상의 처리량 손실(throughput loss)이 발생합니다.
- 자홍색(핑크색) 선
- 단일 프로세서 시스템에서 GC가 전체 실행 시간의 10%를 차지하는 애플리케이션입니다.
- 이 경우 32개 프로세서에서 처리량 손실이 75% 이상으로 증가합니다.
- 즉, 단일 프로세서에서 허용 가능한 GC 오버헤드도, 멀티코어 환경에서는 심각한 성능 저하를 초래할 수 있습니다.
대규모 시스템으로 확장하는 과정에서, GC는 성능 병목이 될 수 있습니다. 그러나 이러한 병목을 조금이라도 줄이면 성능이 크게 향상될 수 있습니다. 충분히 큰 시스템에서는 적절한 가비지 컬렉터를 선택하고, 필요에 따라 튜닝하는 것이 가치 있는 작업이 됩니다.
Serial 가비지 컬렉터는 대부분의 작은 애플리케이션에 충분히 적합하며, 특히 최대 힙 크기가 약 100MB 이하인 현대적인 프로세서 환경에서 적절한 선택이 될 수 있습니다.
반면, 다른 가비지 컬렉터들은 추가적인 오버헤드나 복잡성을 가지며, 이는 특수한 동작을 제공하기 위한 대가입니다.
따라서, 애플리케이션이 특정 GC의 특수한 동작을 필요로 하지 않는다면, Serial 가비지 컬렉터를 사용하는 것이 바람직합니다.
Serial 가비지 컬렉터가 최선의 선택이 아닐 수 있는 한 가지 경우는 대규모 메모리를 사용하고, 두 개 이상의 프로세서를 가진 머신에서 실행되는 다중 스레드 애플리케이션입니다. 이와 같은 서버급(server-class) 머신에서 애플리케이션이 실행될 경우, 기본적으로 Garbage-First(G1) 가비지 컬렉터가 선택됩니다.
#2. Ergonomics
Ergonomics는 Java 가상 머신(JVM)과 가비지 컬렉션(GC) 휴리스틱(heuristics),예를 들어 행동 기반 휴리스틱(behavior-based heuristics)을 통해 애플리케이션 성능을 향상시키는 과정입니다.
JVM은 플랫폼에 따라 기본적으로 가비지 컬렉터(GC), 힙 크기, 런타임 컴파일러(runtime compiler)를 자동으로 선택합니다.
이러한 선택은 다양한 유형의 애플리케이션의 요구 사항에 맞춰 조정되며, 명령줄을 통한 수동 조정(command-line tuning)의 필요성을 줄여줍니다. 또한, 행동 기반 튜닝(behavior-based tuning)을 사용하여 애플리케이션의 특정 동작을 충족하도록 힙 크기를 동적으로 최적화할 수 있습니다.
이 섹션에서는 기본 선택 및 행동 기반 튜닝에 대해 설명합니다. 더 자세한 조정을 하기 전에, 먼저 이러한 기본값을 사용하는 것이 좋습니다.
Garbage Collector, Heap, and Runtime Compiler Default Selections
다음은 중요한 가비지 컬렉터(GC), 힙 크기(Heap Size), 런타임 컴파일러(Runtime Compiler)의 기본 선택 사항입니다.
- Garbage-First (G1) 가비지 컬렉터 사용
- GC 스레드의 최대 개수는 힙 크기와 사용 가능한 CPU 리소스에 의해 제한됨
- 초기 힙 크기(Initial Heap Size): 물리적 메모리의 1/64
- 최대 힙 크기(Maximum Heap Size): 물리적 메모리의 1/4
- Tiered 컴파일러 사용 (C1과 C2를 모두 활용)
Behavior-Based Tuning
Java HotSpot VM의 가비지 컬렉터는 최대 일시 중지 시간(maximum pause-time)과 애플리케이션 처리량(application throughput) 중 하나의 목표를 우선적으로 충족하도록 구성할 수 있습니다.
우선적인 목표가 충족되면, 가비지 컬렉터는 다른 목표도 최대화하려고 시도합니다. 물론, 이러한 목표들이 항상 충족될 수 있는 것은 아닙니다.
애플리케이션은 최소한 모든 살아 있는 데이터(live data)를 저장할 수 있는 최소 힙 크기(minimum heap size) 를 필요로 하며,
다른 구성 요소가 일부 또는 전체 목표 달성을 방해할 수도 있습니다.
Maximum Pause-Time Goal
일시 중지 시간(Pause Time)은 가비지 컬렉터가 애플리케이션을 중지하고, 더 이상 사용되지 않는 공간을 회수하는 데 걸리는 시간입니다.
최대 일시 중지 시간 목표(Maximum Pause-Time Goal)의 목적은 이러한 일시 중지 중 가장 긴 시간을 제한하는 것입니다.
가비지 컬렉터는 평균 일시 중지 시간과 그 분산(variance)을 유지합니다.
이 평균 값은 실행이 시작된 시점부터 계산되지만, 최신 일시 중지 시간이 더 큰 가중치(weighted)를 가집니다.
만약 평균 일시 중지 시간 + 분산(variance)이 최대 일시 중지 시간 목표보다 크다면, 가비지 컬렉터는 이 목표가 충족되지 않았다고 판단합니다.
최대 일시 중지 시간 목표는 명령줄 옵션 -XX:MaxGCPauseMillis=<nnn> 을 통해 지정됩니다. 이 값은 가비지 컬렉터에 <nnn> 밀리초 이하의 일시 중지 시간이 필요하다는 힌트로 해석됩니다.
가비지 컬렉터는 GC 일시 중지 시간을 <nnn> 밀리초 이하로 유지하려고 시도하면서, 자바 힙 크기 및 GC 관련 다른 매개변수를 조정합니다.
각 가비지 컬렉터마다 기본 최대 일시 중지 시간 목표 값은 다릅니다. 이러한 조정 과정에서 GC가 더 자주 실행될 수 있으며, 그로 인해 애플리케이션의 전체 처리량(throughput)이 감소할 수도 있습니다.
일부 경우에는, 요구된 일시 중지 시간 목표가 충족되지 않을 수도 있습니다.
Throughput Goal
처리량 목표는 가비지 컬렉션에 사용된 시간의 측정값이며, 가비지 컬렉션 외의 시간은 애플리케이션 실행 시간입니다.
이 목표는 명령줄 옵션 -XX:GCTimeRatio=nnn 으로 지정됩니다. 가비지 컬렉션 시간과 애플리케이션 실행 시간의 비율은 1 / (1 + nnn) 입니다.
예를 들어, -XX:GCTimeRatio=19로 설정하면, 총 실행 시간의 1/20 또는 5%가 가비지 컬렉션에 사용되도록 목표를 설정하는 것입니다.
가비지 컬렉션에 사용된 시간은 모든 가비지 컬렉션으로 인해 발생한 일시 중지 시간의 총합입니다.
만약 처리량 목표가 충족되지 않는다면, 가비지 컬렉터가 힙 크기를 증가시켜, 가비지 컬렉션 사이의 애플리케이션 실행 시간을 더 길게 유지하려고 할 수 있습니다.
Footprint
처리량 목표(Throughput Goal)와 최대 일시 중지 시간 목표(Maximum Pause-Time Goal)가 충족되면,
가비지 컬렉터는 힙 크기를 줄여서 목표를 유지하려고 합니다. 그러나 결국 처리량 목표가 충족되지 않는 지점까지 힙 크기를 줄이게 됩니다.
가비지 컬렉터가 사용할 수 있는 최소 및 최대 힙 크기는 각각 -Xms=<nnn>(최소 힙 크기) 와 -Xmx=<mmm>(최대 힙 크기) 옵션을 사용하여 설정할 수 있습니다.
Tuning Strategy
JVM은 설정된 처리량 목표(throughput goal)를 유지하기 위해 힙 크기를 자동으로 증가하거나 감소시킵니다.
이를 효과적으로 관리하려면, 최대 힙 크기 및 일시 중지 시간 목표 설정과 같은 힙 튜닝 전략(heap tuning strategies)을 고려해야 합니다.
힙 크기 설정 시 주의할 점
- 최대 힙 크기(-Xmx)를 무조건 설정하지 말아야 합니다.
- 기본값보다 큰 힙이 필요할 때만 설정하는 것이 좋습니다.
- 불필요하게 큰 힙을 설정하면 메모리 낭비 및 성능 저하가 발생합니다.
- 애플리케이션에 적절한 처리량 목표를 설정합니다.
- 너무 높은 처리량 목표를 설정하면 GC가 자주 실행되어 성능이 오히려 떨어질 수 있습니다.
힙 크기 조정이 필요한 경우
- 애플리케이션이 더 많은 객체를 빠르게 할당할 경우, 처리량을 유지하기 위해 힙 크기가 증가합니다.
- 만약 힙이 최대 크기까지 증가했음에도 처리량 목표를 충족하지 못한다면,최대 힙 크기(-Xmx)가 너무 작을 가능성이 높습니다.
- 해결 방법
힙 크기를 플랫폼의 물리적 메모리에 가깝게 설정하되,메모리 스와핑(swapping)이 발생하지 않도록 주의해야 합니다.
- 해결 방법
처리량과 일시 중지 시간 목표의 균형
- 만약 처리량 목표를 충족할 수 있지만, GC로 인해 일시 중지 시간이 너무 길다면 최대 일시 중지 시간(-XX:MaxGCPauseMillis)을 설정하는 것이 좋습니다.
- 일시 중지 시간을 짧게 설정하면 처리량이 감소할 수 있습니다 → 적절한 균형을 찾아야 합니다.
힙 크기의 변동(진동) 현상
- GC는 처리량 목표(더 큰 힙 필요) vs 일시 중지 시간 목표(더 작은 힙 필요) 사이에서 균형을 맞추려고 합니다.
- 따라서, 애플리케이션이 안정적인 상태(steady state)에 도달하더라도, 힙 크기가 계속 증가하거나 감소하는 것은 정상적인 현상입니다.
정리
- Java HotSpot VM은 다양한 애플리케이션을 지원하기 위해 여러 가지 가비지 컬렉터(GC)를 제공합니다.
- GC는 동적 메모리 할당을 자동으로 관리하며, 사용되지 않는 메모리를 회수하여 재사용할 수 있도록 합니다.
- GC는 최대 일시 중지 시간(Max Pause Time)과 처리량(Throughput) 목표 중 하나를 우선적으로 최적화합니다.
- 최대 일시 중지 시간을 제한하려면 -XX:MaxGCPauseMillis=<nnn> 옵션을 사용합니다.
- 처리량 목표를 설정하려면 -XX:GCTimeRatio=nnn 옵션을 사용하며, GC 시간과 애플리케이션 실행 시간 비율을 조정합니다.
- 힙 크기는 JVM이 자동 조정하지만, 필요에 따라 -Xms(최소 힙 크기)와 -Xmx(최대 힙 크기)를 설정할 수 있습니다.
- 힙 크기는 처리량 목표를 충족하는 범위 내에서 줄어들지만, 결국 처리량을 유지할 수 없는 지점까지 감소합니다.
- 멀티코어 서버에서는 기본적으로 G1(Garbage-First) GC가 선택되며, 작은 애플리케이션에는 Serial GC가 적절합니다.
- GC 튜닝 시, 지나치게 높은 처리량 목표를 설정하면 GC가 자주 실행되어 성능이 오히려 저하될 수 있습니다.
- GC는 처리량과 일시 중지 시간 목표 간 균형을 유지하려고 하며, 힙 크기가 일정하게 변동하는 것은 정상적인 현상입니다.