레이블이 메모리 할당인 게시물을 표시합니다. 모든 게시물 표시
레이블이 메모리 할당인 게시물을 표시합니다. 모든 게시물 표시

2017. 10. 18.

[C#] 메모리 관리와 가비지 콜렉션(garbage collection) - 가비지 콜렉션을 유발하는 예시

개요

이 글은 다음과 같은 항목으로 구성되어 있습니다. 그리고 오늘은 가비지 콜렌셕을 정리하도록 겠습니다.




지금까지 Type에 대한 이해를 바탕으로 메모리 할당과 해제 대해서 알아보았습니다. 가비지 콜렉션이 발생하는 경우에 대해서도 이미 설명하였습니다. 이번에는 클래스의 instance가 참조하는 구조체와 같은 실제 상황을 예시로 지금까지의 내용을 적용해서 설명하려고 합니다. 그리고 이번 글을 끝으로 C#의 기본적인 가비지 콜렉션에 관한 연재는 마치도록 하겠습니다.

클래스에서 참조하는 구조체

Windows Forms 애플리케이션은 예시로 설명하도록 하겠습니다. Windows Forms를 예시로 사용하지만 Windows Forms이 무엇인지는 중요하지 않습니다. 클래스와 구조체가 메모리에서 어떻게 다르고 관리되는지를 알고 있기만 하면 전혀 새로운 내용은 없습니다. Type에 대해서 잘 이해하고 있고, 메모리가 어떻게 할당되고 해제되는지를 알고 있다면 오늘 설명은 복습하는 기분으로 보고 넘어갈 수 있습니다.


먼저 간단한 코드로 클래스와 코드를 보겠습니다.


Size s = new Size (100, 100);          // 구조체. value type입니다.
Font f = new Font ("Arial",10);        // 클래스. reference type입니다.


여기서 Font 클래스는 중에는 Size 구조체도 있습니다. 그림으로 보면 좀더 구체적으로 알 수 있습니다.


먼저 Font의 instance가 하나 생성되었습니다. Font는 클래스, 즉 reference type이므로 힙(heap)에 생성됩니다. 그리고 reference type을 가리키는 변수 f는 스택(stack)에 생성됩니다. 이전 글에서 설명하였듯이 reference type을 가리키는 변수는 스택에 생성됩니다. Size가 하나 생성되었습니다. Size는 구조체, 즉 value type이므로 스택에 생성됩니다. 이 구조체를 가리키는 변수 s도 역시 스택에 생성됩니다.


reference type인 클래스의 instance만 힙에 생성되었고, 나머지는 모두 스택에 생성된 상황입니다.


여기서 새로운 클래스 Form이 등장합니다. Form은 Size와 Font를 멤버변수로 가지는 클래스입니다. Form의 instance를 하나 생성하겠습니다.


Form myForm = new Form();


Form의 멤버변수 Size와 Font에 앞의 변수 s와 f를 대입하도록 하겠습니다.


myForm.Size = s;
myForm.Font = f;


지금까지의 메모리 상황을 그림으로 살펴보면 다음과 같습니다.




그림을 보면 새롭게 힙에 생성된 Form instance를 볼 수 있습니다. Form은 Size와 Font를 멤버변수로 가지고 있습니다. reference type을 가리키는 변수 myForm은 스택에 생성되어 있습니다. 그리고 myForm의 멤버변수 Size는 구조체이지만 reference type인 Form 클래스에 포함된 것이므로 힙에 생성됩니다. Font도 Form 클래스에 포함된 것이므로 힙에 생성됩니다.


그리고 myForm.Size에 미리 생성해둔 s를 대입합니다. 이전에 설명드렸듯이 value type을 대입하면 값이 복사됩니다. 즉, Size s의 복사본이 힙에 생성됩니다. myForm.Font에도 미리 생성해둔 f를 대입합니다. reference type의 변수를 대입하면 복사본이 생성되지 않고 reference가 그대로 대입됩니다. 지금까지 설명한 내용이 모두 적용된 결과가 바로 위의 그림입니다.


그러므로 myForm.Size는 s가 가리키는 Size와는 별개의 구조체입니다. myForm.Font는 f가 가리키는 Font와 동일합니다. 만약 s를 통해서 Width, Height를 수정하면 myForm.Size에 전혀 영향을 주지 못합니다. f를 통해서 FontFamily, Size 등을 수정하면 myForm.Font도 수정됩니다.

가비지 콜렉션을 유발하는 예시

코드를 먼저 살펴보겠습니다.


public class ExampleScript {
   float[] RandomList(int numElements) {
       var result = new float[numElements];
       
       for (int i = 0; i < numElements; i++) {
           result[i] = Random.value;
       }
       
       return result;
   }
}


기능은 매우 단순합니다. int로 갯수를 전달하면, 그 수 크기의 배열을 힙에 생성합니다. 만약 이 함수가 매우 잦게 호출된다면 어떻게 될까요? 호출될 때마다 배열이 생성될 것이고, 이 배열의 참조를 유지하지 않는다면 이 배열은 가비지가 됩니다. 이렇게 생성되는 가비지를 막으려면 다음과 같이 수정하는 것이 좋습니다.


public class ExampleScript {
   void RandomList(float[] arrayToFill) {
       for (int i = 0; i < arrayToFill.Length; i++) {
           arrayToFill[i] = Random.value;
       }
   }
}


차이는 간단합니다. 앞의 코드에서는 함수 내에서 새로운 배열을 만든 반면에, 이 코드에서는 배열을 매개변수로 전달받습니다. 더이상 힙에 새로운 배열이 생성되지 않습니다. 인자로 전달한 변수에 대한 참조만 잘 유지된다면, 이 배열은 가비지가 되지 않을 것입니다.


또 다른 예를 살펴보겠습니다. 다음 함수는 매 프레임 실행되는 함수입니다.


void Update()
{
   List myList = new List();
   DoSomething(myList);
}


단순한 함수입니다. 매 프레임마다 새로운 List를 생성하고, 이 List를 가지고 어떤 일을 합니다. List는 당연히 클래스입니다. 그러므로 매 프레임 우리는 List를 힙에 생성하고, Update() 함수를 빠져나오면 생성된 List는 모두 유효한 참조가 없으므로 가비지가 됩니다. 가비지를 생성하지 않도록 코드를 수정하면 다음과 같습니다.


private List myList = new List();
void Update()
{
   myList.Clear();
   DoSomething(myList);
}


Update() 함수가 매 프레임 실행되므로 이를 계속 생성하지 않고, 차라리 멤버변수로 가지고 있습니다. 이제 힙에는 한 개의 List만 생성됩니다. 매 프레임 이 List를 비워서 재사용합니다.


이 외에도 가바지를 유발하는 예시는 구글링하여 쉽게 찾을 수 있습니다. 각 예시를 보면서 지금까지 다룬 내용을 바탕으로 따져보면 좋을 것입니다.

정리하며


지금까지 우리는 C#의 Type, 메모리 할당과 해제를 통해서 가비지가 어떻게 생성되는지 알아보았습니다. 매 프레임 적은 양으로 생성되는 가비지는 언제 수거되어 메모리를 해제할지 우리는 알 수 없습니다. 문제는 가비지를 수집하고 메모리를 해제하는 것이 결코 가벼운 일이 아니라는 것입니다. 특히 CPU가 바쁘게 돌아가는 중에 가비지 콜렉션이 동작하면 이용자에게 불편함을 줄 수도 있습니다. 평소 가비지를 생성하지 않는 코드를 작성하는 습관을 가질 수 있도록 노력해야 겠습니다.

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() 메소드를 확인해보시길 바랍니다.

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


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

2015. 1. 29.

[OS]가상 메모리(Virtual Memory) - 1. 시스템과 메모리 할당 사이의 관계

 가상 메모리가 등장했다. 윈도우를 사용하다보면 어쩌다가 한 번쯤은 만나볼만한 녀석이다. 나는 종종 가상 메모리를 늘려달라는 잔소리를 Windows7으로부터 들은 경험이 있다.


  Google에서 검색해보면 가상 메모리의 크기를 조절하여 Windows7을 최적화할 수 있다는 글도 보인다. 이래나 저래나 가상메모리가 중요한 녀석일 것 같다는 생각은 가질만하다.


32비트 시스템과 프로세스 메모리 할당

 처음부터 '이거 뭐야?'하는 내용이 등장한다. 32비트 시스템에서 프로세스 생성 시 4G바이트의 메모리를 할당받을 수 있다고 한다. 시작부터 황당하기 그지 없다. 내가 전에 사용했던 컴퓨터는 32비트 시스템이고, 돈 좀 써서 2G바이트 RAM를 장착했었다. 그 당시에 상당히 좋은 사양이었고, 게임도 아주 잘 돌아갔다. 그런데 이상한 점은 32비트 시스템에서 프로세스 생성 시 4G바이트의 메모리를 할당받을 수 있다는 사실이다. 나는 분명 2G바이트 크기의 RAM을 가지고 있었다. '모자라는 상태에서 어떻게 메모리를 할당했지?', '뭐하러 4G바이트나 되는 크기를 할당하지? 윈도우 계산기 켜는데 4G를 할당한다고?' 등등의 의문이 생길 수 밖에 없다. 

 이런 의문들을 하나씩 해결해보자. 먼저 32비트 시스템에서 프로세스 생성 시 최대 4G바이트의 메모리를 할당받을 수 있다는데 왜 그런가? 32비트 시스템은 또 무슨 말인가? 이것에 대한 답을 찾기위해서는 시스템 버스(system bus)라는 것에 대해서 간단히 언급할 필요가 있다. 시스템 버스는 컴퓨터에서 데이터를 이곳에서 저곳으로 옮기는 통로와 이를 관리하는 장치들이라고 생각하자. 이 시스템 버스에서 데이터가 이동하는 실질적인 통로 역할을 하는 것이 바로 I/O 버스이다. 이 I/O 버스가 한번에 전달할 수 있는 데이터의 크기가 바로 32비트이다. 이것 말고도 32비트 시스템에서 32비트가 의미하는 것은 바로 CPU와 같은 장치가 한번에 처리할 수 있는 데이터의 크기이다. 정리하면 한번에 I/O 버스로 이동시킬 수 있는 데이터의 크기도 32비트이고, CPU가 처리할 수 있는 데이터의 크기도 32비트이다. 이 둘의 크기가 같은 것은 우연이 아니다. 한번에 32비트를 처리할 수 있는데, 16비트를 전달해주는 것도 웃긴 일이고 64비트를 전달해주는 것은 무리이다.


 위 그림에서 address bus, control bus, data bus라고 표시된 것들이 바로 시스템 버스에 속하는 녀석들이다. 각 장치들을 연결하면서 데이터 전달 통로 역할을 한다는 사실에 집중하자. 그리고 3가지 bus들의 역할은 각각 다르지만 여기서는 넘어가도록 하겠다.

 그런데 프로세스와 쓰레드를 포스팅할 때도 그랬지만 프로그래머에게 가장 중요한 것 중 하나가 메모리 아니던가. 프로그래머는 메모리를 생각하지 않을 수 없다. 32비트 시스템을 생각할 때도 메모리 측면에서 접근해보자. 

 또 어이없는 상황을 가정해보자. 나는 8비트 시스템에 100G 바이트 RAM를 소유하고 있다. 100G바이트라니 엄청 신나는 일이다. 과연 신나는 일인지 생각해보자. 먼저 8비트 시스템에서는 메모리 주소 공간을 표현할 수 있는 범위는 2^8 만큼이다. 왜 2^8인가? 주소값을 표현하는데 8비트 이상 사용할 수 없기 때문이다.(주소값도 데이터 중 하나이다) 8비트 시스템이 한번에 처리할 수 있는 비트 수는 8개이고, 이에 따라 한번에 다룰 수 있는 주소값은 2^8만큼을 표현할 수 있다. 결국 2^8 = 256, 즉 0 ~ 255까지만 주소를 표현할 수 있다. 100G 바이트 RAM을 달아놓으면 뭐하나? 결국 쓸 수 있는 메모리 공간의 크기는 고작 256바이트 만큼이다. 1/4G 바이트 밖에 사용하지 못하였다.

 그러면 32비트 시스템의 경우는 어떠할까? 한번에 처리할 수 있는 데이터의 크기가 32비트이고, 주소값을 표현하는데 32비트를 모두 사용한다면 최대로 표현할 수 있는 메모리 공간의 크기는 2^32가 된다. 딱 봐도 커보이는 숫자이다. 이를 Windows에 있는 전지전능한 calc.exe를 실행시켜 계산해보면 4294967296이라는 숫자가 나온다. 이 숫자가 바로 4G 바이트를 의미하는 숫자이다. 32비트 시스템에서는 프로세스는 최대 4G 바이트를 할당받을 수 있다는 이유가 이제 설명되었으리라 믿는다.

 최근에는 64비트 시스템이 일반화되고 있다. 그럼 64비트 시스템의 경우 프로세스 당 최대 얼마만큼의 메모리를 할당할 수 있는가? calc.exe의 도움을 받아서 계산해보자. 무려 18446744073709551616라는 숫자가 계산된다. 그냥봐도 큰 수이다. 매우 크다. 여튼 중요한 사실은 한 번에 처리할 수 있는 데이터의 단위가 곧 몇 비트 시스템인지를 결정하고, 이는 곧 메모리를 나타내는 주소값을 표현하는데도 연결되는 사항이라는 사실이다.


아직도 해결되지 못한 궁금증

 시스템과 메모리 할당 사이의 관계는 이해했다. 그렇다면 이제 처음으로 돌아가서 32비트 시스템에서 2G 바이트 RAM에 어떻게 4G 바이트 메모리를 할당할 수 있는지 따져보자. 뜬금없지만 포스트이 길어졌으므로 다음 포스팅으로 넘겨야겠다.