가비지 컬렉션 (Garbage Collection, GC)

가비지 컬렉션(Garbage Collection, GC)은 컴퓨터 프로그래밍에서 더 이상 사용되지 않는 메모리, 즉 ‘가비지(garbage)‘를 자동으로 식별하고 해제하는 메모리 관리 기법이다.
프로그래머가 명시적으로 메모리를 할당하고 해제하는 대신, 런타임 시스템이 메모리 관리를 담당함으로써 개발 생산성을 높이고 메모리 관련 오류를 줄이는 데 기여한다.

가비지 컬렉션은 현대 프로그래밍 언어의 핵심 기능으로, 메모리 관리의 복잡성을 줄이고 개발 생산성을 높이는 데 크게 기여해왔다. 다양한 GC 알고리즘과 최적화 기법이 발전하면서, 초기의 단순한 마크-스윕 접근법에서 오늘날의 정교한 세대별, 동시, 병렬 가비지 컬렉터에 이르기까지 많은 진보가 있었다.

GC는 편의성과 성능 사이의 균형을 추구한다. 개발자는 자신의 애플리케이션 특성과 요구사항에 맞는 GC 알고리즘과 설정을 선택하고, 객체 생성 패턴과 메모리 사용을 최적화하여 GC 오버헤드를 최소화해야 한다.

가비지의 정의

가비지 컬렉션에서 ‘가비지’는 다음과 같이 정의된다:

예를 들어, 다음 Java 코드를 살펴보면:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void createGarbage() {
    Object obj1 = new Object(); // obj1 객체 생성
    Object obj2 = new Object(); // obj2 객체 생성
    
    obj1 = obj2; // obj1이 이제 obj2를 가리킴
    // 최초 obj1이 가리키던 객체는 이제 가비지가 됨
    
    obj1 = null; // obj1 참조 제거
    obj2 = null; // obj2 참조 제거
    // 두 객체 모두 이제 가비지가 됨
}

가비지 컬렉션의 목적

가비지 컬렉션은 다음과 같은 목적을 가지고 있다:

  1. 메모리 누수 방지: 더 이상 사용되지 않는 메모리를 자동으로 회수하여 메모리 누수 방지
  2. 댕글링 포인터(dangling pointer) 방지: 이미 해제된 메모리를 참조하는 위험한 상황 예방
  3. 이중 해제(double free) 방지: 이미 해제된 메모리를 다시 해제하는 오류 예방
  4. 개발자 부담 감소: 메모리 관리의 복잡성을 줄여 개발자가 핵심 로직에 집중할 수 있게 함

가비지 컬렉션의 역사

가비지 컬렉션의 개념은 1959년 John McCarthy가 Lisp 언어를 위해 처음 도입했다. 이후 다양한 프로그래밍 언어와 시스템에서 발전해왔다:

가비지 컬렉션의 기본 원리

가비지 컬렉션의 작동 원리는 크게 두 가지 단계로 나눌 수 있다:

가비지 식별(Garbage Detection)

가비지를 식별하는 대표적인 두 가지 방법이 있다:

참조 카운팅(Reference Counting)

각 객체마다 그것을 참조하는 개수를 추적하는 방식이다.

작동 방식:

  1. 객체가 생성될 때 참조 카운트는 1로 시작
  2. 새로운 참조가 생성될 때마다 카운트 증가
  3. 참조가 제거될 때마다 카운트 감소
  4. 참조 카운트가 0이 되면 객체는 가비지로 간주되어 즉시 수거됨

예시 코드 (의사 코드):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function createObject():
    object = allocate_memory()
    object.reference_count = 1
    return object

function addReference(object):
    object.reference_count += 1

function removeReference(object):
    object.reference_count -= 1
    if object.reference_count == 0:
        collect(object)

장점:

단점:

도달성 분석(Reachability Analysis)

프로그램의 “루트(roots)“에서 시작하여 접근 가능한 모든 객체를 찾는 방식이다.
루트에서 접근할 수 없는 객체는 가비지로 간주된다.

루트의 예:

작동 방식:

  1. 루트에서 시작하여 참조를 따라 모든 도달 가능한 객체를 표시(marking)
  2. 표시되지 않은 객체는 가비지로 간주

장점:

단점:

가비지 수집(Garbage Collection)

