레이블이 Garbage Collection인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Garbage Collection인 게시물을 표시합니다. 모든 게시물 표시

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. 15.

[C#] 메모리 관리와 가비지 콜렉션(garbage collection) - Type에 대한 이해

들어가기

저의 첫 프로그래밍 언어는 C였습니다. C를 공부하면서 C++로 공부하기 시작하였습니다. 이후 C#을 사용하게 되었고, C 계열 언어라서 쉽게 사용할 수 있을 거라고 생각했습니다. 하지만 겉보기와는 다르게 C#은 C나 C++과 완전히 다른 특성의 언어라는 것을 얼마되지 않아서 알게 되었습니다. 특히 Value 타입과 Reference 타입에 대한 접근 방식은 생소하였습니다. 이 개념은 가비지 콜렉션(Garbage Collection)과도 관련된 개념이라서 중요합니다. 이번 글은 C#의 Value 타입과 Reference 타입의 차이를 살펴볼 것입니다. 이 차이를 이해해야 C#의 메모리 관리 및 가비지 콜렉션에 대해서 알 수 있습니다. 참고로 이 글은 C#에 대한 기초 지식이 필요합니다.

개요

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




이번 글에서는 Type에 대해서 알아보겠습니다.

구조체(struct)

먼저 구조체(struct)를 살펴보겠습니다. 구조체가 무엇일까요? 상속과 객체를 소멸시킬 수 있는 finalizer를 지원하지 않는 클래스(class)를 상상해봅시다. 그게 바로 구조체입니다. 구조체는 class와 동일하게 정의됩니다. 방금 언급한 제한말고 또 다른 점은 struct라는 키워드를 사용한다는 것입니다. 나머지는 동일합니다. struct는 멤버 변수 및 함수를 가질 수 있고, property와 연산자(operator)를 가질 수 있습니다. 아래의 코드는 단순한 struct를 잘 보여줍니다.


struct Point
{
  private int x, y;             // private fields

  public Point (int x, int y)   // constructor
  {
        this.x = x;
        this.y = y;
  }

  public int X                  // property
  {
        get {return x;}
        set {x = value;}
  }

  public int Y
  {
        get {return y;}
        set {y = value;}
  }
}    

Value Type과 Reference Types

앞에서 간단히 구조체와 클래스의 차이에 대해서 알아보았습니다. 이 차이에 대해서 좀더 살펴보려고 합니다. 사실 지금 언급하는 것이 가장 중요한 차이일 것입니다. 구조체는 value type입니다. 반면에 클래스는 reference type입니다. 프로그램이 실행되면 이 둘은 다르게 처리됩니다. value type instance는 생성되고, 값을 저장하기 위해서 메모리의 단일 공간에 할당됩니다. int, float, bool, char와 같은 기본(primitive) type도 value type이고 구조체와 같은 방식으로 처리됩니다. 프로그램이 실행되어 value type을 처리할 때, 기본적인 데이터를 직접 처리하기 때문에 매우 효과적입니다.


하지만 reference type object가 메모리에 생성될 때에는 포인터와 같이 분리된 참조로 제어합니다. 다음 코드의 Point는 구조체이며 Form은 클래스라고 가정하겠습니다. 그리고 Point와 Form을 각각 하나씩 생성하겠습니다.


Point p1 = new Point();         // Point는 구조체
Form f1 = new Form();           // Form은 클래스


이 경우 p1을 위한 Point가 단일 메모리 공간에 할당됩니다. 반면에 f1을 위한 Form은 두 공간에 나누어져 할당됩니다. 하나는 Form 자체를 위한 공간이고, 다른 하나는 참조 f1을 위한 공간입니다. 지금부터 이 차이에 대해서 알아보겠습니다.


Form f1;                        // Allocate the reference
f1 = new Form();                // Allocate the object


이제 다음과 같이 객체를 새로운 변수에 복사합니다.


Point p2 = p1;
Form f2 = f1;


구조체 p2는 독립적인 p1 복사본이 됩니다. 이 독립적인 복사본은 분리된 자신의 field를 가집니다. 그래서 p1과 p2는 각각의 객체를 가리킵니다. 하지만 f2는 reference만 복사됩니다. 결과적으로 f1과 f2는 동일한 객체를 가리키고 있습니다.


메소드(method)에 매개변수를 전달할 때에는 이런 C#의 특성을 더 주목해야합니다. 매개변수는 (기본적으로) value로 전달됩니다. value로 전달된 매개변수는 묵시적으로 복사되는 것입니다. value-type 매개변수는 (p2의 경우와 같이) 메모리를 실제로 복사합니다. 반면에 reference-type 매개변수는 (f2의 경우와 같이) reference만 복사합니다. reference가 가리키는 메모리 공간을 복사하지 않습니다. 다음 코드를 살펴보겠습니다.


Point myPoint = new Point (0, 0);      // value-type
Form myForm = new Form();              // reference-type
Test (myPoint, myForm);                

void Test (Point p, Form f)
{
     p.X = 100;                 // myPoint에 영향없음. p는 myPoint의 복사본
     f.Text = "Hello, World!";  // f는 myForm과 동일 대상 가리킴.
                               
     f = null;                  // f만 null, myForm은 여전히 유효
}


앞에서 언급하였듯이 value type을 매개변수로 전달하면 복사본을 생성합니다. Test() 함수의 p 매개변수는 myPoint의 복사본을 메모리에 생성하고, 그 복사본을 가리킵니다. 반면에 f 매개변수는 reference type이므로 myForm이 가리키는 instance을 복사하지 않고 참조인 myForm만 복사합니다. value type과는 다르게 reference 자체만 복사된다는 점에 주목합시다.


Test() 함수 내부에서 벌어지는 일을 더 자세히 살펴보겠습니다. 매개변수 p가 가리키는 대상은 myPoint가 가리키는 것과는 다른 복사본입니다. 그래서 p.X에 다른 값을 넣더라도 myPoint에는 영향을 주지 않습니다. 매개변수 f는 참조를 복사한 것이므로 f가 가리키는 대상은 myForm이 가리키는 것과 동일합니다. 그래서 f.Text 값을 변경하면 myForm.Text이 수정됩니다. 그런데 f에 null을 대입하면 myForm에는 아무런 영향을 미치지 않습니다. myForm도 null이 될 것으로 착각할 수도 있습니다. 하지만 f는 myForm이 가리키는 메모리 공간에 대한 참조입니다. f에 null을 대입하면 f는 더이상 이 공간을 가리키지 않습니다. 그러므로 myForm에는 영향을 줄 수 없습니다. myForm은 여전히 instance를 유효하게 가리키고 있습니다.


함수의 매개변수를 ref 한정어(modifier)를 사용해서 설정할 수 있습니다. “ref”로 전달하면 메소드는 호출자(caller)의 인자에 직접 접근할 수 있습니다. 아래의 예시 코드를 살펴보겠습니다.


Point myPoint = new Point (0, 0);      // value-type
Form myForm = new Form();              // reference-type
Test (ref myPoint, ref myForm);        // 참조로 전달

void Test (ref Point p, ref Form f)
{
     p.X = 100;                       // myPoint.X 수정
     f.Text = "Hello, World!";        // myForm.Text 수정
     f = null;                        // myForm에 null을 대입!
}


Test() 함수의 매개변수는 모두 ref로 설정되어 있습니다. 만약 우리가 인자를 ref로 전달하면 인자의 type에 상관없이 모두 원본 instance에 접근하여 수정할 수 있습니다.

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

이번 글에서는 C#에서 value-type과 reference-type을 처리하는 과정이 어떻게 다른지 간단하게 살펴보았습니다. 다음에는 이 차이점을 메모리 관점에 좀더 상세하게 살펴보고자 합니다. value-type과 reference-type이 스택(stack)과 힙(heap)에 어떻게 할당되고 해제되는지 알아볼 것입니다. 이를 이해하면 C#의 가비지 콜렉션(garbage collection)은 쉽게 이해될 것입니다.