Skip to content

Latest commit

 

History

History
120 lines (91 loc) · 7.56 KB

File metadata and controls

120 lines (91 loc) · 7.56 KB

22. 가비지 컬렉션

22.1 가비지 컬렉터를 아시나요?

  • C#이 아닌 가령 C/C++ 언어로 프로그래밍은 메모리 관리가 귀찮다. C++에서는 객체를 할당하기 위해 일일이 메모리 공간을 확보해야 하며, 객체를 할당한 후에는 힙을 가리키는 포인터릴 잘 유지하고 있다가 객체를 다 사용하면 해당 포인터가 가리키고 있는 메모리를 해제해줘야 한다.
  • C# 프로그래머들은 메모리 문제들로부터 자유롭다. CLR이 자동 메모리 관리(Automatic Memory Management) 기능을 제공한다. 자동 메모리 관리의 중심에는 **가비지 컬레션(Garbage Collection)**이 있다.
  • 가비지 컬렉션은 "쓰레기 수거"라는 뜻으로, 더이상 사용하지 않는 객체를 수거한다.
  • 이 덕분에 컴퓨터가 무한한 메모리를 가진 것 처럼 간주하고 코드를 작성 할 수 있게 한다.
  • 가비지 컬렉터(Garbage Collector)는 객체 중에 쓰레기인 것과 쓰레기가 아닌 것을 완벽하게 분리해서 쓰레기들만 조용히 수거해 간다.
  • 가비지 컬렉터가 최소한으로 자원을 사용하게 만들기 위해서, 가비지 컬렉터에 대해서 알아본다.



22.2 개처럼 할당하고 정승처럼 수거하라

  • 가비지 컬렉터가 쓰레기를 어떻게 수집하는지에 대해서 알려면, CLR이 어떻게 객체를 메모리에 할당하는지 알아야 한다.
  • CLR은 프로그램을 위한 일정 크기의 메모리를 확보한다. C-런타임처럼 메모리를 쪼개는 일은 하지 않는다.
  • 하나의 관리되는 힙(Managed Heap)을 마련하고, 할당한다.
  • 다음과 같은 코드가 있다고 하자.
    if ( true ) {
      object a = new object();
    }
  • if 블록은 하나의 객체 A 로써 힙에 저장된다. 그리고 그 주소를 스택에 있는 a가 가리킨다.
  • if 블록이 끝나면 객체 A가 위치하고 있는 메모리를 참조하는 a는 존재하지 않게 된다.
  • 객체 A는 어느 코드에도 접근할 수 없기 때문에 쓰레기가 되었다. 이를 가비지 컬렉터가 수거해간다.
  • 가비지 컬렉터가 루트 목록을 애용해서 쓰레기 객체를 정리하는 과정은 다음과 같다.
    1. 작업을 시작하기 전에, 가비지 컬렉터는 모든 객체(ABCDEF)가 쓰레기라고 가정한다. 즉, 루트 목록 내의 어떤 루트도 메모리를 가리키지 않는다고 가정한다.
    2. 루트 목록을 순회하면서 각 루트가 참조하고 있는 힙 객체와의 관계 여부를 조사한다. 만약 루트가 참조하고 있는 힙의 객체가 또 다른 힙 객체를 참조하고 있다면 이 역시도 해당 루트와 관계가 있는 것으로 판단한다(A CD F). 이때 어떤 루트와도 관계가 없는 힙의 객체들은( B E ) 쓰레기로 간주된다.
    3. 쓰레기 객체가 차지하고 있던 메모리는 이제 빈 공간이 된다.
    4. 루트 목록에 대한 조사가 끝나면, 가비지 컬렉터는 이제 힙을 순회하면서 쓰레기가 차지했건 빈 공간에 쓰레기의 인접 객체들(A CD F)을 이동시켜 차곡차곡 채워넣는다.(ACDF) 모든 객체 이동이 끝나면 깨끗한 상태의 메모리(A CD F -> ACDF)를 얻게 된다.



22.3 세대별 가비지 컬렉션

  • CLR 메모리 구역을 나누어 메모리에서 빨리 해제될 객체와 오래도록 살아남을 것 같은 객체들을 따로 담아 관리한다.
  • 구체적으로 CLR은 메모리를 0, 1, 2의 3개의 세대로 나누고 0세대에는 빨리 사라질 것, 2세대는 오래 살아남을 것 같은 객체를 따로 담아 관리한다.
  • 따라서 가비지 컬렉션을 한 번도 격어보지 못한 갓 생선된 객체는 0세대에 위치하고, 최소 2회에서 수차례 가비지 컬렉션을 격고도 살아남은 객체들이 위치한다.
  • 2세대의 힙이 가득 차게 되면 CLR은 애플리케이션의 실행을 잠시 멈추고 Full GC(Full Garvage Collection)을 수행함으로써 여유 메모리를 확보하려 든다.
  • 이때 애플리케이션이 차지하고 있던 메모리가 크면 클 수록 Full GC 시간이 길어지므로 문제가 생길 수 있다.