식별된 가비지를 실제로 수집하여 메모리를 해제하는 단계이다.
주요 방식으로는:

  1. 마크-스윕(Mark and Sweep)
    1. 마크 단계: 루트에서 도달 가능한 모든 객체에 마크를 함
    2. 스윕 단계: 마크되지 않은 객체를 가비지로 간주하고 해제함
  2. 마크-콤팩트(Mark and Compact)
    1. 마크 단계: 도달 가능한 객체에 마크
    2. 콤팩트 단계: 살아있는 객체들을 메모리의 한쪽 끝으로 모아 메모리 단편화를 줄임
  3. 복사(Copying)
    1. 메모리를 두 영역으로 나눔
    2. 한 영역에서만 객체 할당
    3. GC 시점에 살아있는 객체를 다른 영역으로 복사
    4. 원래 영역은 모두 지움

가비지 컬렉션 알고리즘

다양한 가비지 컬렉션 알고리즘과 그 특성을 살펴보겠습니다.

기본 알고리즘

마크-스윕 알고리즘

앞서 설명한 대로, 도달 가능한 객체를 마크하고 나머지를 스윕(해제)하는 방식.

구체적인 과정:

  1. GC 루트에서 시작하여 모든 참조를 따라가며 도달 가능한 객체에 마크 비트 설정
  2. 힙 전체를 순회하며 마크되지 않은 객체 메모리 해제
  3. 마크 비트 초기화

특징:

마크-콤팩트 알고리즘

마크-스윕에 추가로 살아있는 객체들을 메모리 한쪽으로 모으는 단계가 있다.

구체적인 과정:

  1. 마크 단계: 도달 가능한 객체 표시
  2. 콤팩트 단계: 살아있는 객체들을 메모리 한쪽으로 이동시키고 참조 업데이트

특징:

복사 알고리즘

메모리를 동일한 크기의 두 반으로 나누고(From-space와 To-space), 한 공간에서만 객체 할당이 이루어진다.

구체적인 과정:

  1. From-space에서만 객체 할당
  2. GC 시점에 살아있는 객체를 To-space로 복사
  3. From-space와 To-space 역할 교체

특징:

세대별 가비지 컬렉션(Generational Garbage Collection)

객체의 수명 특성을 활용한 최적화된 GC 알고리즘이다.
“약한 세대 가설(Weak Generational Hypothesis)“에 기반하는데, 이는 대부분의 객체가 짧은 생명주기를 가지며 오래 생존한 객체는 계속 생존할 가능성이 높다는 관찰에 근거한다.

세대 구분:

작동 방식:

  1. 새 객체는 Eden에 할당
  2. Eden이 가득 차면 Minor GC 발생:
    • 살아있는 객체는 비어있는 Survivor 공간으로 이동
    • Eden은 완전히 비워짐
  3. 다음 Minor GC 때는 Eden과 사용 중인 Survivor 공간에서 살아있는 객체가 다른 Survivor 공간으로 이동
  4. 객체가 특정 연령(age) 임계값을 넘기면 Old Generation으로 승격(promotion)
  5. Old Generation이 가득 차면 Major GC(Full GC) 발생

장점:

단점:

병렬, 동시 및 증분 가비지 컬렉션

GC로 인한 애플리케이션 중단 시간을 최소화하기 위한 고급 기법.

병렬 가비지 컬렉션(Parallel GC)

여러 스레드를 사용하여 GC 작업을 병렬로 수행한다.

특징:

동시 가비지 컬렉션(Concurrent GC)

GC 작업의 일부를 애플리케이션 실행과 동시에 수행한다.

특징:

증분 가비지 컬렉션(Incremental GC)

GC 작업을 작은 단위로 나누어 점진적으로 수행한다.

특징:

다양한 프로그래밍 언어의 가비지 컬렉션

Java의 가비지 컬렉션

Java는 가장 발전된 GC 시스템 중 하나를 가지고 있으며, JVM은 다양한 GC 알고리즘을 제공한다.

