23. 가비지 컬렉션

김민영·2023년 3월 2일
0

C# 기초 프로그래밍

목록 보기
17/18

🌟 메모리와 가비지 컬렉션

1. 리소스란?

1) 리소스
: 운영체제로부터 빌려와 사용한 후 반환해야하는 것

2) 메모리도 리소스
→ 동적 할당된 메모리를 사용한 후에는 시스템에 돌려줘야 함

2. 메모리 반환 관련 문제

1) 메모리 누수 (Memory Leak)
: 메모리 사용이 끝났음에도 불구하고 해제하지 않은 것

2) 이중 해제 (Double Free)
: 이미 해제된 메모리임에도 불구하고 다시 해제하는 것

+) 댕글린 포인터/레퍼런스 (Dangling Pointer/Reference)
: 이미 해제된 메모리를 가리키는 포인터/레퍼런스

3) 섣부른 해제 (Premature Free)
: 아직 사용이 끝나지 않았음에도 불구하고 해제하는 것

→ 자동 메모리 관리(automatic memory management)
: 위와 같은 불편함을 해결하기 위한 기술로, 가비지 컬렉션이 이에 해당함


🌟 가비지 컬렉션 (Garbage Collection)

1. 가비지 컬렉션이란?

1) 가비지 컬렉터(Garbage Colletor)가 더이상 사용하지 않는 메모리를 재사용하는 것

2) 종류

  • 보수적 가비지 컬렉션 (Conservative Garbage Collection)
  • 복제 가비지 컬렉션 (Copying Garbage Collection)
  • 분산 가비지 컬렉션 (Distributed Garbage Collection)
  • 증분 가비지 컬렉션 (Incremental Garbage Collection)
  • 세대별 가비지 컬렉션 (Generational Garbage Collection)

2. 객체의 사용 유무(Liveness) 판단 방법

▶ 추적 가비지 컬렉션 (Tracing Garbage Collection)

1) 도달 가능성(Reachability)으로 생존을 가정
→ 루트(Root)에서 출발해 해당 메모리까지 도달할 수 없는 경우 가비지로 가정

2) 루트의 종류
: 스택 루트, CPU 레지스터, 정적 필드 (static) 등

▶ 참조 카운팅 (Reference Counting)

1) 해당 메모리에 참조하는 것이 없을 때 가비지로 가정

2) 서로 다른 두 메모리가 서로를 참조하는 순환 참조를 주의해야 함
→ 실제로 살아있지 않지만, 살아있다고 판단할 수 있음
→ 참조 횟수에 영향을 주지 않는 약한 참조를 사용할 수 있음

3. .NET에서의 가비지 컬렉션

1) 매니지드 언어 (Managed Language)
: 가비지 컬렉션을 지원하는 언어 (C# 포함)
→ 언매니지드 언어(Unmanaged Language)는 매니지드 언어의 반대 개념으로, 네이티브 언어(Native Language)라고도 함

2) C#의 가비지 컬렉션

  • 세대별 가비지 컬렉션 사용
  • CLR이 지원

▶ 세대 (Generation)

1) 매니지드 힙 (Managed Heap)
: 가비지 컬렉터가 관리하는 메모리

2) 매니지드 힙은 0세대, 1세대, 2세대 총 3세대로 나누어 관리됨

  • 각 세대의 시작을 가리키는 포인터가 존재

3) 세대를 나눈 이유는 메모리를 재사용하기 용이하기 때문

  • 가비지 컬렉션이 일어날 때 메모리 단편화를 방지하기 위해 메모리를 압축하는데, 힙 전체를 대상으로 하기보다 앞부분에서만 수행하는 것이 빠름
  • 최근에 만들어진 객체일수록 수명이 짧고, 오래 사용된 객체일수록 수명이 길어 재사용할 메모리를 빠르게 분류할 수 있음
  • 메모리 할당은 0세대에서만 일어나는데, 최근에 만들어진 객체끼리 서로 연관되는 경향이 있어 캐싱 측면에서 유리함

▶ 메모리 할당

1) C#의 모든 참조 타입 객체는 매니지드 힙의 0세대에 할당, 연속적 배치

