Java 가비지 컬렉션
자바 가비지 컬렉션의 동작 알고리즘
자바의 가비지 컬렉션은 더 이상 사용되지 않는 객체들을 자동으로 메모리에서 제거하는 JVM의 핵심 기능이다. 가비지 컬렉션은 개발자가 수동으로 메모리 관리를 하지 않아도 되도록 하여 메모리 누수를 방지하고 개발 생산성을 향상시킨다.
약한 세대 가설 (Weak Generational Hypothesis)
자바의 가비지 컬렉션은 약한 세대 가설이라는 전제를 기반으로 설계되었다. 이 가설은 다음 두 가지 관찰에 기반한다.
- 대부분의 객체는 금방 접근 불가능한 상태(Unreachable)가 된다.
- 오래된 객체에서 새로운 객체로의 참조는 매우 적게 존재한다.
이러한 가설에 따라 JVM은 힙 메모리를 Young Generation과 Old Generation으로 분할하여 효율적인 메모리 관리를 수행한다.
기본 가비지 컬렉션 알고리즘
Mark-and-Sweep 알고리즘
가비지 컬렉션의 가장 기본적인 알고리즘으로, Mark 단계와 Sweep 단계로 구성된다.
Mark 단계:
- 가비지 컬렉터는 가비지 루트를 찾아 시작한다.
- 루트에는 스택의 메소드와 변수, 정적 변수와 메소드, 상수, 네이티브 스택에서 사용되는 객체들이 포함된다.
- 루트에서 시작하여 참조 체인을 따라 도달 가능한 모든 객체(reachable)에 마크 비트를 1(true)로 설정한다.
- 도달할 수 없는 객체들(unreachable)은 마크 비트가 0(false)로 남아있다.
Sweep 단계:
- 힙을 순회하면서 마크 비트가 0인 객체들을 메모리에서 제거한다.
- 살아남은 객체들의 마크 비트는 다시 false로 초기화하여 다음 GC 사이클을 준비한다.
Mark-Sweep-Compact 알고리즘
Mark-and-Sweep 알고리즘의 메모리 단편화 문제를 해결하기 위해 고안된 알고리즘이다.
Compact 단계:
- 정리 작업 후 남은 객체들을 힙의 한쪽 끝으로 이동시켜 연속된 메모리 공간을 확보한다.
- 모든 객체 참조를 새로운 위치로 업데이트한다.
- 시간과 자원 소모가 큰 대신 메모리 단편화를 해결한다.
Generational GC
Young Generation 구조
Young Generation은 다음 세 영역으로 구성된다.
- Eden Space: 새로운 객체가 할당되는 공간
- Survivor Space 1 (S0): 첫 번째 생존자 공간
- Survivor Space 2 (S1): 두 번째 생존자 공간
Minor GC 과정
- 새 객체 할당: 모든 새 객체가 Eden 공간에 할당된다.
- Eden 공간 포화: Eden이 가득 차면 Minor GC가 시작된다.
- 생존 객체 이동: 참조되는 객체들은 첫 번째 Survivor 공간(S0)으로 이동한다.
- Age 증가: 다음 Minor GC에서 생존한 객체들은 두 번째 Survivor 공간(S1)으로 이동하며 Age가 증가한다.
- Survivor 공간 교체: 매 Minor GC마다 두 Survivor 공간의 역할이 바뀐다.
- Promotion: Age가 특정 임계값에 도달한 객체들은 Old Generation으로 승급된다.
Major GC
Old Generation이 가득 차면 Major GC가 발생하여 Old Generation 전체를 정리한다. Major GC는 Minor GC보다 훨씬 오래 걸리므로 최소화해야 한다.
주요 GC 알고리즘 종류
Serial GC
- 단일 스레드로 동작하는 가장 단순한 GC
- Young Generation에는 Mark-Copy, Old Generation에는 Mark-Sweep-Compact 사용
- 소규모 단일 스레드 환경에 적합
Parallel GC
- 여러 스레드를 사용하여 처리 속도를 향상시킨 GC
- Java 9 이전의 기본 GC였으며, 처리량 개선에 중점
- GC 수행 중에는 애플리케이션 스레드가 정지됨
G1 GC (Garbage First)
Java 9부터 기본 GC로 채택된 고급 알고리즘이다.
- 힙 분할: 힙을 동일한 크기의 작은 지역들로 분할
- SATB 알고리즘: Snapshot-At-The-Beginning 알고리즘을 사용한 마킹
- 목표 정지 시간: 사용자가 설정한 목표 정지 시간 내에서 동작
- Mixed GC: Young과 Old Generation을 함께 수집하는 방식
ZGC (Z Garbage Collector)
Java 11에서 도입된 초저지연 GC
- 10ms 미만 정지 시간: 대용량 힙에서도 10ms 미만의 정지 시간 목표
- Colored Pointers: 객체 추적을 위한 고급 기법 사용
- Load Barriers: 데이터 일관성을 위한 메모리 연산 제어
- 32GB 이상 대용량 힙에 최적화되어 있다.
Shenandoah GC
Red Hat에서 개발한 저지연 GC
- 동시 압축: 애플리케이션과 동시에 메모리 압축 수행
- 참조 업데이트: 객체 이동 후 참조를 동시에 업데이트
- 힙 크기와 무관한 정지 시간: 200GB나 2GB 힙 모두 유사한 저지연 동작
Stop-The-World 현상
모든 GC는 Stop-The-World 이벤트를 발생시킨다. 이는 GC가 실행되는 동안 모든 애플리케이션 스레드가 정지되는 현상으로, 특히 Major GC에서 더 오래 지속된다. 최신 GC 알고리즘들(G1, ZGC, Shenandoah)은 이러한 정지 시간을 최소화하는 것을 목표로 한다.
자바의 가비지 컬렉션은 Weak Generational Hypothesis을 기반으로 한 generation별 메모리 관리와 다양한 알고리즘을 통해 효율적인 자동 메모리 관리를 제공하며, 애플리케이션의 요구사항에 따라 적절한 GC 알고리즘을 선택할 수 있다.
개발자를 위한 GC 최적화 방법
백엔드 개발자로서 GC 시간을 줄이고 메모리 효율성을 향상시킬 수 있는 실무적인 방법들이 있다.
1. 메모리 사용량 최적화
메모리 사용을 줄이는 것이 GC 최적화의 가장 기본적이면서도 효과적인 방법이다. 메모리 사용량이 적을수록 GC가 처리해야 할 객체 수가 줄어들어 전체적인 GC 시간이 단축된다.
- 객체 재사용: StringBuilder, StringBuffer와 같은 객체를 재사용하여 불필요한 객체 생성을 방지
- 컬렉션 크기 예측: ArrayList, HashMap 등의 초기 크기를 적절히 설정하여 내부 배열 재할당 최소화
- 메모리 누수 방지: 불필요한 객체 참조를 null로 설정하여 조기 가비지 컬렉션 유도
2. 대량 객체 생성 방지
한번에 대량으로 객체를 생성하는 것을 주의해야 한다. 대량의 객체가 동시에 생성되면 Eden 공간이 빠르게 가득 차서 빈번한 Minor GC를 유발하고, 경우에 따라서는 Old Generation으로의 조기 승급을 야기할 수 있다.
특히 파일 다운로드와 같은 기능을 구현할 때는 스트림을 활용하는 것이 중요하다
// 비효율적인 방법 - 전체 파일을 메모리에 로드
byte[] fileData = Files.readAllBytes(Paths.get("largefile.zip")); // 대량 객체 생성
// 효율적인 방법 - 스트림을 사용한 청크 단위 처리
try (InputStream inputStream = Files.newInputStream(Paths.get("largefile.zip"));
OutputStream outputStream = Files.newOutputStream(Paths.get("output.zip"))) {
byte[] buffer = new byte[8192]; // 작은 버퍼 재사용
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
3. 추가 최적화 기법
- 적절한 GC 알고리즘 선택: 애플리케이션 특성에 맞는 GC 알고리즘 선택 (처리량 vs 지연시간)
- 힙 크기 튜닝: Xms, Xmx 옵션을 통한 적절한 힙 크기 설정
- GC 모니터링: JVisualVM, GCeasy 등의 도구를 활용한 GC 성능 모니터링