C# 메모리 관리에 대하여 Garbage Collector
안녕하십니까. 이번 포스팅에서는 제가 업무적으로 주로 사용하는 C#의 메모리 관리에 대하여 알아보도록 하겠습니다.
C#, CLR, .net Garbage Collection
공식 문서 : https://docs.microsoft.com/ko-kr/dotnet/standard/garbage-collection/fundamentals
C# 자체는 언어일 뿐이고 자바의 JVM와 같이 C#은 Common Language Runtime이라는 환경에서 관리를 합니다.
C# 자체는 언어일 뿐이고 자바의 JVM와 같이 C#은 Common Language Runtime이라는 환경에서 관리를 합니다.
공식 문서에서는 "관리 코드를 사용하여 작업하는 개발자는 메모리 관리 작업을 수행하기 위해 코드를 작성할 필요가 없습니다." 라고 합니다.
자바 언어를 경험하였을때 JVM 메모리를 관리하는 명령어가 있었지만, C#을 사용하고 나서는 딱히 그런 관리를 한 경험은 없습니다. 이는 장점이 될수 있고 단점이 될수 있겠지만, 일반적인 개발자들에게는 장점이지 않을까 생각 합니다.
단순하게 CLR이 관리한다고 알기 보다는 어떻게 관리하는지를 아는것도 중요하다고 생각 하여서 알아보도록 하겠습니다.
메모리 영역
메모리 관리에 대하여 알아보기 전에 어떤 메모리 구조가 있는지 간략하게 소개하도록 하겠습니다.
메모리 구조로는 다음과 같이 3 영역이 있습니다.
- static
- stack
- heap
static 영역
C#에서는 static이라는 키워드를 통하여 변수를 전역변수로 선언 할 수 있습니다. 이렇게 선언시 어떠한 영역에서도 접근이 가능하게 됩니다. 전역 변수는 프로그램이 시작하거나 종료하면 생성, 삭제되기 때문에 메모리 관리에 큰 영향을 주지는 않습니다.
stack 영역
stack 영역은 함수, if문과 같은 block으로 감싼 코드들에 할당되는 지역 변수들을 저장 합니다. 해당 영역은 코드 문이 실행될때 생성되고, 끝날때 삭제가 됩니다. 일반적인 메모리 관리에 영향을 주지는 않지만, 많은 함수가 실행이 되고 끝나지 않으면 stack 영역이 꽉 차는 stackoverflow가 발생 합니다.
heap 영역
heap 영역은 C#에서는 new라는 키워드를 통해서 객체를 생성되면 heap영역에 할당이 됩니다. 해당 영역은 모든 쓰레드끼리 공유합니다. 객체가 생성되는 시기는 사용자 코드에 의해 new가 접근될때 이며, 해제되는 시점은 Garbage Collector에 의해서 해제 됩니다.
메모리 관리 기법
기본적인 메모리 관리 기법
현재의 메모리 관리 기법이 있기 전에 여러 발전이 있었습니다.
가장 쉬운 방법은 각 객체마다 참조 카운트를 지정하고, 참조될때마다 카운트를 늘리고, 참조가 제거되면 카운트를 제거하는 방식이 있습니다. 이를 Reference Counting 이라고 합니다. 이러한 방식은 간단하지만 매번 객체가 참조되고 제거될때마다 연산이 필요하기에 객체가 많아지면 부하가 커집니다. 또한 참조 순환이 됨에 따라 고아 참조가 발생하여 로직상으로는 제거가 어려운 객체가 생성되게 됩니다.
그 다음으로는 Mark Sweep 방식이 있습니다. 이 방식은 사용중인 객체들을 표기하는 Root Set을 생성 하고, 여기서는 사용중인 객체를 저장 합니다. 이는 Mark Phase 입니다. 객체가 참조되지 않아서 사용중이 해제된 객체들은 Sweep Phase에서 제거 합니다. 여기서 더 나아가 제거할때 살아남은 객체들의 공간을 가능하면 옮겨서 빈공간을 줄이는 Mark Sweep Compaction 방식이 있습니다.
Generational Collector
공식 문서 : https://docs.microsoft.com/ko-kr/dotnet/standard/garbage-collection/fundamentals#generations
C#에서는 Generational Collector라는 방식을 사용합니다. 이는 기본적으로 위의 Mark Sweep 방식을 사용 합니다. 여기서 더 나아가 공간을 3개로 분할 하여 0세대, 1세대, 2세대 메모리 영역이 존재 합니다.
여기서 세대를 분할하는 이유는 다음과 같은 사상이 담겨있습니다.
- 대부분의 오브젝트는 오래 살아남지 못한다.
- 대부분의 오브젝트는 사라질때 같이 사라진다.
그렇기 때문에 객체 생성, 삭제 주기에 따라 공간을 분리 하였습니다.
대략적으로 표현하면 각 세대는 위와 같으며 GEN 0은 처음 생성된 객체가 있는곳이며, Garbage Collecte가 될때마다 살아남으면 점차적으로 1,2로 옮겨가게 됩니다.
위의 GEN 영역 이외에 85K 이상의 대형 오브젝트를 관리하기 위한 메모리도 있습니다. 여기는 오브젝트가 커서 복사시 성능이 안좋기 때문에 따로 두는 것이고, 단순하게 Mark Sweep 방식을 사용 합니다.
각 메모리의 크기 등은 개발자가 조정하는 옵션은 딱히 없고 CLR에서 알아서 관리 합니다.
주의해야할것
Destructor - IDisposable
Garbage Collection은 매번 객체가 사라질때 시작되는게 아니라 메모리가 꽉차는등의 여러 요소로 인하여 주기적으로 발생 합니다. 따라서 객체가 삭제될때 실행되는 종료자에 특정 로직을 넣을 경우 실행이 언제될지 모르고 실행이 되는지 조차 확실하지 않습니다. 이러한 로직은 보통 커넥션을 맺을때 close()와 같은 함수에 구현하는등 방식을 사용 합니다.
다른 방식으로는 IDIsposable을 상속 받아 using block을 사용하는 것 입니다. close와 같이 명백히 사용 종료후 실행되야하는 코드를 dispose()로 구현을 한 다음에 해당 함수를 직접 실행 하거나 아래와 같이 using block으로 감싸면 block이 끝날시에 해당 함수를 실행하게 됩니다.
using (StreamReader sr = new StreamReader(filename)) {
txt = sr.ReadToEnd();
}
댓글
댓글 쓰기