자바의 가비지 콜렉터는 어떻게 돌아갈까?
목차
GC(Garbage Collection란 무엇인가?
자바에서 객체는 new
키워드로 생성되며 힙(heap) 메모리 공간에 저장된다.
그런데 사용하지 않는 객체들이 계속 쌓인다면 메모리가 부족해질 것이다.
그래서 자바는 Garabage Collecting(쓰레기 수집)이라는 자동 메모리 관리 기능을 통해 더 이상 필요 없는 객체를 찾아내어 메모리에서 제거한다.
GC의 기본 구조 (객체 생존 주기에 따른 Generational GC)
자바의 GC는 객체의 생존 주기에 따라 메모리를 나누고, 각 영역별로 최적화된 방식을 적용한다. 이것이 바로 Generational GC(세대별 GC)이다.
세대별 메모리 영역
- Young Generation (젊은 세대): 새로 생성된 객체들이 저장된다. 대부분 여기서 소멸한다.
- Old Generation (늙은 세대): 여러 차례 GC를 거쳐 살아남은 장기 객체가 저장된다.
Young Generation 내부 구조: Eden과 Survivor
Young Generation은 다시 세 공간으로 나누어져 있다.
- Eden 공간: 새롭게 생성된 객체들이 처음 위치하는 곳이다.
- Survivor 공간: Eden에서 살아남은 객체들이 번갈아 가면서 저장되는 두 공간(Survivor 0, Survivor 1)이 있다.
GC는 어떻게 동작하는가(더 이상 접근 불가한 객체를 어떻게 찾아내는가)
자바 GC의 핵심: Mark-and-Sweep과 도달 가능성
자바는 메모리 관리에서 객체가 여전히 사용 중인지 아닌지를 판단할 때, 참조 수를 세는 방식 대신 도달 가능성(Reachability)에 기반한 방식을 채택했다.
도달 가능성이란, 프로그램 실행 중 특정 객체가 GC Root(예: 현재 실행 중인 스레드의 스택 변수, static 변수, JNI 등)로부터 참조 경로가 존재하는지를 의미한다.
- Reachable 객체: GC Root에서 직접 또는 간접적으로 참조 가능한 객체다. 즉, 프로그램에서 여전히 사용할 수 있는 객체이므로 메모리에서 제거하지 않는다.
- Unreachable 객체: 어떤 경로로도 GC Root에서 접근할 수 없는 객체다. 프로그램에서 더 이상 참조하지 않는 객체로 판단하여 메모리에서 해제 대상이 된다.
이 도달 가능성을 판단하는 대표적인 알고리즘이 바로 Mark-and-Sweep아다.
- Mark(표시) 단계: GC Root에서 시작해 도달 가능한 모든 객체를 탐색하면서 ‘살아있음’을 표시한다.
- Sweep(정리) 단계: 표시되지 않은 객체들, 즉 도달 불가능한 객체를 힙에서 제거해 메모리를 해제한다.
이 방식은 순환 참조(Circular Reference)가 있어도 문제없이 동작하기 때문에 자바 GC의 핵심 원리로 자리잡았다.
번외: 참조 수 기반 메모리 관리 (Reference Counting)
참조 수 기반 방식은 각 객체가 몇 번 참조되는지 카운트를 유지하며, 참조될 때마다 1씩 증가하고 참조가 끊기면 1씩 감소시킨다. 참조 수가 0이 되면 객체를 즉시 해제한다.
하지만 이 방식에는 다음과 같은 단점이 있다.
- 순환 참조 문제: 서로 참조하는 두 객체가 있을 경우, 참조 수가 0이 되지 않아 메모리가 해제되지 않는 상황이 발생할 수 있다.
이러한 한계 때문에 자바는 참조 수 기반 대신, 객체가 프로그램에서 도달 가능한지 여부(Reachability)에 따라 메모리를 관리하는 Mark-and-Sweep 방식을 선택했다.
Minor GC와 Major GC의 시점과 과정
Minor GC
Young Generation 내 Eden 공간이 꽉 차면 Minor GC가 발생한다. 이때 Eden과 현재 활성화된 한쪽 Survivor 공간에 있는 살아남은(Reachable) 객체들을 반대편 Survivor 공간으로 복사한다.
Survivor 공간은 두 개가 있으며, 매번 Minor GC 때마다 읽기와 쓰기 공간이 번갈아 바뀐다.
- 1번째 Minor GC: Eden + Survivor0 → Survivor1
- 2번째 Minor GC: Eden + Survivor1 → Survivor0
- 이후 이 과정을 반복
age-bit: 객체 생존 횟수 체크와 승격
살아남은 객체는 Minor GC를 거칠 때마다 age-bit (생존 카운터)가 1씩 증가한다. 이 값이 일정 기준을 넘으면 해당 객체는 Young Generation에서 Old Generation으로 승격(promotion)되어, 장기 객체로 관리된다.
Major GC (Full GC)
Old Generation이 가득 차거나, 명시적으로 Full GC가 요청되면 Major GC(또는 Full GC)가 발생한다. Major GC는 Old Generation과 Young Generation 전체를 대상으로 수행되며, 다음 특징이 있다.
- Young + Old 영역의 모든 unreachable 객체를 정리
- Minor GC보다 훨씬 무겁고 느림
- 시스템 전체가 멈추는 Stop-The-World(STW) 시간이 길어져 애플리케이션 성능에 큰 영향
따라서 Major GC는 자주 발생하지 않도록 메모리 튜닝과 최적화가 중요하다.
Minor GC vs Major (Full) GC: 속도와 영향 차이
구분 | Minor GC | Major (Full) GC |
---|---|---|
대상 | Young Generation | Old Generation |
발생 시점 | Eden 공간이 가득 찰 때 | Old 공간이 가득 차거나 Full GC 요청 시 |
정지 시간 | 짧은 Stop-The-World(STW) 발생 | 긴 Stop-The-World 발생 |
속도 | 빠름 (수 ms 이내) | 느림 (수백 ms ~ 수 초) |
Stop-The-World(STW)란?
GC가 실행되는 동안 자바 애플리케이션의 모든 스레드가 잠시 멈추는 현상을 말한다. 이 시간이 길어질수록 애플리케이션 반응성이 떨어지고 사용자 경험에 영향을 준다. Minor GC는 자주 발생하지만 짧게 멈추고, Major GC는 적게 발생하지만 멈춤 시간이 길어 성능에 더 큰 영향을 미친다.
- Stop-The-World(STW)란 GC가 실행되는 동안 애플리케이션의 모든 작업이 일시적으로 멈추는 현상이다.
- Minor GC도 Young Generation 영역을 정리할 때 STW가 발생하지만, 대상 영역이 작고 작업이 빨라 정지 시간이 매우 짧다.
- 반면 Major GC는 Old Generation 전체를 대상으로 하므로 STW 시간이 길어 사용자 경험에 큰 영향을 줄 수 있다.
- 따라서 Minor GC는 자주 발생하지만 비용이 적고, Major GC는 적게 발생하나 성능에 미치는 영향이 크기 때문에 Major GC 발생을 최소화하는 것이 중요하다.