C#에서는 데이터를 저장할 때 스택(Stack)과 힙(Heap)이라는 두 가지 메모리 영역을 사용한다.
| 구분 | 스택(Stack) | 힙(Heap) |
|---|---|---|
| 저장 위치 | 메모리의 스택 영역 | 메모리의 힙 영역 |
| 저장 방식 | LIFO (Last In, First Out)방식으로 관리 | 동적 할당 방식 |
| 속도 | 빠름 (메모리 할당/해제 속도가 빠름) | 느림 (GC가 개입하여 메모리 정리) |
| 저장된는 데이터 | 값 타입(int, bool, char, double 등) | 참조 타입(class, array, string 등) |
| 데이터 할당 방식 | 자동 할당 및 해제 (함수가 끝나면 자동 정리) | GC(Garbage Collector)가 해제 |
| 수명 | 코드 블록이 끝나면 삭제된다. | 명시적으로 삭제하지 않으면 계속 유지된다. |
void StackExample()
{
int a = 10; // 스택에 저장
int b = 20; // 스택에 저장
}
a와 b는 스택 메모리에 저장된다.StackExample() 함수가 끝나면, a와 b는 메모리는 자동으로 해제된다.class Person
{
public string Name;
}
void HeapExample()
{
Person p1 = new Person(); // 힙에 할당
p1.Name = "Alice";
}
Person 객체는 힙 메모리에 저장된다.p1 변수 자체는 스택에 저장되지만, 실제 객체는 힙에 저장된다.HeapExample() 함수가 끝나도, 힙에 남아있다. (GC가 수거하기 전까지 유지가 된다.)C#에서는 힙(Heap)에 할당된 객체를 자동으로 관리하는 시스템이 바로 GC(Garbage Collector)이다.
GC는 사용되지 않는 메모리를 자동으로 해제하여 메모리 누수를 방지한다.
GC는 메모리를 Generation 0, 1, 2로 나누어 관리한다.
| Generation | 특징 | 예제 |
|---|---|---|
| Gen 0 (Generation 0) | 새로 생성된 객체가 저장된다. (가장 빠르게 정리된다. | 지역 변수 객체 (new Person()) |
| Gen 1 (Generation 1) | 한 번 GC를 통과한 객체 (다시 사용될 가능성이 있는 객체, 조금 더 오래 유지되는 객체) | 중간 수명의 객체, 메서드 실행 중 유지되는 데이터, 캐시 데이터 |
| Gen 2 (Generation 2) | 장기간 유지되는 객체 (오랫동안 사용된다.) | 싱글톤 객체, 애플리케이션 전체에서 사용되는 데이터, 데이터베이스의 연결 등 |
여기서 GC는 Generation 0부터 우선적으로 정리하고, 필요하면 Generation 1과 2까지 정리한다.
즉, 짧게 사용하는 객체는 빠르게 정리되고, 오랫동안 사용하는 객체는 더 오래 유지된다.
일반적으로 GC는 자동으로 실행되지만, 필요하면 아래와 같이 명시적으로 실행할 수도 있다.
GC.Collect(); // 강제로 GC 실행
GC.WaitForPendingFinalizers(); // 모든 정리 작업이 끝날 때까지 대기
하지만, 너무 자주 호출하면 성능이 떨어질 수 있으므로 주의해야 한다!!
메서드 내부에서 생성되고 빠르게 사라지는 객체들은 Gen 0 에 할당된다.
void ShortLivedObjects()
{
for (int i = 0; i < 1000; i++)
{
string temp = "Hello, World!"; // 새롭게 생성된 객체 (Gen 0에 저장)
} // 메서드가 끝나면 temp는 더 이상 사용되지 않으므로 GC가 수거
}
조금 더 오래 유지되는 객체들은 Gen 1에 위치한다.
void MediumLivedObjects()
{
List<int> numbers = new List<int>(); // 리스트 객체는 힙에 저장됨 (초기에 Gen 0)
for (int i = 0; i < 100; i++)
{
numbers.Add(i); // 리스트에 데이터 추가 (객체 크기 증가)
}
// GC가 실행되더라도 리스트가 계속 사용되고 있으면 Gen 1으로 이동할 가능성이 있음
}
List<int>)는 한 번만 생성되고 계속 사용된다.오래 유지되는 객체들은 Gen 2로 이동하고, GC가 가장 적게 실행된다.
class Program
{
static readonly Singleton _instance = new Singleton(); // 애플리케이션이 종료될 때까지 유지됨
static void Main()
{
Console.WriteLine("Singleton is always here!");
}
}
class Singleton
{
public string Name = "Singleton Instance";
}
static readonly)는 애플리케이션이 실행되는 동안 유지된다.먼저, 짧게 사용되는 객체는 빨리 삭제할수록 메로리를 절약할 수 있기 때문이다.
Gen 0을 가장 자주 정리하는 이유는 메서드 내부에서 생성되는 짧은 수명의 객체가 가장 많기 때문이다.
그리고, 오래 사용하는 객체는 재생성 하는 비용이 크기 때문이다.
예를 들어, Gen2의 객체를 자주 삭제하면 다시 생성할 때 성능이 저하될 수 있다.
또한, GC는 성능 최적화를 위해 단계를 나눠서 객체를 관리한다.
Gen 0 → Gen 1 → Gen 2 순서로 점점 오래 가는 객체가 이동한다. 이렇게 단계가 나눠지면, GC는 Gen 0을 자주 실행하고, Gen 2는 가능한 한 오랫동안 유지혀러 한다.
void CreateShortLivedObject()
{
string temp = "Short-lived object"; // Gen 0에 할당
} // 메서드가 끝나면 temp가 필요 없으므로 GC가 수거함
➡ GC는 주기적으로 Gen 0을 청소해서 짧게 사용된 객체를 제거한다.
List<int> numbers = new List<int>(); // Gen 0
numbers.Add(10); // 사용 중이므로 GC 실행 시 삭제되지 않고 Gen 1으로 이동
➡ Gen 0에서 삭제되지 않은 객체는 Gen 1으로 이동한다.
➡ numbers리스트가 계속 사용되면, GC가 실행될 때 Gen 1으로 이동한다.
static readonly Singleton _instance = new Singleton(); // Gen 2에 위치
➡ Gen 1에서도 삭제되지 않은 객체는 Gen 2로 이동한다. GC는 Gen 2를 거의 정리하지 않는다.
➡ 싱글톤 객체처럼 오랫동안 사용되는 객체는 Gen 2에 저장된다.
| Generation | 특징 | 예시 |
|---|---|---|
| Gen 0 | 새롭게 생성된 객체, 가장 자주 GC 실행됨 | 메서드 내 지역 변수, 임시 문자열 |
| Gen 1 | Gen 0에서 살아남은 객체, 중간 수명의 데이터 | 컬렉션(List, Dictionary), 캐시 데이터 |
| Gen 2 | 장기간 유지되는 객체, GC가 가장 적게 실행됨 | 싱글톤 객체, 전역 객체, 데이터베이스 연결 |
GC는 다음과 같은 경우에 자동으로 실행된다.
GC.Collect() 호출 시GC.Collect()를 호출하면 GC가 실행된다.GC는 객체가 더 이상 사용되지 않는지 확인한 후 자동으로 정리하는데, 다음과 같은 3단계 과정(Mark, Sweep, Compact)이 존재한다.