HotSpot JVM의 주요 GC 알고리즘
  1. Serial GC: 단일 스레드 GC로 작은 애플리케이션에 적합

    1
    2
    
    // 활성화 방법
    java -XX:+UseSerialGC MyApp
    
  2. Parallel GC: 다중 스레드로 GC 수행, 처리량 중심

    1
    2
    
    // 활성화 방법
    java -XX:+UseParallelGC MyApp
    
  3. Concurrent Mark Sweep (CMS): 애플리케이션 중단을 최소화하는 동시 수집기

    1
    2
    
    // 활성화 방법
    java -XX:+UseConcMarkSweepGC MyApp
    
  4. Garbage First (G1) GC: 큰 힙을 위한 낮은 지연 시간 수집기

    1
    2
    
    // 활성화 방법
    java -XX:+UseG1GC MyApp
    
  5. Z Garbage Collector (ZGC): 매우 낮은 지연 시간 보장(10ms 미만)

    1
    2
    
    // 활성화 방법 (Java 11+)
    java -XX:+UseZGC MyApp
    
Java GC 모니터링 및 튜닝

Java는 GC 활동을 모니터링하고 튜닝하기 위한 풍부한 도구를 제공한다:

1
2
3
4
5
6
7
8
// GC 로깅 활성화
java -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps MyApp

// 힙 크기 조정
java -Xms1g -Xmx4g MyApp  // 초기 힙 1GB, 최대 힙 4GB

// 세대 크기 조정 (G1 GC)
java -XX:NewRatio=2 -XX:SurvivorRatio=8 MyApp

Python의 가비지 컬렉션

Python은 참조 카운팅을 기본 메커니즘으로 사용하며, 순환 참조 처리를 위한 추가 GC를 제공한다.

참조 카운팅

Python의 모든 객체는 참조 카운트를 유지하며, 카운트가 0이 되면 즉시 해제된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import sys

# 객체의 현재 참조 카운트 확인
obj = {}
print(sys.getrefcount(obj) - 1)  # 1 출력 (getrefcount 호출 자체가 임시 참조 추가)

# 참조 추가
ref = obj
print(sys.getrefcount(obj) - 1)  # 2 출력

# 참조 제거
ref = None
print(sys.getrefcount(obj) - 1)  # 1 출력
순환 참조 컬렉터

순환 참조를 처리하기 위해 Python은 세대별 가비지 컬렉터를 제공한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import gc

# 현재 GC 임계값 확인 (각 세대별 임계값)
print(gc.get_threshold())  # 기본값: (700, 10, 10)

# GC 수동 실행
gc.collect()

# GC 비활성화/활성화
gc.disable()
gc.enable()

# 가비지 컬렉터 통계
print(gc.get_stats())

JavaScript의 가비지 컬렉션

JavaScript 엔진은 대부분 도달성 분석 기반의 GC를 사용한다.
V8(Chrome, Node.js)을 예로 들면:

V8 엔진의 GC

JavaScript에서는 직접적인 GC 제어가 제한적이지만, 메모리 사용 패턴을 최적화할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 참조 제거로 객체 회수 가능하게 만들기
let largeObject = { data: new Array(10000000) };
// 작업 완료 후
largeObject = null;  // 참조 제거로 GC 대상이 됨

// WeakMap/WeakSet 사용으로 약한 참조 생성
const weakMap = new WeakMap();
const key = {};
weakMap.set(key, "value");
// key에 대한 다른 참조가 없어지면 WeakMap의 항목도 GC 대상이 됨

Go의 가비지 컬렉션

Go는 동시 마크-스윕 알고리즘을 사용하며 간결한 설계가 특징이다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Go에서는 GC 제어가 최소화되어 있음
import "runtime"

// 수동 GC 실행 (테스트/디버깅 목적)
runtime.GC()

// GC 통계
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
println("Next GC:", stats.NextGC)
println("GC Cycles:", stats.NumGC)

Go의 GC는 짧은 중단 시간(1ms 미만)과 낮은 오버헤드를 목표로 설계되어 있다.

가비지 컬렉션 최적화 전략

애플리케이션 성능을 최적화하기 위한 GC 관련 전략.

