- C#이 아닌 가령 C/C++ 언어로 프로그래밍은 메모리 관리가 귀찮다. C++에서는 객체를 할당하기 위해 일일이 메모리 공간을 확보해야 하며, 객체를 할당한 후에는 힙을 가리키는 포인터릴 잘 유지하고 있다가 객체를 다 사용하면 해당 포인터가 가리키고 있는 메모리를 해제해줘야 한다.
- C# 프로그래머들은 메모리 문제들로부터 자유롭다. CLR이 자동 메모리 관리(Automatic Memory Management) 기능을 제공한다. 자동 메모리 관리의 중심에는 **가비지 컬레션(Garbage Collection)**이 있다.
- 가비지 컬렉션은 "쓰레기 수거"라는 뜻으로, 더이상 사용하지 않는 객체를 수거한다.
- 이 덕분에 컴퓨터가 무한한 메모리를 가진 것 처럼 간주하고 코드를 작성 할 수 있게 한다.
- 가비지 컬렉터(Garbage Collector)는 객체 중에 쓰레기인 것과 쓰레기가 아닌 것을 완벽하게 분리해서 쓰레기들만 조용히 수거해 간다.
- 가비지 컬렉터가 최소한으로 자원을 사용하게 만들기 위해서, 가비지 컬렉터에 대해서 알아본다.
- 가비지 컬렉터가 쓰레기를 어떻게 수집하는지에 대해서 알려면, CLR이 어떻게 객체를 메모리에 할당하는지 알아야 한다.
- CLR은 프로그램을 위한 일정 크기의 메모리를 확보한다. C-런타임처럼 메모리를 쪼개는 일은 하지 않는다.
- 하나의 관리되는 힙(Managed Heap)을 마련하고, 할당한다.
- 다음과 같은 코드가 있다고 하자.
if ( true ) { object a = new object(); }
- if 블록은 하나의 객체 A 로써 힙에 저장된다. 그리고 그 주소를 스택에 있는 a가 가리킨다.
- if 블록이 끝나면 객체 A가 위치하고 있는 메모리를 참조하는 a는 존재하지 않게 된다.
- 객체 A는 어느 코드에도 접근할 수 없기 때문에 쓰레기가 되었다. 이를 가비지 컬렉터가 수거해간다.
- 가비지 컬렉터가 루트 목록을 애용해서 쓰레기 객체를 정리하는 과정은 다음과 같다.
- 작업을 시작하기 전에, 가비지 컬렉터는 모든 객체(ABCDEF)가 쓰레기라고 가정한다. 즉, 루트 목록 내의 어떤 루트도 메모리를 가리키지 않는다고 가정한다.
- 루트 목록을 순회하면서 각 루트가 참조하고 있는 힙 객체와의 관계 여부를 조사한다. 만약 루트가 참조하고 있는 힙의 객체가 또 다른 힙 객체를 참조하고 있다면 이 역시도 해당 루트와 관계가 있는 것으로 판단한다(A CD F). 이때 어떤 루트와도 관계가 없는 힙의 객체들은( B E ) 쓰레기로 간주된다.
- 쓰레기 객체가 차지하고 있던 메모리는 이제 빈 공간이 된다.
- 루트 목록에 대한 조사가 끝나면, 가비지 컬렉터는 이제 힙을 순회하면서 쓰레기가 차지했건 빈 공간에 쓰레기의 인접 객체들(A CD F)을 이동시켜 차곡차곡 채워넣는다.(ACDF) 모든 객체 이동이 끝나면 깨끗한 상태의 메모리(A CD F -> ACDF)를 얻게 된다.
- CLR 메모리 구역을 나누어 메모리에서 빨리 해제될 객체와 오래도록 살아남을 것 같은 객체들을 따로 담아 관리한다.
- 구체적으로 CLR은 메모리를 0, 1, 2의 3개의 세대로 나누고 0세대에는 빨리 사라질 것, 2세대는 오래 살아남을 것 같은 객체를 따로 담아 관리한다.
- 따라서 가비지 컬렉션을 한 번도 격어보지 못한 갓 생선된 객체는 0세대에 위치하고, 최소 2회에서 수차례 가비지 컬렉션을 격고도 살아남은 객체들이 위치한다.
- 2세대의 힙이 가득 차게 되면 CLR은 애플리케이션의 실행을 잠시 멈추고 Full GC(Full Garvage Collection)을 수행함으로써 여유 메모리를 확보하려 든다.
- 이때 애플리케이션이 차지하고 있던 메모리가 크면 클 수록 Full GC 시간이 길어지므로 문제가 생길 수 있다.
- CLR의 가비지 컬렉션 메커니즘에 근거한 효율적인 코드 작성을 위한 지침이 몇 가지 존재한다.
- 객체를 너무 많이 할당하지 마세요
- 너무 큰 객체 할당은 피하세요
- 너무 복잡한 참조 관계는 만들지 마세요
- 루트를 너무 많이 만들지 마세요
- CLR의 객체 할당 속도가 빠르긴 하지만, 너무 많은 수의 객체는 관리되는 힙의 각 세대에 대해 메모리 포화를 초래하고, 빈번한 가비지 컬렉션을 부르는 결과를 낳는다.
- 꼭 필요한 객체인지 아닌지 여부를 고려하자.
- CLR은 보통 크기의 객체를 할당하는 힙과는 별도로 85KB 이상의 대형 객체 힙에 대비되는 개념으로 "대형 객체 힙 (LOH, Karge Object Heap)"을 따로 유지한다.
- 평소 사용하는 힙은 소형 객체 힙(SOH, Small Object Heap)이라고 부른다.
- 수MB~수백MB의 경우 메모리를 복사하는 비용이 너무 비싸기 때문에, 힙 할당의 성능뿐만 아니라 메모리 공간 효율도 크게 떨어진다.
- 또한 CLR이 LOH를 2세대 힙으로 간주하기 때문에 조심스러워야 한다.
- 가비지 컬렉션의 성능이 아닌 코드의 가독성을 위해서라도 따라야 한다.
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; }
- 읽기 어렵다. 그림으로 바꿔봐도 어렵다.
Loading
flowchart LR D & A--->C B & C & D--->A C & D ---> B
- 이러한 참조 관계가 많은 객체는 각비지 컬렉션 후에 살아남았을 때가 문제이다.
- 가비지 컬렉터는 가비지 컬렉션 후에 살아남은 객체의 세대를 옮기기 위해 메모리 복사를 수행한다. 이때 참조 관계가 복잡한 객체의 경우에는 단순히 메모리 복사를 하는데 끝나는 것이 아닌, 객체를 구성하고 있는 각 필드 객체간 찾고 관계를 일일이 조사해서 메모리 주소를 전부 수정한다.
- 이는 메모리 복사만으로 끝날 문제가 탐색과 수정으로 끌어들이게 된다.
- 또, D 클래스의 경우 D 자체는 오래되어 2세대에 있다고 할 때, A 형식의 필드 a를 새로 생성하여 0세대에 있다고 하면 0세대에 존재하는 a는 수거될 수 있다. 이때 수거되는 것을 방지하기 위해 쓰기 장벽(Write Barrier)이라는 장치가 생성된는데, 오버헤드가 꽤 크기 때문에 문제가 된다.
- 가비지 컬렉터는 루트 목록을 돌면서 쓰레기를 찾아낸다.
- 루트 목록이 작아지면 그만큼 빨리 카비지 컬렉션을 끝낼 수 있다.
- 필요 이상으로 루트를 가급적 만들지 않는 것이 성능에 유리하다.
- 잘 못된 코딩 습관은 문제가 몇가지 누적되다간 골치아픈 문제로 발전한다.
- 공부한 내용들을 염두에 두고 프로그래밍 경험을 쌓아서, 지식에서 습관으로 바꾸자.