Skip to main content

[C#] 객체와 메모리




프로그램을 실행하면 4개의 힙이 생긴다. #

이름 설명
Process Heap
JIT Code Heap 모든 메서드는 호출시에 JIT 컴파일되며 그 결과(native code)는 프로그램의 ‘데이타가 저장되는 Managed Heap’과 전혀 다른 JIT Code Heap 영역에 저장된다. JIT 컴파일된 코드 영역은 (비록 메모리가 모자라 Paging을 할 수는 있으나) Code Heap에서 프로그램이 종료될 때까지(좀 더 정확히 AppDomain이 종료될 때까지) Clear되지 않는다. 이 영역은 GC가 관리하지 않는 영역이며, 관리할 수 없는 영역이다.
Small Object Heap GC가 관리하는 Managed Heap, 클래스의 필드들이 담긴다.
Large Object Heap GC가 관리하는 Managed Heap, 클래스의 필드들이 담긴다.



클래스의 객체를 생성하면 다음과 같은 구조가 Managed Heap에 생긴다. #

객체 메모리 구조
Image Source

구성요소 설명
Object Header SyncBlock의 인덱스 값을 가진다. 여기에는 객체의 lock, 해쉬값, Thunking 데이터, AppDomain 정보를 담고있다. 필요하지 않은 경우 경우 기본적으로 0을 할당한다.
Type Handle Method Table을 가리키는 포인터이다.
객체 필드들 부모 클래스의 필드들이 먼저 공간을 차지하고, 자식클래스의 필드들이 그 뒤에 추가된다.



객체의 메모리 레이아웃 #

  • CLR은 기본적으로 Optimiazation의 일환으로 클래스 필드의 순서를 자동으로 변경하는데 이것을 Auto Layout이라 부른다. Auto Layout은 Managed Memory 상에서 클래스 필드의 순서를 자동으로 재배치한다.
  • class와 같은 참조 타입은 디폴트로 Auto Layout을 사용한다.
  • struct와 같은 값 타입은 디폴트로 Squential Layout을 사용한다.
  • [StructLayout(LayoutKind.Explicit, Pack = 1)] 과 같이 사용해서 Managed Memory 상에서 필드에 저장된 [FieldOffset()] 속성에 따라 메모리 위치가 설정된다.
[StructLayout(LayoutKind.Explicit, Pack = 1)]
class MyClass
{
    [FieldOffset(0)]
    public int i;
    [FieldOffset(4)]
    public double d;
    [FieldOffset(12)]
    public byte b;
}

// MyClass 라는 클래스에 Explicit 레이아웃을 사용하여 
// 필드 i 가 처음에, 필드 d가 4번째 바이트 위치에, 필드 b가 12번째에 각각 위치하게 됨을 지정하고 있다.



struct는 값형식, class는 참조형식 #

struct Employee
{
    public string Name { get; set; }
    public int Salary { get; set; }
}

class HR
{
    public void RaiseSalary(Employee emp)
    {
        emp.Salary = emp.Salary + 1000;
        // struct는 값형식이므로 여기서 올린 Salary는
        // 복제본의 Salary이다. 
        // 따라서 원본이 변하지는 않는다. 
    }
}

class Main
{
    public void Run()
    {
        Employee emp = new Employee();
        emp.Name = "Lee";
        emp.Salary = 9000;

        HR hr = new HR();
        Console.WriteLine(emp.Salary); // 인상전 9000
        hr.RaiseSalary(emp);
        Console.WriteLine(emp.Salary); // 인상후 9000 ???
    }
}



interface를 상속받는 struct의 주의점 #

public interface IAdd
{
    void AddOne();
}

struct MyValue : IAdd
{
    int value;
    public int Value => value;

    public MyValue(int value)
    {
        this.value = value;
    }

    public void AddOne()
    {
        value++;
    }
}

class Main
{
    public void Run()
    {
        MyValue v1 = new MyValue(3);
        v1.AddOne();
        Console.WriteLine(v1.Value); // 4. 좋다.
        
        MyValue v2 = new MyValue(3);
        IAdd itf = v2 as IAdd;
        itf.AddOne();
        Console.WriteLine(v2.Value); // 3. ???
        
        // struct는 값 형식이다.
        // 그래서 인터페이스로 박싱하면 (값 -> 참조) 
        // 스택에 있던 값이 힙으로 복사되고, 힙에 있는 값이 증가된다. 
        // 그래서 기존 스택의 값을 출력하면 그대로이다. 
    }
}



method hiding에 대하여 #

class Animal
{
    public void Eat(int kg)
    {
        Console.WriteLine("Animal.Eat: {0} kg", kg);
    }
}

class Dog : Animal
{
    public void Eat(long kg)
    {
        Console.WriteLine("Dog.Eat: {0} kg", kg);
    }        
}

class Bulldog : Dog
{
    public void Eat(string kg)
    {
        Console.WriteLine("Bulldog.Eat: {0} kg", kg);
    }
}

class Main
{
    public void Run()
    {
        Bulldog bulldog = new Bulldog();
        int kg = 2;

        bulldog.Eat(kg);  // Dog.Eat() 호출

        ((Animal)bulldog).Eat(kg);  // Animal.Eat() 호출
        
        // 메서드 검색은 동일 메서드 명으로
        // 동일 타입 or 호환 가능한 타입의 파라미터를 발견할 때까자
        // 상위 클래스를 찾아 가면서 진행된다. 
        // 그래서 일단 중간에 조건에 맞는 메서드를 발견하면 검색을 중지하고 
        // 해당 메서드를 사용한다. 

        // 특정 메서드를 지정해서 호출하고 싶다면,
        // 해당 클래스로 캐스팅할 후 메서드를 호출하면 된다.
    }
}



ref를 클래스에 사용하는 경우 #

class DataSet
{
    public List<DataTable> Tables = new List<DataTable>();
}

class DataTable
{
    public List<string> Columns = new List<string>();
}

class Main
{
    public void AddTable(DataSet ds) // ref로 해야 함. 
    {
        DataSet newDs = new DataSet();
        DataTable dt = new DataTable();
        dt.Columns.Add("ColA");
        newDs.Tables.Add(dt);

        ds = newDs; 
        // ds 내부의 멤버를 변경할 순 있지만, 
        // 다른 새로운 레퍼런스 객체를 할당할 순 없다. 
        // 그래서 참조형식인 class도 ref를 사용해야 한다. 
    }
    
    public void Run()
    {
        DataSet ds = new DataSet();
        AddTable(ds);
        Console.WriteLine(ds.Tables.Count); // 0이 나온다. ???
    }
}



References #