객체 생명주기 관리

  1. 단기 객체 최소화
    대부분의 GC 오버헤드는 짧은 수명의 객체에서 발생한다.
    이를 줄이는 전략:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    // 최적화 전 (많은 임시 객체 생성)
    String result = "";
    for (int i = 0; i < 1000; i++) {
        result += "Value " + i;  // 매 반복마다 새 String 객체 생성
    }
    
    // 최적화 후 (한 개의 가변 객체 재사용)
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 1000; i++) {
        sb.append("Value ").append(i);  // 기존 객체 재사용
    }
    String result = sb.toString();  // 최종 결과만 String으로 변환
    
  2. 객체 풀링 활용
    자주 생성되고 해제되는 객체는 풀링을 통해 재사용할 수 있다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    // Apache Commons Pool 라이브러리 사용 예시
    GenericObjectPool<MyExpensiveObject> pool = 
        new GenericObjectPool<>(new MyObjectFactory());
    pool.setMaxIdle(10);
    pool.setMaxTotal(20);
    
    MyExpensiveObject obj = null;
    try {
        obj = pool.borrowObject();
        // 객체 사용
    } finally {
        if (obj != null) {
            pool.returnObject(obj);  // 풀로 반환
        }
    }
    

메모리 사용 패턴 최적화

  1. 불필요한 참조 제거
    불필요한 참조를 유지하면 메모리 누수가 발생할 수 있다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    public class Cache {
        private Map<String, Object> cache = new HashMap<>();
    
        // 문제: 캐시에서 항목이 제거되지 않음
        public void add(String key, Object value) {
            cache.put(key, value);
        }
    
        // 해결: 약한 참조 사용
        private Map<String, WeakReference<Object>> betterCache = 
            new WeakHashMap<>();
    
        public void addBetter(String key, Object value) {
            betterCache.put(key, new WeakReference<>(value));
        }
    }
    
  2. 큰 객체 관리
    큰 객체는 특별한 관리가 필요하다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    // 대규모 데이터 처리를 위한 청크 단위 처리
    public void processLargeFile(File file) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            String line;
            List<String> batch = new ArrayList<>(1000);
    
            while ((line = reader.readLine()) != null) {
                batch.add(line);
    
                if (batch.size() >= 1000) {
                    processBatch(batch);
                    batch.clear();  // 동일한 List 재사용
                }
            }
    
            if (!batch.isEmpty()) {
                processBatch(batch);
            }
        }
    }
    

언어/플랫폼별 GC 튜닝

각 언어와 플랫폼은 고유한 GC 튜닝 옵션을 제공한다.

GC 모니터링과 분석

효과적인 GC 튜닝을 위해서는 모니터링과 분석이 필수적이다.

가비지 컬렉션의 현대적 접근법

현대적인 GC 알고리즘과 기법은 성능과 응답성을 더욱 개선하고 있습니다.

가비지 컬렉션의 문제점과 해결 방안

가비지 컬렉션은 많은 이점을 제공하지만, 몇 가지 도전 과제와 문제점이 있다.

  1. GC 일시 중지(Stop-the-World) 문제
    대부분의 GC 알고리즘은 일시적으로 애플리케이션 실행을 중단시켜야 한다.
    문제:

    • 긴 GC 일시 중지는 응답성에 영향을 미침
    • 실시간 애플리케이션에서 문제가 될 수 있음

    해결 방안:

    1. 동시 GC 사용: CMS, G1, ZGC와 같은 동시 수집기 활용
    2. 증분 GC 사용: 작은 단위로 GC 작업 분할
    3. 힙 크기 최적화: 너무 크거나 작은 힙은 GC 성능에 부정적 영향
    1
    2
    
    // Java에서 짧은 GC 중단을 위한 ZGC 설정 예시
    java -XX:+UseZGC -Xms4G -Xmx4G -XX:+UnlockExperimentalVMOptions MyApp
    
  2. 메모리 단편화(Fragmentation)
    해결 방안:

    1. 압축(Compaction) 알고리즘 사용: 살아있는 객체들을 메모리의 한쪽으로 모음
    2. 객체 할당 전략 최적화: 비슷한 수명을 가진 객체들을 함께 할당
    3. 메모리 풀링(Memory Pooling): 특정 크기의 객체를 위한 메모리 풀을 미리 할당

    압축 알고리즘의 작동 예시:

    1
    2
    3
    
    [객체1][빈공간][객체2][빈공간][객체3]  // 단편화된 상태
                    ↓ (압축 후)
    [객체1][객체2][객체3][빈공간          ]  // 압축된 상태
    
  3. 리소스 소비
    문제:

    • GC는 CPU 시간을 소비함
    • 메모리 오버헤드 발생 (GC 메타데이터, 객체 헤더 등)

    해결 방안:

    1. 객체 생성 최소화: 불필요한 객체 생성 제한
    2. 객체 풀링: 자주 사용되는 객체를 재사용
    3. 적절한 초기 힙 크기 설정: 빈번한 힙 크기 조정 방지
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    // Java에서 객체 풀 사용 예시
    public class StringBuilderPool {
        private static final ThreadLocal<StringBuilder> pool = 
            ThreadLocal.withInitial(() -> new StringBuilder(1024));
    
        public static StringBuilder acquire() {
            StringBuilder sb = pool.get();
            sb.setLength(0); // 재사용 전 초기화
            return sb;
        }
    
        public static String buildAndRelease(StringBuilder sb) {
            String result = sb.toString();
            // 자동으로 ThreadLocal에 유지됨
            return result;
        }
    }
    
  4. 예측 불가능성
    문제:

    • GC 타이밍과 지속 시간 예측이 어려움
    • 일부 실시간 시스템에서는 허용할 수 없는 상황

    해결 방안:

    1. 실시간 GC 알고리즘: 최대 중단 시간을 보장하는 GC 사용
    2. GC 빈도 튜닝: 적절한 힙 크기와 GC 트리거 조정
    3. 수동 메모리 관리 영역 도입: 극도로 중요한 경로에서는 GC를 회피하는 설계
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // Java에서 직접 메모리 사용 예시 (GC 없음)
    import java.nio.ByteBuffer;
    
    ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024 * 1024);
    // DirectByteBuffer는 힙 외부에 할당되어 GC 영향을 덜 받음
    
    // 사용 후 명시적 해제
    directBuffer = null;
    System.gc(); // Native 메모리 해제 힌트
    

