CLR 환경에서의 메모리 구조
.NET Common Language Runtime(CLR) 위에서 실행되는 C# 애플리케이션은 논리적으로 두 가지 주요 메모리 영역으로 나뉩니다: 스택(Stack)과 힙(Heap, 즉 관리 힙). 이들 각각은 데이터의 종류와 수명에 따라 다르게 사용됩니다.
힙 (관리 힙)
- 동적 할당: 객체 인스턴스 및 복잡한 데이터 구조를 위한 동적 메모리 공간입니다.
- 자동 정리: 가비지 컬렉터(GC)가 더 이상 참조되지 않는 객체를 자동으로 제거합니다.
- 접근 속도 느림: 비연속적인 메모리 배치로 인해 스택보다 접근 속도가 느릴 수 있습니다.
스택
- 고속 접근: 연속된 메모리 블록을 사용하므로 매우 빠른 읽기/쓰기 성능을 제공합니다.
- 자동 관리: 함수 호출 시 생성되고 종료 시 자동으로 해제됩니다.
- 용량 제한: 일반적으로 크기가 작으며, 짧은 수명의 로컬 데이터에 적합합니다.
데이터 유형별 메모리 할당 규칙
1. 값 형식(Value Types)
값 형식은 int, float, bool 등과 같은 내장 형식이나 struct로 정의된 사용자 정의 형식을 포함합니다. 저장 위치는 사용 맥락에 따라 달라집니다.
로컬 변수 및 매개변수
메서드 내에서 선언된 값 형식은 일반적으로 스택에 할당됩니다.
void Calculate()
{
double radius = 5.5; // 스택에 저장
}
void Process(double input) // 매개변수도 스택에 복사됨
{
// 처리 로직
}
클래스의 필드로 포함된 경우
값 형식이 클래스 멤버로 존재하면, 해당 클래스 인스턴스와 함께 힙에 저장됩니다.
public class Circle
{
public double Radius; // Circle 객체가 힙에 있으면 Radius도 힙에 있음
}
구조체(struct) 내 필드
구조체 자체가 스택에 있으면 그 안의 필드도 스택에 있고, 힙에 있는 객체의 필드라면 전체가 힙에 위치합니다.
배열 요소
값 형식의 배열이라도 배열 자체는 힙에 할당되며, 모든 요소가 힙에 저장됩니다.
int[] scores = new int[5]; // scores 배열은 힙에 있음
익명 함수 또는 비동기 코드에서 캡처된 변수
람다식이나 async 메서드에서 지역 변수가 포획(captured)되면 스택에서 힙으로 승격됩니다.
Func<int> CreateCounter()
{
int count = 0;
return () => ++count; // count는 힙으로 이동
}
2. 참조 형식(Reference Types)
클래스, 델리게이트, 배열, 문자열 등은 참조 형식이며, 항상 힙에 인스턴스가 생성됩니다.
로컬 변수
참조 변수 자체는 스택에 있지만, 실제 데이터는 힙에 있으며 스택에는 그 주소만 저장됩니다.
void Example()
{
List<string> list = new List<string>();
// list 변수는 스택, new List<string>()는 힙
}
매개변수
참조 형식의 매개변수도 마찬가지로 스택에 참조만 전달됩니다 (기본적으로 참조 복사).
필드 및 배열 요소
모든 참조 형식의 멤버 필드나 배열 요소는 힙에 위치하며, 그들이 가리키는 객체 역시 힙에 존재합니다.
public class Container
{
public object Item; // 힙에 저장
public string[] Tags; // 배열과 요소 모두 힙
}
3. 정적 멤버(Static Members)
정적 필드는 인스턴스와 무관하게 하나의 공유된 메모리 공간에 저장됩니다. CLR은 이를 "타입 초기화 영역(Type Initialization Area)" 또는 "정적 데이터 영역"이라 부르며, 이 또한 힙의 특수 영역에 위치합니다.
public class Utility
{
public static int InstanceCount = 0; // 힙의 정적 영역에 저장
}
4. 상수(Constants)
컴파일 타임 상수(const)는 IL 코드에 직접 삽입되며, 런타임 중 메모리를 차지하지 않습니다. JIT 컴파일러가 필요할 때마다 값을 인라인으로 포함시킵니다.
public const double Pi = 3.14159; // 실행 파일에 포함, 런타임 메모리 미사용
5. 메서드(Methods)의 메모리 처리
메서드는 데이터가 아니라 코드이므로 스택이나 힙에 "할당"되는 개념과 다릅니다. 하지만 메서드에 대한 정보는 다음과 같이 관리됩니다.
메서드 테이블(Method Table)
CLR은 각 타입에 대해 메서드 테이블을 생성하며, 이는 힙의 특수 영역에 저장됩니다. 여기에는 모든 인스턴스 및 정적 메서드의 포인터가 포함됩니다.
호출 시 스택 프레임 생성
메서드가 호출될 때마다 현재 스레드의 스택에 새로운 스택 프레임이 만들어집니다. 이 프레임은 다음을 포함합니다:
- 로컬 변수
- 매개변수
- 반환 주소
this포인터 (인스턴스 메서드의 경우)
값 형식의 인스턴스 메서드 호출
구조체의 메서드를 호출할 때 this는 복사본으로 전달되므로 원본이 변경되지 않도록 주의해야 합니다.
public struct Point
{
public int X, Y;
public void Move(int dx, int dy)
{
X += dx; Y += dy; // 수정은 복사본에서 발생할 수 있음!
}
}
정적 메서드
정적 메서드는 특정 인스턴스와 연결되지 않으며, this 키워드를 사용할 수 없습니다. 타입 이름으로 직접 호출됩니다.
Math.Sqrt(16); // 인스턴스 필요 없음
델리게이트와 메서드 참조
델리게이트는 메서드를 가리킬 수 있는 참조 형식입니다. 델리게이트 인스턴스는 힙에 할당되며, 하나 이상의 메서드를 참조할 수 있습니다.
Action messageAction = () => Console.WriteLine("델리게이트 호출");
messageAction(); // 힙에 있는 델리게이트 인스턴스 실행
종합 요약
| 카테고리 | 저장 위치 | 비고 |
|---|---|---|
| 값 형식 - 로컬 변수 | 스택 | 일반적인 경우 |
| 값 형식 - 클래스 필드 | 힙 | 클래스 인스턴스와 함께 |
| 참조 형식 - 인스턴스 | 힙 | 변수는 스택에 참조만 |
| 정적 멤버 | 힙 (정적 영역) | 타입 당 하나 |
| 상수 | 없음 | IL에 인라인 포함 |
| 델리게이트 | 힙 | 참조 형식 |
| 배열 (모두) | 힙 | 요소 포함 전체 |
실제 예제와 메모리 흐름
using System;
struct Coordinate
{
public double Lat, Lng;
public void Display() => Console.WriteLine($"위치: {Lat}, {Lng}");
}
class Location
{
public string Label;
public Coordinate Position;
public static int TotalLocations = 0;
public Location(string label)
{
Label = label;
TotalLocations++;
}
public void InnerOperation()
{
Coordinate temp = new Coordinate { Lat = 37.5, Lng = 127.0 };
temp.Display();
Action log = () => Console.WriteLine($"작업: {Label}");
log();
}
}
class AppRunner
{
static void Main()
{
const int Version = 1; // 컴파일 상수
Coordinate home = new Coordinate { Lat = 35.0, Lng = 129.0 };
Location seoul = new Location("Seoul");
seoul.InnerOperation();
}
}
메모리 배치 시나리오
스택 프레임 (Main)
Version: 상수 → IL에 인라인home: 스택 주소 0x1000 (값 직접 저장)seoul: 스택 주소 0x2000 (힙 주소 0xA000 가리킴)
스택 프레임 (InnerOperation)
temp: 스택 주소 0x3000log: 스택 주소 0x4000 (델리게이트 인스턴스 힙 주소 0xB000)
힙 영역
0xA000: Location 객체 (Label="Seoul", Position={...})0xB000: Action 델리게이트 인스턴스0xC000: 정적 데이터 영역 - TotalLocations = 1