본문 바로가기

C#(ASP.NET)

닷넷 가비지 컬렉터의 동작원리


흔히 닷넷기반의 환경을 관리되는(Managed) 환경으로 부르곤 한다. 그렇다면 도대체 무엇이 관리가 된다는 것인가? 그 관리의 주체는 바로 메모리이다. 이전 Native 시대에는 메모리를 할당하고 해제하는 부분을 개발자가 직접 처리하였지만 닷넷은 그 부분을 자동으로 관리해주는 것이다. 닷넷이 관리되는 환경의 수행이 가능할 수 있는 것은 바로 “가비지 컬렉터(Garbage Collector)”가 닷넷에 존재하기 때문이다. 관리환경의 장점을 최대한 활용하고 사용하기 위해서는 가비지 컬렉터에 대해서 잘 알아 두어야 하고 동작원리를 파악하고 있어야 한다. 필자는 가비지 컬렉터의 원리에 대한 내용을 2002년도 “Chappell의 .NET 여행”이라는 책에서 처음 접했었고, 이 내용은 닷넷의 메모리 관리를 이해하는데 어느 정도의 기반지식이 될 수 있었다.

1. 가비지 컬렉터와 가비지 컬렉션

가비지 컬렉터는 앞에서 설명한 것과 같이 메모리를 관리해주는 메카니즘이다. 많이 혼동하는 부분이 바로 가비지 컬렉터와 가비지 컬렉션일 수 있다. 가비티 컬렉터는 Mark&Compact 알고리즘을 이용하여 객체들의 관계를 추적한다. 즉, 인스턴스화 시켰던 DataSet에 null을 할당하면 DataSet은 사용하지 않는 객체로 간주되고 가비지 컬렉션시에 메모리 해제의 대상이 된다. 하지만 DataSet은 DataTable을 가지고 있고, DataRow, DataItem과 같은 여러 객체들을 참조하고 있다. 바로 DataSet이 해제가 되면 그와 상호관련이 있었던 모든 객체들 역시 메모리를 해제해야 할 것이고 바로 가비지 컬렉터는 이러한 복잡한 관계를 Mark&Compact 알고리즘을 이용해서 이해하고 각각 해제될 수 있는 것이다.

가비지 컬렉션은 바로 메모리를 해제하고 새롭게 재배치 하는 작업을 칭하고 자동으로 수행되지만 수동으로도 수행을 명령할 수도 있다. 가비지 컬렉션이 수행되는 시기는 개발자가 알 수 없지만 분명한 것은 메모리가 부족하면 분명 가비지 컬렉션이 일어난다는 것이다.

System.GC.Collect();

가비지 컬렉터는 세대별로 나누어서 메모리를 관리한다. 즉, 메모리를 관리하는 그릇이 3개가 존재한다고 보면 된다. 세대는 0,1,2 세대로 나누어지고 최초의 메모리는 무조건 0이라는 공간에서 관리가 된다고 보면 된다. 그리고 가비지를 한번 정리하였지만 해제되지 않은 객체는 바로 다음 세대로 이동하게 된다. 가비지 컬렉션은 세대별로 독립적이라고 보면 된다. 즉, 가비지 컬렉션이 일어날 때 0,1,2 세대 별로 동시에 발생하는 것이 아니라 개별적으로 가비지 컬렉션을 수행한다는 것이다. 최근의 생긴 메모리 수록 즉, 0세대일 수록 컬렉션이 많이 발생한다. 이것은 당연한 동작 원리이다. 보통 한번 사용한 객체는 꾸준히 많이 사용하지만 그렇지 않은 객체는 단기적으로 사용을 많이 하기 때문이다. 다음 [그림1]은 가비지 컬렉터가 가비지 컬렉션을 수행하는 장면을 보여주고 있다.

[그림1]가비지 컬렉션의 동작

이렇게 수행을 하고 남은 객체는 바로 그 위의 세대로 승격된다. 다음 [그림2]를 살펴보자.


[그림2]다음 세대로 승격


이 승격은 2세대까지 2번 승격된다. 0세대는 활발하게 가비지 컬렉션이 수행되지만 2세대는 거의 발생되지 않는다고 보면 된다.


2. 메모리의 효율적인 사용

앞에서 살펴본 가비지 컬렉터는 관리되는 환경의 모든 메모리를 책임지고 관리한다. 이전 Native 시대에서는 메모리 누수와 같은 문제가 발생하면 개발자의 책임이었지만 닷넷에서는 그 책임을 대신 주어준다는 것이다. 하지만 관리되어지는 환경이라고 해서 모든 것을 전적으로 가비지 컬렉터에 의존해서는 안 된다. 물론 가비지 컬렉터가 하드웨어가 무척 발전한 지금의 환경에 부응하고 있는 기능임은 틀림없다. 하지만 필자가 말하고자 하는 것은 가비지 컬렉션이 언제 어떤 객체를 수집하고 어떻게 동작하는 것임을 알아두고 그에 맞추어 코드를 작성해야 한다는 것이다. 이런 효츌적인 메모리 관리를 위해서 몇 가지 지침들을 적어보도록 하겠다.

