JAVA (22) - 가비지 컬렉션
1. 개요
자바(Java)는 C나 C++과 달리 개발자가 객체의 메모리를 직접 해제하지 않는다. JVM(Java Virtual Machine)의 가비지 컬렉터(Garbage Collector)가 더 이상 사용할 수 없는 객체를 찾아 메모리를 자동으로 회수하기 때문이다.
자동 메모리 관리는 개발 편의성을 높여 주지만 모든 메모리 문제를 해결해 주는 것은 아니다. 객체가 계속 참조되고 있거나 Heap 공간이 부족하면 가비지 컬렉션이 동작하더라도 OutOfMemoryError가 발생할 수 있으므로 기본 원리를 이해할 필요가 있다.
2. 가비지 컬렉션(GC)의 기본 개념
가비지 컬렉션은 JVM의 Heap 영역에서 더 이상 도달할 수 없는(Unreachable) 객체를 찾아 해당 메모리를 회수하는 과정이다. 단순히 변수에 null을 대입했는지가 아니라, 실행 중인 프로그램이 객체까지 이어지는 참조 경로를 가지고 있는지를 기준으로 판단한다.
- Reachable: GC Root에서 참조를 따라가 도달할 수 있는 객체다.
- Unreachable: GC Root에서 어떤 참조 경로로도 도달할 수 없어 회수 대상이 된 객체다.
GC Root의 대표적인 예로는 실행 중인 스레드의 스택에 있는 지역 변수, 정적(static) 필드가 참조하는 객체, JNI 참조 등이 있다.
Member member = new Member();
member = null;
위 코드에서 생성된 Member 객체를 가리키는 다른 참조가 없다면 GC 대상이 될 수 있다. 다만 대상이 되었다고 해서 즉시 메모리에서 제거되는 것은 아니며, 실제 회수 시점은 JVM이 결정한다.
3. GC의 메모리 회수 과정
가비지 컬렉터마다 구현 방식은 다르지만, 메모리를 회수하는 원리는 다음 세 가지 작업으로 이해할 수 있다.
- Mark(표시): GC Root에서 참조를 따라가며 살아 있는 객체를 식별한다.
- Sweep(제거): 살아 있지 않은 객체가 차지하던 메모리를 회수한다.
- Compact(압축): 살아 있는 객체를 한쪽으로 모아 메모리 파편화를 줄인다.
모든 GC가 항상 위 세 단계를 같은 방식으로 수행하는 것은 아니다. 일부 컬렉터는 살아 있는 객체를 다른 공간으로 복사하거나 이동하는 방식(Evacuation)을 사용한다. 핵심은 도달 가능한 객체는 남기고, 도달할 수 없는 객체의 공간은 다시 사용할 수 있게 만드는 것이다.
Stop-the-World
GC 과정 중 JVM이 애플리케이션 스레드의 실행을 일시적으로 멈추는 현상을 Stop-the-World(STW) 라고 한다. 이 시간에는 GC 작업을 수행하는 스레드를 제외한 애플리케이션 스레드가 작업을 진행하지 못한다.
최신 가비지 컬렉터는 많은 작업을 애플리케이션과 동시에 수행하여 중단 시간을 줄이지만, 객체 참조를 안전하게 확인해야 하는 일부 단계에서는 STW가 발생할 수 있다. 따라서 GC 튜닝에서는 처리량(Throughput)뿐 아니라 중단 시간(Pause Time)도 중요한 기준이 된다.
4. 약한 세대 가설과 Heap 구조
세대별 가비지 컬렉션(Generational GC)은 다음과 같은 약한 세대 가설(Weak Generational Hypothesis) 을 바탕으로 한다.
- 새로 생성된 객체 대부분은 짧은 시간 안에 사용되지 않게 된다.
- 오래 살아남은 객체가 새 객체를 참조하는 경우는 상대적으로 적다.
이 가설에 따라 Heap을 객체의 생존 기간별로 나누면 매번 전체 Heap을 검사하지 않고, 객체가 많이 사라지는 영역을 자주 정리할 수 있다.
| 영역 | 역할 |
|---|---|
| Eden | 새로 생성된 객체가 주로 할당되는 공간이다. |
| Survivor 0 / 1 | Young GC에서 살아남은 객체가 이동하는 공간이다. 두 영역을 번갈아 사용한다. |
| Old Generation | 여러 번의 Young GC에서 살아남은 객체가 승격(Promotion)되는 공간이다. |

