JVM (Java Virtual Machine)에서 가비지 컬렉션(GC)은 더 이상 사용하지 않는 객체를 메모리에서 자동으로 해제하여 메모리를 효율적으로 관리하는 핵심 기능이다.
=> 프로그래머가 직접 메모리를 해제할 필요 없이, JVM이 알아서 불필요한 메모리를 정리해주므로 메모리 누수(Memory Leak)를 방지하는 데 도움을 준다.
공통 과정
GC의 기본적인 메커니즘은 알고리즘의 종류와 관계없이 유사하다.
JVM 힙(Heap) 메모리에 할당된 모든 객체를 추적하여 어떤 객체가 사용되고 있는지(참조되고 있는지) 판단(Mark)
사용되지 않는(참조되지 않는) 객체들을 폐기하는 과정이다.(Sweep)
1. Mark
목적: 프로그램에서 "살아있는 객체(Live Object)", 즉 여전히 유효한 참조를 통해 접근 가능한 객체를 식별하는 단계이다.
동작 방식:
GC는 GC Roots라는 특별한 기준점부터 시작한다. GC Roots는 애플리케이션 실행에 필수적인 객체들이다.
a. 메서드 영역(Method Area)에 있는 클래스 레벨 변수(static 변수)
b. 현재 실행 중인 스레드의 스택(Stack) 영역에 있는 로컬 변수
c. JNI(Java Native Interface)를 통해 생성된 객체에 대한 참조
d. 모니터(Monitor)에 의해 잠겨있는 객체 등
GC는 이 GC Roots에서부터 시작하여 객체들 간의 참조 관계를 따라가면서 "살아있는 객체"들을 방문한다. => 마치 미로에서 출구(GC Roots)부터 시작하여 갈 수 있는 모든 길(참조)을 따라가는 것과 같다.
이렇게 방문된 객체들은 "살아있음"으로 표시(마킹)된다.
결과: Mark 단계가 끝나면, 힙 메모리 내의 모든 객체는 '살아있는 객체'로 마킹되거나, 아무런 마킹이 되지 않은 '죽은 객체(Dead Object)' 상태가 된다. 죽은 객체는 GC Roots로부터 어떤 경로로도 접근할 수 없는 객체들이다.
2. Sweep
목적: Mark 단계에서 '살아있음'으로 마킹되지 않은 객체들, 즉 '죽은 객체'들이 차지하고 있던 메모리 공간을 회수하는 단계이다.
동작 방식:
GC는 힙 메모리 전체를 순회하면서 Mark 단계에서 마킹되지 않은 객체들을 찾는다.
마킹되지 않은 객체가 발견되면, 해당 객체가 사용하던 메모리 공간을 '사용 가능한 공간'으로 표시하거나 연결 리스트에 추가하는 등의 방식으로 회수한다. 이 공간은 이후 새로운 객체를 할당하는 데 사용될 수 있다.
결과: '죽은 객체'들이 차지하고 있던 메모리가 회수되어 새로운 객체를 할당할 수 있는 여유 공간이 확보된다.
2.1 Compact (압축 단계 - 일부 GC 알고리즘 해당)
Mark와 Sweep 과정만으로는 회수된 메모리 공간들이 여기저기 흩어지게 되어 메모리 단편화(Fragmentation)가 발생할 수 있다. 메모리 단편화가 심해지면 충분히 큰 연속적인 메모리 공간이 없어 큰 객체를 할당하지 못하는 문제가 생길 수 있다.
일부 GC 알고리즘(예: Serial GC, Parallel GC)은 Sweep 단계 이후 Compact 단계를 추가로 수행하여 힙 메모리의 살아있는 객체들을 한 곳으로 모아 메모리 단편화를 제거하고 연속적인 빈 공간을 확보한다. 하지만 이 Compact 단계는 객체들을 이동시켜야 하므로 추가적인 비용과 시간을 소모한다. CMS GC는 Compact 과정을 수행하지 않으며, G1 GC는 Region 단위로 Compact를 수행한다.
JVM 힙 메모리 영역(Generational GC)
대부분의 JVM GC는 객체의 생명 주기가 짧다는 특성에 기반하여 세대별 가비지 컬렉션(Generational GC) 방식을 사용한다. 힙 메모리를 여러 영역으로 나누고, 영역별로 다른 GC 전략을 적용하여 효율성을 높인다.
Young Generation
새롭게 생성된 객체들이 대부분 이 영역에 할당한다.
빈번하게 GC가 발생한다. (Minor GC)
1.1 Eden: 새로운 객체가 최초로 할당되는 공간이다.
1.2 Survivor Spaces(S0, S1)
Eden 영역에서 Minor GC 후 살아남은 객체들이 이동하는 공간이다.
일반적으로 두 개의 서바이버 공간이 있으며, Minor GC 시 하나의 서바이버 공간은 비우고 다른 공간으로 살아남은 객체를 이동시키는 방식으로 동작한다.
여러 번의 Minor GC에서도 살아남은 객체는 Old 영역으로 이동할 후보가 된다.
Old Generation
Young 영역에서 여러 번의 Minor GC에서도 살아남아 오랫동안 유지될 것으로 예상되는 객체들이 이동하는 영역이다.
Young 영역보다 크고, GC(Major GC)는 Young 영역보다 덜 자주 발생하지만, 한 번 발생하면 처리할 객체가 많아 시간이 더 오래 걸릴 수 있다.
Permanent Generation (PermGen) / Metaspace(Java 8 부터 대체)
JVM의 클래스 로더(Class Loader)가 로드하는 클래스와 메서드에 대한 메타데이터, Static 변수, 상수 풀(Constant Pool) 정보 등이 저장되는 영역이다.
과거에는 PermGen이라고 불렸으며, 힙 영역의 일부로 크기가 고정되어 있어 OutOfMemoryError가 발생하는 주된 원인이 되기도 했다.
Metaspace는 네이티브 메모리(Native Memory) 영역에 위치하며, 기본적으로 최대 크기가 정해져 있지 않아 시스템 메모리가 허용하는 한 동적으로 크기가 늘어난다 (-XX:MaxMetaspaceSize 옵션으로 최대 크기 지정 가능). GC 대상이 될 수도 있다.
GC의 종류
Minor GC: Young Generation 영역에서 발생하는 GC이다. 새로운 객체가 많이 생성되는 만큼 가장 빈번하게 발생한다. Eden 영역이 가득 차면 Minor GC가 트리거된다.
Major GC: Old Generation 영역에서 발생하는 GC이다. Minor GC보다 훨씬 덜 빈번하지만, 처리할 객체가 많아 일반적으로 Minor GC보다 시간이 더 오래 걸린다.
Full GC: Heap 메모리 전체 (Young Generation, Old Generation, 그리고 PermGen/Metaspace까지)를 대상으로 하는 GC이다. 모든 영역을 대상으로 하므로 가장 많은 시간이 소요되며, 애플리케이션 성능에 가장 큰 영향을 줄 수 있다.