22.4 가비지 컬렉션을 이해했습니다. 우리는 뭘 해야 하죠?

  • CLR의 가비지 컬렉션 메커니즘에 근거한 효율적인 코드 작성을 위한 지침이 몇 가지 존재한다.
    • 객체를 너무 많이 할당하지 마세요
    • 너무 큰 객체 할당은 피하세요
    • 너무 복잡한 참조 관계는 만들지 마세요
    • 루트를 너무 많이 만들지 마세요

22.4.1 객체를 너무 많이 할당하지 마세요

  • CLR의 객체 할당 속도가 빠르긴 하지만, 너무 많은 수의 객체는 관리되는 힙의 각 세대에 대해 메모리 포화를 초래하고, 빈번한 가비지 컬렉션을 부르는 결과를 낳는다.
  • 꼭 필요한 객체인지 아닌지 여부를 고려하자.

22.4.2 너무 큰 객체 할당은 피하세요

  • CLR은 보통 크기의 객체를 할당하는 힙과는 별도로 85KB 이상의 대형 객체 힙에 대비되는 개념으로 "대형 객체 힙 (LOH, Karge Object Heap)"을 따로 유지한다.
  • 평소 사용하는 힙은 소형 객체 힙(SOH, Small Object Heap)이라고 부른다.
  • 수MB~수백MB의 경우 메모리를 복사하는 비용이 너무 비싸기 때문에, 힙 할당의 성능뿐만 아니라 메모리 공간 효율도 크게 떨어진다.
  • 또한 CLR이 LOH를 2세대 힙으로 간주하기 때문에 조심스러워야 한다.

22.4.3 너무 복잡한 참조 관계는 만들지 마세요

  • 가비지 컬렉션의 성능이 아닌 코드의 가독성을 위해서라도 따라야 한다.
    class A {
      public C c;
    }
    
    class B {
      public A a;
    }
    
    class C {
      public A a;
      public B[] b;
    }
    
    class D {
      public A a;
      public B b;
      public C c;
    }
  • 읽기 어렵다. 그림으로 바꿔봐도 어렵다.
    flowchart LR
    D & A--->C
    B & C & D--->A
    C & D ---> B
    
    Loading
  • 이러한 참조 관계가 많은 객체는 각비지 컬렉션 후에 살아남았을 때가 문제이다.
  • 가비지 컬렉터는 가비지 컬렉션 후에 살아남은 객체의 세대를 옮기기 위해 메모리 복사를 수행한다. 이때 참조 관계가 복잡한 객체의 경우에는 단순히 메모리 복사를 하는데 끝나는 것이 아닌, 객체를 구성하고 있는 각 필드 객체간 찾고 관계를 일일이 조사해서 메모리 주소를 전부 수정한다.
  • 이는 메모리 복사만으로 끝날 문제가 탐색과 수정으로 끌어들이게 된다.
  • 또, D 클래스의 경우 D 자체는 오래되어 2세대에 있다고 할 때, A 형식의 필드 a를 새로 생성하여 0세대에 있다고 하면 0세대에 존재하는 a는 수거될 수 있다. 이때 수거되는 것을 방지하기 위해 쓰기 장벽(Write Barrier)이라는 장치가 생성된는데, 오버헤드가 꽤 크기 때문에 문제가 된다.

22.4.4 루트를 너무 많이 만들지 마세요

  • 가비지 컬렉터는 루트 목록을 돌면서 쓰레기를 찾아낸다.
  • 루트 목록이 작아지면 그만큼 빨리 카비지 컬렉션을 끝낼 수 있다.
  • 필요 이상으로 루트를 가급적 만들지 않는 것이 성능에 유리하다.

22.4.5 작은 구멍이 댐을 무너뜨립니다

  • 잘 못된 코딩 습관은 문제가 몇가지 누적되다간 골치아픈 문제로 발전한다.
  • 공부한 내용들을 염두에 두고 프로그래밍 경험을 쌓아서, 지식에서 습관으로 바꾸자.