Eden과 Survivor 영역으로 구성된 Young Generation 및 Old Generation의 구조
참고: 그림의 Permanent Generation(PermGen)은 과거 HotSpot JVM의 구조다. JDK 8부터 PermGen이 제거되고 네이티브 메모리를 사용하는 Metaspace로 대체되었으므로, 그림에서는 Young Generation과 Old Generation의 관계를 중심으로 살펴본다.
전통적인 세대별 Heap 구조에서는 Eden과 두 Survivor 공간이 Young Generation을 구성한다. G1 GC에서는 Heap이 물리적으로 연속된 세대 공간으로 고정되는 대신 여러 Region이 Eden, Survivor, Old 역할을 논리적으로 담당한다.
5. 세대별 가비지 컬렉션 과정
일반적인 세대별 GC의 객체 이동 과정은 다음과 같다.
- 새로운 객체가 주로 Eden 영역에 생성된다.
- Eden 영역에 공간이 부족해지면 Young Generation을 대상으로 GC가 실행된다.
- 도달할 수 없는 객체는 제거되고 살아남은 객체는 Survivor 영역으로 이동한다.
- 다음 GC에서는 Eden과 사용 중인 Survivor 영역의 생존 객체가 반대쪽 Survivor 영역으로 이동하며 객체의 나이(Age)가 증가한다.
- 일정 횟수 이상 살아남았거나 Survivor 공간이 부족하면 객체가 Old Generation으로 승격된다.
- Old 영역도 회수할 공간이 필요해지면 해당 영역을 포함하는 GC가 실행된다.
객체 생성
↓
Eden ── Young GC ──→ Survivor 0 ↔ Survivor 1
↓ 반복 생존
Old Generation
S0와 S1 중 한쪽은 생존 객체를 받는 공간으로 사용하고, 복사가 끝나면 역할을 바꾼다. 따라서 일반적으로 두 Survivor 영역을 동시에 생존 객체 저장 공간으로 사용하지 않는다.
6. Young GC, Major GC, Full GC의 차이
| 구분 | 주된 대상 | 특징 |
|---|---|---|
| Young GC(Minor GC) | Young Generation | 수명이 짧은 객체가 많아 비교적 자주 실행된다. |
| Major GC 또는 Old GC | 주로 Old Generation | 용어와 동작 범위가 컬렉터에 따라 달라질 수 있다. |
| Full GC | 일반적으로 전체 Heap | 대체로 중단 시간이 길어질 가능성이 크다. |
Major GC와 Full GC는 흔히 같은 의미로 사용되지만 엄밀히 항상 동일한 것은 아니다. 실제 동작 범위와 명칭은 사용하는 가비지 컬렉터 및 GC 로그를 기준으로 확인해야 한다.
7. 대표적인 가비지 컬렉터
자바는 애플리케이션의 규모와 성능 목표에 따라 여러 가비지 컬렉터를 제공한다.
- Serial GC: 하나의 GC 스레드를 사용한다. 데이터 규모가 작고 단순한 환경에 적합하다.
- Parallel GC: 여러 GC 스레드를 사용하여 전체 처리량을 높이는 데 초점을 둔다.
- G1 GC(Garbage-First GC): Heap을 Region으로 나누고 회수 효율이 높은 영역을 우선 처리한다. 중단 시간과 처리량의 균형을 목표로 하며, 현재 HotSpot JVM의 대부분 환경에서 기본 컬렉터로 선택된다.
- ZGC: 대부분의 작업을 애플리케이션과 동시에 수행하여 매우 짧은 중단 시간을 목표로 하는 저지연 컬렉터다.
8. GC가 있어도 메모리 누수가 발생하는 이유
가비지 컬렉터는 도달할 수 없는 객체만 회수한다. 더 이상 사용하지 않는 객체라도 컬렉션, 정적 필드, 리스너 등이 계속 참조하고 있으면 Reachable 상태로 판단되어 제거되지 않는다.
private static final List<Member> members = new ArrayList<>();
위 목록에 객체를 계속 추가하고 제거하지 않으면 모든 객체가 정적 필드를 통해 계속 참조된다. 이런 논리적 메모리 누수가 누적되면 GC가 반복해서 실행되어도 충분한 공간을 확보하지 못하고 OutOfMemoryError가 발생할 수 있다.
9. 정리
가비지 컬렉션은 GC Root에서 도달할 수 없는 Heap 객체를 찾아 메모리를 회수하는 JVM의 자동 메모리 관리 기능이다. 객체를 표시하고 제거하며 필요에 따라 압축하거나 이동하는 방식으로 사용 가능한 공간을 확보한다.
세대별 GC는 대부분의 새 객체가 빠르게 사라진다는 가설을 이용해 Young 영역을 자주 정리하고, 오래 살아남은 객체를 Old 영역으로 승격한다. 다만 GC가 모든 메모리 문제를 해결해 주는 것은 아니므로 객체의 참조 관계와 애플리케이션의 중단 시간도 함께 살펴봐야 한다.