[C#] 가비지 컬렉션
Table of Contents
C, C++의 경우 #
- C, C++에서는 프로그래머가 수동으로 Heap 영역의 해제를 관리해주어야 한다.
- 또한 Heap에 객체를 할당하기 위해 비싼 비용을 치루어야한다.
- C, C++기반의 프로그램을 실행하는 C-런타임은 메모리를 여러 블록으로 나눈 뒤 이 블록들을 링크드 리스트로 묶어서 관리 한다. 따라서 객체를 메모리에 할당하기 위한 코드가 실행되면, 링크드 리스트를 순차적으로 탐색하면서 여유 있는 블록을 찾고, 찾으면 블록을 쪼개서 객체를 할당하고 링크드 리스트를 재조정한다.
- 따라서 단순히 객체 할당을 하는 것이 아니라 오버헤드가 발생한다.
C#의 경우 #
- C#은 CLR이 자동으로 메모리를 관리 해준다.
- 관리형 코드
- C#으로 작성된 모든 코드는 CLR에 의해 관리된다. CLR은 메모리 할당, 보안, 스레딩, 쓰레기 수거 등의 업무를 한다.
- 비관리형 코드
unsafe
키워드를 이용하면 CLR이 제공하는 서비스를 받을 수 없다.
- 관리형 코드
- CLR은 객체 할당이 전부이다.
- C-런타임처럼 메모리를 쪼개지 않고 메모리 공간을 통째로 확보해서 하나의 관리되는 힙(Managed Heap)을 마련한다. 그리고 다음 객체를 할당할 주소를 가리키는 포인터를 옮겨가면서 순서대로 객체를 할당한다.
- C#의 메모리 할당 방식은 C, C++과 달리 리스트를 탐색하거나 재조정하는 과정이 불필요하기 때문에 속도가 훨씬 빠르다.
- CLR이 메모리를 관리하는 방법
- 참조형식의 객체가 할당될 때는 스택영역에 힙의 메모리 주소(a)가, 힙 영역에 실제 값(A)이 할당된다.
- (a)처럼 할당된 메모리의 위치를 참조하고 있는 객체를 일컬어 루트(Root) 라고 부른다. 루트는 스택에 생성될 수도 있고, 정적 필드처럼 힙에 생성될 수도 있다.
- .NET 애플리케이션이 실행되면 JIT 컴파일러는 루트들을 목록으로 만들고, CLR이 이 루트 목록을 관리하며 상태를 갱신한다. 가비지 컬렉터는 이 루트 목록을 참조해서 쓰레기 수집을 한다.
- 여기서 만약에 스택 영역의 메모리가 회수되면 힙 영역 값이 쓰레기가 된다.
가비지 컬렉터 동작 순서 #
- 가비지 컬렉터는 힙 영역의 임계치에 다다르면 이 루트 목록을 참조해서 쓰레기를 수집한다.
- 모든 객체가 쓰레기라고 가정한다.
- 루트 목록을 순회하면서 참조하고 있는 힙 객체와 관계 여부를 조사한다. 어떤 루트와도 관계가 없다면 쓰레기로 간주한다.
- 쓰레기가 차지하고 있던 메모리를 회수하고 인접 객체들을 이동시켜서 차곡차곡 채워서 정리(Memory Compaction)한다.
세대별 가비지 컬렉션 #
- CLR의 메모리는 구역을 나눠서 관리한다.
- 0세대, 1세대, 2세대로 분리하여서 0세대는 빨리 사라질 객체, 2세대는 오래 남아있을 객체를 위치시킨다.
- 프로그램을 실행하면 0세대부터 할당된 객체들이 차오르기 시작한다.
- 0세대 가비지 컬렉션 임계치에 도달하면 0세대에 대해 가비지 컬렉션을 수행한다. 여기서 살아남은 객체는 1세대로 옮겨진다.
- 1번 과정을 반복하다보면, 1세대 가비지 컬렉션이 임계치에 도달하게 되고, 1세대에 대해 가비지 컬렉션을 수행한다. 여기서 살아남은 객체는 2세대로 옮겨진다.
- 2번 과정도 반복하다보면, 2세대 가비지 컬렉션이 임계치에 도달하게 되고, 이 때에는 0, 1, 2세대 전체 가비지 컬렉션을 수행한다.
- 2세대 힙이 가득차게 되면 CLR은 응용 프로그램의 실행을 멈추고 전체 가비지 컬렉션을 수행(Full GC) 하면서 메모리를 확보하려고 하기 때문에, 응용 프로그램이 차지하는 메모리가 많을 수록 프로그램이 정지하는 시간도 그 만큼 늘어나게 된다.
85KB이상의 대형 객체의 할당 #
- 대형 객체의 경우에는 0세대에 할당하면 가비지 컬렉션을 자주 수행하기 때문에 CLR은 85KB 이상의 대형 객체를 할당하기 위해 대형 객체 힙(Large Object Heap; LOH) 을 따로 유지하고 있다.
- 대형 객체가 할당될 때는 포인터를 사용하지 않고, 힙을 탐색하며 그 크기만큼 들어갈 수 있는 위치를 찾는다.
- 대형 객체 힙은 해제된 공간을 인접 객체를 이동시켜서 채우는 것이 아니라 그대로 둔다. 왜냐하면 복사하여 옮기는 비용이 너무 비싸기 때문이다. 이렇기 때문에 큰 공간 사이사이의 메모리를 낭비하게 된다.
- CLR은 대형 객체 힙을 2세대 힙으로 간주한다. 따라서 대형 객체 힙에 있는 쓰레기 객체가 수거되려면 2세대 가비지 컬렉션이 수행되어야 한다. 2세대 가비지 컬렉션은 전 세대에 대한 가비지 컬렉션을 유발하기 때문에 수거되는 메모리의 양이 클수록 어플리케이션이 정지되는 경우가 발생하게된다.
가비지 컬렉션 메서드 #
메서드명 | 설명 |
---|---|
GC.Collect() | 모든 세대 GC 즉시 수행 |
GC.Collect(int) | 세대 0에서 부터 지정된 세대까지 GC 즉시 수해 |
GC.CollectionCount(int) | 지정된 세대의 개체에 대해 GC가 수행된 횟수 반환 (GC가 언제 발생하는지 모니터링하는 가장 쉬운 방법) |
GC.GetGerneration(object) | obj의 현재 세대 반환 |
GC.MaxGeneration() | 시스템에서 현재 지원하는 가장 큰 세대 번호 반환 (시스템에 따라 0~n세대로 나누어 질 수 있음) |
주의점 #
- 객체를 너무 많이 할당하지 않는다.
- 너무 큰 객체를 할당하는 것을 피한다.
- 너무 복잡한 참조관계를 피한다.
- 복잡한 참조 관계를 가진 객체가 가비지 컬렉션 후에 살아 남으면, 세대를 옮기기 위해 메모리 복사를 진행하는데, 참조 관계가 복잡할 경우 객체를 구성하고 있는 각 필드 객체간의 참조관계를 조사하여 메모리 주소를 전부 수정해야 되기 때문에 탐색과 수정의 오버헤드가 발생한다.
- 또한 A객체가 2세대인데 A객체안에 B객체를 이제 막 생성하여 0세대로 되었다면, A의 인스턴스는 2세대에 있고 B 필드를 참조하는 메모리는 0세대에 위치하게 된다. 이때 0세대 가비지 컬렉션이 수행된다면 B필드가 수거될 수 있다. 하지만 CLR은 쓰기 장벽(Write barrier) 이라는 장치를 통해서 B필드가 루트를 갖고 있는 것으로 간주하게 해서 수거 되지 못하게 한다. 이 쓰기 장벽을 생성하는 데 오버헤드가 크다는 것이 문제가 된다.
- 루트를 너무 많이 만들지 않는다.
- 가비지 컬렉터는 루트 목록을 순회하면서 쓰레기를 찾아낸다.
- 루트 목록이 작아진다면 그만큼 가비지 컬렉터가 검사를 수행하는 횟수가 줄어들므로 더 빨리 가비지 컬렉션을 끝낼 수 있다.
Unity의 경우 #
Boehm-Demers-Weiser의 알고리즘 #
- 이것은 가비지콜렉터가 가비지 수집을 수행할때 프로그램 코드 실행 및 CPU 메인스레드를 중지하며 전체 힙을 검사하는 방식이다. (Stop the world 방식)
- 따라서 힙의 모든 오브젝트를 처리한 이후에 어플리케이션 실행을 재개함으로 성능에 영향을 미치는 GC Spike(중단으로 인해 프로파일러 윈도우의 그래프에서 나타나는 큰 스파이크)가 발생한다.
- 더 이상 할당할 수 있는 메모리가 없다면 Managed Heap의 크기를 2배로 늘린다. 줄어들지는 않는다.
- 세대 구분이나 SOH, LOH, 메모리 압축같은 것이 없다.
Incremental GC #
- 부하가 있는 GC를 점진적으로 수행하는 방법이다. 즉, 하나의 작업을 여러 프레임 동안 나눠서 하는 것이다.
- GC에 드는 시간 총량이 줄어들지는 않지만 워크로드 분산으로 GC Spike 문제점을 개선시킬 수 있다.
가비지 컬렉션과 string #
string
문자열은 변경할 수 없는 객체이다.- 그래서 변경하면 내부적으로 항상 새로운 문자열이 만들어진다.
// string + string의 경우
string str = "one";
str += "two"; // 새로운 객체를 만들고, str이 이것을 참조하도록 한다.
- 대안 :
StringBuilder
클래스를 사용해보자.string
과 달리 변경 가능한 객체이다.StringBuilder
클래스는 연결될 새 데이터를 수용할 버퍼를 유지한다. 만약 용량을 초과하면 새 공간이 자동으로 할당되고 용량이 두 배로 증가한다.
// StringBuilder의 경우
StringBuilder sb = new StringBuilder("one");
sb.Append("two");