Java 가비지 컬렉션(Garbage Collection) 작동 원리와 알고리즘

지난 글에서는 JVM의 핵심 메모리 영역인 스택(Stack)과 힙(Heap) 영역의 구조에 대해 알아보았습니다. new 연산자를 통해 생성된 객체들은 모두 힙 영역에 저장된다고 설명해 드렸는데요. 그렇다면 이 힙 영역에 쌓이는 객체들은 언제, 어떻게 메모리에서 해제될까요?

C나 C++ 같은 언어에서는 개발자가 직접 free() 같은 함수를 호출해 메모리를 해제해야 합니다. 반면, 자바(Java)에서는 가비지 컬렉터(GC, Garbage Collector)가 더 이상 사용되지 않는 메모리를 알아서 청소해 줍니다.

백엔드 개발자에게 GC를 이해하는 것은 매우 중요합니다. GC가 작동하는 동안 애플리케이션이 멈추는 현상이 발생하기 때문입니다. 이번 글에서는 가비지 컬렉션의 핵심 원리인 Stop-the-World, Weak Generational Hypothesis, 그리고 대표적인 GC 알고리즘들을 상세히 파헤쳐 보겠습니다.

1. 가비지 컬렉션(GC)의 기본 원리: Reachability

가비지 컬렉터는 힙 메모리에 있는 객체 중 ‘쓰레기(Garbage)’를 어떻게 판별할까요? 자바는 Reachability(도달 가능성)라는 개념을 사용합니다.

  • Reachable (도달 가능한 객체): 현재 자바 애플리케이션에서 참조되고 있는 객체입니다. 메모리에 유지되어야 합니다.
  • Unreachable (도달 불가능한 객체): 그 어떤 곳에서도 참조하고 있지 않은 객체입니다. 바로 이 객체들이 GC의 청소 대상(Garbage)이 됩니다.

JVM Stack 영역의 변수나 메서드 영역의 상수 풀 등에서 시작하여 힙 영역의 객체를 참조하는 연결 고리를 ‘Root Set’이라고 부릅니다. 이 Root Set에서부터 화살표를 따라가 도달할 수 없는 객체들은 아무리 용량이 커도 쓰레기로 간주되어 메모리에서 사라지게 됩니다.

2. 가비지 컬렉션의 대전제: 가설과 힙 영역의 분리

가비지 컬렉터는 힙 영역의 모든 객체를 매번 전부 검사하지 않습니다. 만약 수 기가바이트(GB)에 달하는 힙 메모리를 매번 전수 조사한다면 서버가 엄청나게 느려질 것입니다. 이를 해결하기 위해 엔지니어들은 한 가지 통계적 사실에 주목했습니다.

2-1. 약한 세대 가설 (Weak Generational Hypothesis)

가비지 컬렉션은 다음 두 가지 가설을 기반으로 설계되었습니다.

  1. 대부분의 객체는 생성된 지 얼마 되지 않아 곧 도달 불가능(Unreachable) 상태가 된다.
  2. 오래된 객체에서 젊은 객체로의 참조는 아주 드물게 일어난다.

즉, 90% 이상의 객체는 메서드 안에서 잠시 쓰이고 금방 쓰레기가 된다는 뜻입니다. 이 가설을 바탕으로 JVM 힙 영역은 Young GenerationOld Generation으로 나뉘게 되었습니다.

2-2. Minor GC와 Major GC

  • Young Generation: 새롭게 생성된 객체들이 머무는 곳입니다. 대부분의 객체가 여기서 생성되었다가 금방 사라집니다. 이 영역에서 발생하는 가비지 컬렉션을 Minor GC라고 부르며, 속도가 매우 빠릅니다.
  • Old Generation: Young 영역에서 살아남은(계속 참조되고 있는) 객체들이 이동하는 곳입니다. Young 영역보다 크기가 크게 할당되며, 이곳의 메모리가 가득 차면 발생하는 가비지 컬렉션을 Major GC(또는 Full GC)라고 합니다. Major GC는 영역이 큰 만큼 시간이 훨씬 오래 걸립니다.

3. GC의 공통 동작 단계: Mark and Sweep

거의 모든 가비지 컬렉션 알고리즘은 가장 기본적인 ‘Mark and Sweep(마크 앤 스윕)’ 과정을 거칩니다.

  1. Mark (마킹): Root Set으로부터 연결된 모든 객체를 탐색하며, 사용 중인 객체에 ‘Mark(표시)’를 합니다. 이 과정에서 메모리를 사용 중인 객체와 사용하지 않는 객체가 식별됩니다.
  2. Sweep (스윕): 마킹되지 않은, 즉 도달 불가능한(Unreachable) 객체들을 힙 메모리에서 쓸어 담아 삭제합니다.
  3. Compact (압축): (일부 알고리즘에서 수행) 객체들이 삭제되고 남은 빈 공간들을 채우기 위해, 살아남은 객체들을 메모리의 한쪽 끝으로 차곡차곡 모읍니다. 이를 통해 메모리 파편화(Fragmentation)를 방지합니다.

