C#에서의 메모리 관리와 데이터 저장 위치 분석

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: 스택 주소 0x3000
  • log: 스택 주소 0x4000 (델리게이트 인스턴스 힙 주소 0xB000)

힙 영역

  • 0xA000: Location 객체 (Label="Seoul", Position={...})
  • 0xB000: Action 델리게이트 인스턴스
  • 0xC000: 정적 데이터 영역 - TotalLocations = 1

태그: C# .NET CLR 메모리 관리 스택

6월 2일 00:44에 게시됨