본문 바로가기

C#

[C#] Garbage Collector

✅ GC의 목적

  • 힙(Heap)에 쌓인 더 이상 참조되지 않는 객체를 자동으로 찾아 제거하여 메모리 누수를 방지
  • 프로그래머가 수동으로 delete 같은 메모리 해제를 하지 않아도 되게 함 (C++과 비교되는 장점)

🔍 GC의 동작 흐름 요약

1. 객체가 생성되면 힙(Heap)에 저장됨

var player = new Player(); // new → 힙 메모리 할당
  • 이 객체는 스택 변수 player가 참조하고 있음
  • 참조가 사라지지 않는 한, GC는 이 객체를 제거하지 않음

2. GC는 "참조 그래프"를 분석해서 도달 불가능한 객체를 수거

  • 루트(스택 변수, static, CPU 레지스터 등)에서 시작해,
  • 따라갈 수 없는 객체는 "고아 객체(unreachable)" → 수거 대상

3. 수거 시점: GC는 항상 즉시 작동하지 않음

  • 다음 조건 중 일부일 때 작동:
    • 힙 메모리가 부족해졌을 때
    • 명시적으로 GC.Collect() 호출했을 때
    • 백그라운드 GC가 자동으로 주기적으로 실행

➡ 그래서 GC는 "비결정적(언제 실행될지 보장되지 않음)"

 

✅ 세대(Generation) 기반 GC

GC는 효율을 위해 객체를 '세대(Generation)'별로 분류합니다:

세대의미특징
Gen 0 가장 최근에 생성된 객체 대부분 한두 프레임만 존재하는 임시 객체
Gen 1 Gen 0에서 살아남은 객체 아직도 참조됨
Gen 2 오래 살아남은 객체 게임 매니저, 싱글톤 등
 

GC는 먼저 Gen 0부터 수거하고, 살아남은 객체는 다음 세대로 승급됩니다.

➡ 오래된 객체는 GC 비용이 높아서 자주 검사하지 않음
➡ 게임 루프 안에서 Gen 0 대상의 임시 객체를 자주 만들면 GC 부담 커짐!

 

 

✅ 참조 그래프란?

프로그램 실행 중 루트(Root)부터 시작해서

"누가 누구를 참조하고 있는가"를 따라가 만든 연결 구조

🔹 루트(Root)에는 이런 것들이 포함됩니다:

스택 변수 메서드 내부의 지역 변수
static 변수 GameManager.instance, 전역 설정 등
레지스터에 올라간 변수 현재 실행 중인 코드의 변수
GCHandle로 고정한 객체 GCHandle.Alloc(obj) 등

 

🔍 GC의 참조 그래프 분석 과정

  1. GC가 시작되면, 우선 루트 객체들을 스캔합니다
    (예: player, GameManager, SceneManager 등)
  2. 루트에서 참조하고 있는 객체들을 따라갑니다
  3. 그 객체들이 다시 참조하는 객체들도 따라갑니다 → 트리 구조 생성
  4. 이 참조 경로에서 단 한 번도 도달하지 못한 객체
    “더 이상 필요 없는 객체” → 수거 대상(Garbage)

✅ 순환 참조도 상관없다!

class Node
{
    public Node next;
}

Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a; // 순환참조!

 

  • GC는 순환 참조 자체를 문제로 보지 않음
  • 중요한 건 "루트에서 접근 가능한가"
  • 루트에서 b에 접근 가능하면 → b는 물론 b가 참조하는 a도 함께 살아남음
  • 루트에서 둘 다 접근 불가능하면 → 순환 참조라도 둘 다 수거됨

 

 

✅ IDisposable과 using 문의 역할

이제 IDisposable과 using은 GC의 수거와는 별도지만, 매우 중요한 역할을 해요.

🔹 어떤 객체는 GC가 수거해도 리소스를 자동으로 해제하지 않음

예:

  • 파일 핸들 (FileStream)
  • DB 연결 (SqlConnection)
  • Unity의 Texture2D, AudioClip (비관리 리소스)
  • 네트워크 소켓 등

➡ 이런 객체는 명시적으로 Dispose()를 호출해서 정리해야 함

 

🔧 방법 1: using 문 사용

using (var stream = new FileStream("path.txt", FileMode.Open))
{
    // 파일 사용
}
// 여기서 Dispose() 자동 호출됨
  • using은 블록이 끝나면 Dispose()를 자동 호출
  • 예외가 발생해도 안전하게 정리됨 (try-finally 자동 포함됨)

🔧 방법 2: IDisposable 직접 구현

public class MyClass : IDisposable
{
    public void Dispose()
    {
        // 리소스 해제 로직
    }
}

→ using 구문에서 사용 가능해짐

 

🎯 GC는 "메모리"는 수거해도, "자원(리소스)"은 수거하지 않아요.
그래서 참조를 끊어야 GC가 작동하고,
자원은 Dispose()로 직접 정리해야 하는 것입니다.

'C#' 카테고리의 다른 글

[C#] 구조체를 쓰는 이유  (1) 2025.08.10
[C#] Boxing과 UnBoxing  (3) 2025.07.25
[C#] LINQ와 Lazy Evaluation(지연 평가)  (2) 2025.07.25
[C#] IEnumerable을 쓰는 이유  (2) 2025.07.24
[C#] delegate vs Action  (1) 2025.06.16