4. 백엔드 성능의 핵심: Stop-the-World (STW)

가비지 컬렉션을 이해할 때 반드시 알아야 하는 용어가 바로 ‘Stop-the-World (STW)’입니다.

Stop-the-World는 GC를 실행하기 위해 JVM이 애플리케이션의 실행을 멈추는 현상을 말합니다. GC 스레드를 제외한 모든 애플리케이션 스레드가 작업의 중단(Stop) 상태에 들어갑니다. 어떤 GC 알고리즘을 사용하더라도 STW는 발생하며, 자바 성능 최적화(GC 튜닝)의 핵심은 바로 이 STW 시간을 최소한으로 줄이는 것입니다.

서버가 0.5초 동안 멈춘다면 사용자는 웹사이트가 느리다고 느낄 것이고, 수 초 동안 멈춘다면 시스템 장애로 이어질 수 있기 때문입니다.

Java 가비지 컬렉션(Garbage Collection) 작동 원리와 알고리즘

5. 실무에서 쓰이는 대표적인 GC 알고리즘 비교

자바가 발전하면서 STW 시간을 줄이기 위해 다양한 GC 알고리즘이 개발되었습니다.

5-1. Serial GC

  • 특징: 단일 스레드(1개)로 GC를 처리하는 가장 단순한 방식입니다.
  • 동작: 마크-스윕-컴팩트(Mark-Sweep-Compact) 알고리즘을 사용합니다.
  • 비고: 멀티코어 환경에서는 성능이 매우 떨어지므로 실무 서버에서는 절대 사용하지 않습니다. 대개 아주 작은 독립형 애플리케이션에서만 쓰입니다.

5-2. Parallel GC (Java 8 default)

  • 특징: Serial GC와 기본 원리는 같지만, Young 영역의 Minor GC를 멀티스레드로 처리합니다.
  • 효과: 코어가 여러 개일 때 유용하며, Serial GC보다 STW 시간이 줄어듭니다. Java 8의 기본 GC이기도 합니다.

5-3. G1 GC (Garbage First GC / Java 9+ default)

  • 특징: 바둑판 모양의 리전(Region)이라는 새로운 개념을 도입했습니다. 힙 영역을 일정한 크기의 작은 구역(Region)으로 쪼개어 가득 찬 구역부터 우선적으로 GC를 수행합니다.
  • 장점: 역할(Young/Old)이 고정되어 있지 않고 동적으로 변하므로 효율적입니다. 대용량 힙 메모리(4GB 이상)에서 매우 안정적이며 STW 시간이 획기적으로 짧습니다. Java 9 버전부터 현재까지 기본 GC로 채택되어 실무에서 가장 많이 쓰입니다.

5-4. ZGC (Java 15+ 정식 도입)

  • 특징: 대용량 힙(최대 수 TB)을 타깃으로 하는 초저지연(Ultra Low-Latency) GC입니다.
  • 장점: 힙 크기가 아무리 커도 STW 시간을 10ms(0.01초) 이하로 유지하는 혁신적인 알고리즘입니다. 최근 최신 자바 버전(Java 17, 21)을 사용하는 고성능 백엔드 시스템에서 적극적으로 도입하고 있습니다.

6. 결론 및 요약

자바의 가비지 컬렉션(GC)은 개발자가 메모리 해제라는 복잡한 작업에서 벗어나 비즈니스 로직에만 집중할 수 있게 해 준 고마운 기능입니다. 하지만 보이지 않는 곳에서 Stop-the-World라는 비용이 발생하고 있음을 항상 인지해야 합니다.

  • 대부분의 객체는 금방 소멸하므로 힙은 YoungOld로 나뉩니다.
  • GC는 Root Set에서 도달할 수 없는 객체를 찾아 메모리에서 청소합니다.
  • 실무 환경이나 대형 프로젝트에서는 주로 G1 GC나 최신 ZGC를 활용하여 시스템 멈춤 현상을 방지합니다.

코드를 작성할 때 객체를 무분별하게 생성하지 않고, 다 쓴 대용량 객체의 참조를 제때 끊어주는 작은 습관이 효율적인 가비지 컬렉션을 돕는 지름길입니다.

다음 글에서는 실제 실무 면접에서도 자주 출제되는 ‘자바의 동시성(Concurrency)과 멀티스레드 환경에서의 동기화 문제’에 대해 심도 있게 다루어 보겠습니다.