가비지 컬렉션의 미래 동향

GC 기술은 계속 발전하고 있으며, 몇 가지 흥미로운 미래 동향이 있다.

  1. 하드웨어 지원 GC
    하드웨어 수준에서 GC를 지원하는 접근법:
    • GC 가속 하드웨어: 참조 추적과 마킹을 가속화하는 특수 하드웨어
    • 메모리 태깅: 참조와 데이터를 하드웨어 수준에서 구분하는 기술
    • 병렬 마킹 유닛: 객체 그래프 탐색을 병렬화하는 특수 프로세서
  2. 기계 학습 기반 GC
    AI와 머신러닝을 활용하여 GC 결정을 최적화:
    • 예측적 GC: 메모리 사용 패턴을 학습하여 선제적으로 GC 수행
    • 적응형 정책: 애플리케이션 행동에 따라 GC 정책 자동 조정
    • GC 타이밍 최적화: 최소 영향을 주는 시점에 GC 수행
  3. 특화된 언어 설계
    GC 효율성을 고려한 프로그래밍 언어 설계:
    • 영역 기반 메모리 관리: 메모리를 영역별로 관리하여 전체 GC 필요성 감소
    • 선형 타입 시스템: 컴파일 시간에 메모리 안전성을 보장하는 타입 시스템
    • 하이브리드 메모리 모델: 안전한 수동 관리와 자동 GC의 결합

가비지 컬렉션과 수동 메모리 관리 비교

항목가비지 컬렉션 (GC)수동 메모리 관리
메모리 해제 방식자동으로, 비결정적으로 이루어짐프로그래머가 명시적으로 free()/delete 등을 호출하여 해제
개발 편의성메모리 관리 코드를 작성할 필요가 없으므로 코드가 간결하며 오류가 줄어듦직접 관리해야 하므로 코드 복잡도가 증가하고, 메모리 누수 등의 위험 존재
메모리 누수 위험자동 회수로 누수 위험이 낮으나, 잘못된 참조 유지 시 누수 가능프로그래머의 실수로 인해 메모리 누수가 발생할 가능성이 높음
성능 오버헤드GC 수행 시 일시적인 정지와 추가 CPU/메모리 사용 등의 오버헤드 발생필요하지 않은 시점에 메모리 해제를 실행할 수 있어 오버헤드 제어 용이
실시간성비결정적 해제로 인해 실시간 시스템에서는 부적합할 수 있음해제 시점을 정확히 제어할 수 있어 실시간 요구 조건에 적합
오류 예방중복 해제, 댕글링 포인터 등 일반적인 메모리 오류를 예방프로그래머가 직접 관리하므로 실수 시 치명적인 오류가 발생할 수 있음

용어 정리

용어설명

참고 및 출처