2) 매니지드 힙은 메모리를 미리 시스템으로부터 할당 받아 놓기 때문에, 스택에서 메모리를 할당하는 속도만큼 빠르게 할당 및 접근할 수 있음

3) 85KB 이상의 큰 객체는 LOH (Large Object Heap)라는 2세대 메모리에 할당

▶ 메모리 해제

1) 추적(Tracing) 방식 사용

2) 가비지 컬렉터가 가장 적합한 때에 자동으로 메모리를 해제

3) 실제로 시스템에 메모리를 돌려주는 것은 아니며, 다른 메모리에 의해 덮여쓰여짐
→ LOH에서는 덮어쓰는 연산이 비싸기 때문에 덮어쓰기를 생략
→ 따라서 단편화 발생할 수 있음

4) 참조 변수의 주소값을 모두 수정하며, 각 세대의 시작을 가리키는 포인터 또한 수정
→ 덮어쓰는 과정에서 뒤쪽에 할당된 데이터가 앞으로 당겨지는데, 이 과정을 통해 메모리 단편화가 발생한 부분을 없앰
→ 데이터가 앞으로 당겨지면 메모리 주소(포인터)도 변경

5) 가비지 컬렉션 발생 순서

  • 0세대에서 가장 먼저 발생
  • 0세대에서 가비지 컬렉션 발생 후 가비지가 아니라고 판단된 메모리는 윗 세대로 승격 (Promotion)
  • 0세대에서 가비지 컬렉션 발생 이후에도 새 객체 생성을 위한 공간이 충분하지 않은 경우 1세대, 2세대 순으로 가비지 컬렉션 수행
  • 0세대 > 1세대 > 2세대 순으로 진행한 후에도 공간이 부족하다면 다시 2세대 > 1세대 > 0세대 순으로 가비지 컬렉션을 수행
  • 이 상황에서도 0세대, 1세대에서 세대 승격 발생

▶ 주의 사항

가비지 컬렉션은 자원을 적게 소모하는 연산이 아니며, 멀티 스레드 환경에서는 가비지 컬렉션이 수행되는 동안 다른 스레드가 중단(Suspended)되기 때문에 가비지 컬렉션과 성능의 관계를 이해하고 있어야 함

1) 참조 카운팅 방식으로 가비지 수집이 일어나지 않음

  • 세대별 가비지 컬렉션은 추적 방식

2) 필요하다면 약한 참조를 사용할 수 있음

  • 어떤 객체에 도달 가능할 때 이를 객체에 대한 강한 참조(Strong Reference)를 갖는다고 표현
  • 객체의 도달 가능성에 영향을 주지 않으면서 해당 객체를 참조하고 싶은 경우 약한 참조(Weak Reference)를 사용할 수 있음

3) 빈번한 할당을 조심해야 함

  • 가비지 컬렉션이 일어나는 조건 중 하나는 객체를 할당할 공간이 충분하지 않을 때 (즉, 0세대가 가득 찼을 때)
  • 객체를 빈번하게 생성하는 경우 0세대에 여유 공간이 부족하여 가비지 컬렉션이 일어날 수 있음

4) 너무 큰 객체 할당을 피하자

  • 85KB 이상의 객체는 LOH에 할당
  • LOH에서는 메모리 할당이 일어나지 않아 내부 단편화가 발생할 수 있음

5) 복잡한 참조 관계를 피하자

  • 가비지 컬렉션 후 메모리 주소 관리가 어려워지기 때문
  • 쓰기 장벽(Write Barrier): 오래된 세대에서 새로운 세대에 대한 메모리를 참조하게될 때 수집을 방지하기위해 만드는 것

6) 관리되지 않는 리소스도 있음

  • 메모리 외의 파일 핸들, 윈도우 핸들, 네트워크 연결 등의 운영체제 리소스를 사용하는 경우 제대로 정리가 되지 않을 수 있음
  • IDisposable와 using문을 이용해 관리할 수 있음
    → using문 사용 예시: StreamReader, StreamWriter과 같이 명시적으로 close해줘야하는 리소스들을 사용 후 반환하도록 함
  • C#에서 관리할 수 있는 것은 메모리 뿐

0개의 댓글