- 코드에서 System.GC.Collect()를 이용해서 직접 가비지 컬렉션의 수행을 호출하는 것은 가급적 피한다.

닷넷에 연고가 있는 독자라면 가비지 컬렉션을 수동으로 수행하면 안좋다 라는 말을 많이 들어봤을 것이다. 그럼 이제 그 이유를 살펴보자. 이유는 크게 두 가지 이유가 있다. 첫 번째는 가비지 컬렉션이 수행하는 메카니즘이 생각보다 간단하지 않기 때문이므로 성능상 부하가 있을 수 있다. 왜냐하면 현재 객체가 사용 중인지 확인하는 작업이 필요하며 그 작업이 끝난 후에 객체를 파괴한다. 객체를 파괴할 때에는 참조되고 있는 객체 역시 파괴해야 한다는 것이다. 뿐만 아니라 파괴된 객체들의 빈자리를 매꾸기 위해서 가비지 컬렉터는 객체들을 재배치 작업(Compaction 작업)을 수행하게 되므로 메모리가 많을수록 그 부하가 클 수 있기 때문이다. 두 번째 이유는 객체가 승격되기 때문이다. 객체가 0세대에서 한번 승격되면 그 객체가 더 이상 사용하지 않는다 하더라도 자동적으로 그 객체가 정리될 확률이 줄어들기 때문이다. 그렇기 때문에 코드에서 직접 가비지 컬렉션의 수행의 호출을 추천하지 않는 것이다.

- 사용하지 않는 객체는 가비지 컬렉터가 수집할 수 있는 대상으로 설정한다.

그렇다면 가비지 컬렉터가 수집하는 대상은 어떤 대상인가? 좀 더 쉽게 이해하기 위해서 다음과 같은 클래스를 만들었다고 가정하자.
Class A
{
  string a="HOONS"
}
Main메서드()
{
  A aClass= new A();//클래스 생성
}


그렇다면 여기서 aClass객체를 사용하고 있는지 어떻게 계산할 것인가이다. 가장 쉬운 방법은 aClass에 null을 대입해 주면 되기도 하지만 가장 바람직한 방법은 using을 이용해서 객체를 사용하는 범위를 지정해주는 것이다. using은 IDisposable 인터페이스를 상속받아서 Dispose() 메서드를 구현해야 한다. 그리고 Dispose()에서는 클래스 안에서 사용했던 자원을 해제하는 것이다. finalizer(~생성자())를 구현한다면 가비지 컬렉터는 자원을 해제하기 바로 직전에 수행하게 될 것이다. 이 finalizer에서는 만약 Dispose()를 호출하지 않을 것을 대비해서 Dispose()를 수행하는 용도나 아니면 관리되지 않은 영역 즉, unmanaged 자원이 있다면 이 자원을 호출하면 된다.


- 전역변수는 가급적 초기화하지 않는다.

다음과 같은 코드가 있다고 가정하자.
Class A
{
  ArrayList a=new ArrayList();
  public A(int Length)
  {
      a=new ArrayList(Length);
  }
}



이 코드는 내부적으로 다음과 같이 동작된다.
Class A
{
  ArrayList a=new ArrayList();
  public A(int Length)
  {
      a=new ArrayList();
      a=new ArrayList(Length);
  }
}



그렇기 때문에 필요없는 가비지가 생기게 되는 것이다.

- 자주 사용하는 객체는 전역변수로 잡아서 가비지를 최소화한다.

자주 사용하는 객체는 여러 개를 만들어 사용할 없다는 것이다. 이 부분은 이론상 쉽게 이해할 수 있으므로 깊게 언급하지 않겠다.



3. CLR 프로필러

그렇다면 가비지 컬렉터에서 내부적으로 관리되고 있는 메모리를 어떻게 들여다 볼 수 있는 것인가? 이것은 Microsoft에서 제공하고 있는 CLR 프로필러(Profiler)라는 툴을 이용하면 된다. 이툴은 MS 다운로드 사이트에서 다운 받을 수 있으며 다음 [화면1]은 CLR 프로필러를 실행한 화면을 보여주고 있다.


[화면1] CLR 프로필러

이 프로필러는 각 세대별로 가비지 컬렉션이 일어난 횟수를 보여주고 있고 현재 각 세대별로 차지하고 있는 힙의 크기 그리고 할당되고 재할당 된 메모리 바이트를 보여주고 있다. 뿐만 아니라 참고 그래프를 제공하고 있기 때문에 메모리 사용을 분석하는데 큰 도움을 줄 수 있다. 이 툴의 자세한 내용은 이전 HOONS 닷넷에서 진행한 세미나에서도 언급한 적도 있고, 이전 마소에서도 다루었던 적이 있으므로 자세한 사용방법은 지면상 생략하도록 한다.

[출처] HOONS의 닷넷 프로필러