C, C++ 같은 언어에서는 메모리를 직접 관리해야 한다.
void* p = malloc(100);
// ...
free(p);
free를 깜빡하면 → 메모리 누수C#/.NET은 이런 귀찮고 위험한 일을 줄이기 위해 가비지 컬렉터(GC)를 도입했다.
GC = 더 이상 사용되지 않는 객체를 자동으로 찾아서 메모리를 회수해주는 시스템
개발자는 보통 new로 객체만 만들고,
“언제 free 할지”는 신경 안 써도 되게 만든 것.
C# 메모리 구조를 아주 단순하게 나누면 이렇게 생각할 수 있다.
void Foo()
{
int x = 10; // x는 스택에 위치
} // Foo가 끝나면 x도 자동으로 사라짐
new로 생성되는 객체들이 저장되는 곳string, List<T>, 사용자 정의 클래스 인스턴스 등void Foo()
{
var list = new List<int>(); // List 객체는 힙에 생성
} // Foo가 끝나도 힙의 List는 자동으로 사라지지 않는다
정리하면:
GC가 객체를 지우려면 기준이 있어야 한다. 핵심은 딱 하나다:
“어디에서도 더 이상 참조하지 않는 객체” → 쓰레기
void Foo()
{
var list = new List<int>();
list.Add(1);
list.Add(2);
} // 여기서 Foo 종료
list는 스택에 있는 지역 변수list도 함께 사라진다new List<int>()를 가리키는 참조가 아무 것도 없다GC 입장에서는 이 객체는 이제 “어디에서도 도달 불가” → 가비지가 된다.
List<int> _cache;
void Make()
{
_cache = new List<int>();
}
void Use()
{
_cache.Add(1);
}
_cache는 필드(멤버 변수) → 프로그램이 살아 있는 동안 계속 유지될 수 있다_cache가 가리키는 List는 GC 대상이 아니다 (아직 “살아 있는 객체”)정리하면:
GC는 “참조가 끊어진 객체”만 지운다.
참조가 남아 있으면, 아무리 우리가 “더 이상 안 쓰는데…”라고 생각해도 GC는 지우지 않는다.
내부 구현은 엄청 복잡하지만, 개념적으로는 대략 이런 일을 한다고 보면 된다.
이 과정에서 잠깐 GC가 도는 동안 해당 스레드가 멈추는 구간(stop-the-world)이 생길 수 있다.
성능 튜닝할 때는 이 GC pause도 고려해야 한다.
.NET GC는 성능을 높이기 위해 힙을 세대(Generation)로 나누어 관리한다.
경험적으로 이런 특징이 있다.
“대부분의 객체는 금방 죽고, 생각보다 오래 사는 객체는 적다.”
따라서 GC는:
지금 단계에서는:
“객체는 처음에 Gen 0에서 시작하고, GC를 통과해 살아남으면 점점 위 세대로 승격된다” 이 정도만 알고 있어도 충분하다.
우리가 new를 계속 호출해서 힙이 어느 정도 차면,
런타임이 “이제 한 번 청소할 때가 된 것 같은데?” 하고 GC를 돌린다.
또한:
등 여러 요인을 고려해서 자동으로 실행된다.
우리가 직접 호출할 수도 있다:
GC.Collect();
하지만 실무에서는 왠만하면 사용을 권장하지 않는다.
런타임이 보통 우리보다 더 좋은 타이밍에 돌릴 수 있고,
우리가 억지로 자주 호출하면 오히려 성능이 떨어질 수 있기 때문이다.
IDisposable, using과의 관계
GC는 어디까지나 “관리 힙 메모리”만 치운다.
그런데 객체 안에는 메모리 말고도 이런 것들이 들어있을 수 있다.
이런 것들은 OS 수준 자원이기 때문에, GC가 직접 정리해 줄 수 없다.
그래서 이런 타입들은 보통 IDisposable을 구현하고,
우리가 Dispose()를 호출해서 정리해야 한다.
using (var fs = new FileStream("test.txt", FileMode.Open))
{
// 파일 사용
} // 여기서 자동으로 fs.Dispose() 호출 → 파일 핸들 반납
정리하면:
IDisposable / using : 파일 핸들, DB 연결 같은 비관리 자원 정리 담당GC가 있는데도 왜 “메모리 누수” 얘기가 나올까? 이유는 아주 간단하다.
GC는 “참조가 있는 객체”는 절대로 지우지 않는다.
예를 들어:
static List<byte[]> _cache = new List<byte[]>();
void Leak()
{
var big = new byte[10 * 1024 * 1024]; // 10MB
_cache.Add(big); // 전역 리스트에 계속 추가
}
_cache는 static 필드라 프로그램이 끝날 때까지 참조를 유지할 수 있다.즉, C#에서도:
우리가 참조를 계속 잡아두면, GC가 치울 수 없다.
그래서 “참조를 언제 끊어줄지”도 중요한 설계 포인트다.
GC는 “메모리 free를 자동으로 해주는 친구”지만, 우리가 참조를 어떻게 관리하느냐에 따라 효율과 안정성이 달라진다.
다음 단계로는:
IDisposable 차이같은 내용을 공부하면, C#/.NET 메모리 관리에 대한 이해가 한층 더 깊어진다.