2017. 10. 16.

[C#] 메모리 관리와 가비지 콜렉션(garbage collection) - 메모리 할당과 해제

개요

이 글은 다음과 같은 항목으로 구성될 예정입니다.




이번 글에서는 메모리 할당과 해제에 대해서 알아보겠습니다.

메모리 할당

지금까지 C#의 value type과 reference type에 대해서 알아보았습니다. 좀더 명확하게 설명하기 위해서 value type과 reference type이 메모리에 어떻게 할당되는지 살펴보고자 합니다.


CLR(Common Language Runtime)은 객체를 스택(stack)과 힙(heap)에 할당합니다. 스택은 first-in last-out 메모리 구조입니다. 용어 그대로 먼저 들어온 데이터가 가장 마지막에 빠져나가는 형태입니다. 스택은 매우 효율적입니다. 메소드가 호출되면 CLR은 현재 스택의 최상단을 기록(bookmark)합니다. 이 메소드는 처리할 데이터를 스택에 push하고 실행합니다. 메소드가 완료되면, CLR은 스택을 이전 기록지점(bookmark)로 돌려놓습니다. 이 과정을 보통 “popping”이라고 합니다. 스택의 모든 메모리 관리는 push와 pop으로 간단하게 처리됩니다. 간단한만큼 효율적입니다.


스택과는 달리 힙은 객체가 임의로 할당됩니다. 스택에서는 이전 메모리의 최상단에 데이터가 배치되었습니다. 힙이 나쁘기만 한건 아닙니다. 스택과 달리 힙은 객체를 힙 공간 내에 임의의 위치에 할당하고 해제할 수 있습니다. 잠시 후에 좀더 자세히 살펴볼 것이지만, 그래서 힙은 메모리 관리(memory manager)와 가비지 콜렉션(garbage collection)에 관련된 성능 부하가 발생합니다. 임의의 공간에 할당된 데이터를 관리하려면 스택과 달리 추가적인 관리가 필요하기 때문입니다.


스택과 힙이 어떻게 사용되는지 확인하기 위해서 다음 메소드를 살펴보겠습니다.


void CreateNewTextBox()
{
     TextBox myTextBox = new TextBox();             // TextBox는 class
}


이 메소드는 TextBox의 instance를 생성하고, 지역변수(local variable) myTextBox가 그것을 참조합니다. 지역변수는 스택에 저장되지만, instance 자체는 힙에 저장됩니다. 다시 말해 TextBox는 클래스이기때문에 instance의 데이터는 힙에 생성되지만, 이를 참조하는 myTextBox는 지역변수이기때문에 스택에 저장됩니다.


위 이미지를 보면 좀더 명확하게 그려질 것입니다. 클래스의 instance는 힙에, 이를 참조하는 지역변수는 스택에 존재하는 것을 알 수 있습니다. 가비지 콜렉션를 아시는 분은 이미 위의 예시 메소드가 가비지를 생성한다는 것을 알아차렸을 것입니다.


여기서 스택과 힙이 각각 저장하는 대상에 대해서 알아보겠습니다.


스택은 다음 두 가지를 저장하기 위해 사용됩니다.
  • reference-type의 지역변수와 매개변수에 대한 참조자(reference). 위의 그림에서 myTextBox reference를 가립니다.
  • Value-type의 지역변수와 메소드 매개변수(struct, integer, char, DateTiems 등등)


힙은 다음 두 가지를 저장하기 위해 사용됩니다.
  • reference-type의 instance가 포함하는 데이터
  • reference-type의 instance 내부에 구성된 모든 것들


위 이미지를 다시 살펴보겠습니다. TextBox로부터 생성한 instance 내부에는 Location, Size, Text 등등이 있습니다. 이 모든 것들은 reference-type 내부에 있는 것이므로 힙에 저장될 것입니다.

메모리 해제

위의 CreateNewTextBox() 함수가 실행을 마치면, 지역 변수로 스택에 할당된 myTextBox스코프(scope)를 벗어나게 되어 스택에서 pop됩니다. 하지만 힙에 할당되었고, myTextBox가 가리키고 있었던 instance는 어떻게 될까요? 사실 이 instance가 어떻게 되는지 걱정하지 않아도 됩니다. 왜냐하면 CLR(Common Language Runtime)의 가비지 콜렉터(garbage collector)가 잠시 후에 이런 instance를 찾아서 힙에서 해제(deallocate)하기 때문입니다. 가바지 콜렉터는 어떤 instance가 제거되어야 하는지 알 수 있습니다. 어떤 instance에 대한 유효한 참조가 더이상 없는 경우에 해당 instance를 해제하면 되기 때문입니다. C++ 프로그래머는 이런 방식에 조금 어색하거나 불편함을 느낄 수 있습니다. C++ 프로그래머는 해당 instance를 바로 제거하기를 원하기 때문입니다. 하지만 C#은 해당 instance를 명시적으로 제거할 수 있는 방법이 없습니다. 우리는 CLR의 메모리 해제에 의지해야만 합니다. ‘.NET 프레임워크’는 모두 마찬가지입니다.


CreateNewTextBox() 함수를 좀더 살펴보도록하겠습니다. 함수가 실행되어 TextBox의 instance가 하나 생성되면 메모리에는 다음과 같은 일이 벌어집니다.


TextBox의 instance가 힙에 생성되었습니다. 이 instance를 참조하는 지역변수 myTextBox는 스택에 저장됩니다. CreateNewTextBox() 함수가 실행을 마치고, 스택은 이전의 최상단(top)으로 돌아가게 됩니다. 이 과정에 지역변수 myTextBox는 제거됩니다. myTextBox가 제거되면 우리는 myTextBox가 가리키던 TextBox의 instance에 대한 유효한 참조난 하나도 남지 않습니다. 가비지 콜렉터는 이 instance를 바로 해제하지 않습니다. 힙 메모리 공간이 부족하지 않는 상태가 유지된다면 가비지 콜렉터는 대기합니다.


만약 힙 메모리 공간이 부족하게 되면 가바지 콜렉터는 힙 메모리 공간에서 유효한 참조가 없는 대상을 찾아 해제합니다. 이제 비로소 TextBox instance가 만들었던 가바지가 해제되었습니다.


하지만 자동 삭제(automatic destruction)에도 주의할 점이 있습니다. 메모리를 제외한 자원(특히 Windows handle, file handle, SQL handle 등의 handle)을 할당한 instance는 더이상 필요없어질 때에 handle이 제어하고 있는 자원을 명시적으로 해제하라고 요청해야 합니다. 여기서 이런 의문이 생길 수 있습니다.


“왜 객체의 finalizer에 그 자원(resource)를 해제하라는 코드를 넣지 않나요?”


finalizer는 instance가 파괴되지 전에 CLR이 실행하는 메소드입니다. 가비지 콜렉터는 메모리 문제에 관련있는 반면에, 자원 문제에 관련없습니다. 그래서 몇 기가 바이트의 메모리 공간을 가지고 있는 PC의 경우, 가비지 콜렉터가 한참 후에나 동작할 수도 있습니다.


자원 해제와 관련된 내용은 여기서 다루고자 하는 내용을 벗어나므로 생략하도록 하겠습니다. 궁금하신 분은 IDsiposable 클래스와 Dispose() 메소드를 확인해보시길 바랍니다.

다음에 다룰 내용은 무엇인가요?


오늘 다룬 메모리 할당과 해제에 대한 지식을 바탕으로 가바지가 생성되는 상황에 대해서 좀더 자세하게 다룰 예정입니다. 그리고 가비지 생성을 최소화하도록 주의할 점도 알아보겠습니다.

댓글 없음:

댓글 쓰기