용어정의
1. Object: 힙의 저장소 단위. 일반적으로는 OOP의 객체를 의미하지만 절차 언어, 또는 함수 언어에도 동일하게 적용된다.
2. Object / Reference graph: 메모리에서 object들의 방향을 나타내는 그래프. 다음 그림이 예이다. node는 메모리에 있는 object이고 edges(arrows)는 한 object가 다른 object의 참조를 들고 있음을 표현하고 있다. object3 , object5 그리고 object6 에서 순환참조가 발생한다.
3. Roots: reference graph에서 시작점의 집합을 뜻한다. roots는 스택에서의 로컬 변수, 전역 변수가 될 수 있다. 위의 예시에서는 녹색 node가 roots에 속한다.
4. Unreachable object: 자신을 참조하는 edge가 없는 node. GC가 어떤 node에서도 접근할 수 없으므로 garbage가 된다.
Mark-and-Sweep GC
이 알고리즘에서 메모리는 garbage가 되는 순간에 해제가 되지 않는다고 한다. 다른 시스템들이 돌고 있는 동안에 메모리를 항상 추적하지는 않는다.
보통의 경우 메모리가 부족한 시점부터 시작을 하게 되고( 혹은 다른 발생 시점을 정의하면 실행한다 )
모든 root를 순환, 재귀로 참조되고 있는 object를 검사한다. 검사(Mark) 단계에서 object에 접근했다는 뜻은 해당 object에 도달할 수 있음을 뜻하게 되고 특정한 플래그로 표시 해둔다. 검사 단계가 끝나고 나면 휩쓸기(Sweep) 단계가 시작되는데 검사 단계에서 표시 되지 못한 object들을 garbage로 판단하고 시스템에서 처리한다.
The algorithm
슈도코드
Root Enumeration
void GC()
{
HaltAllProcessing();
ObjectCollection roots = GetRoots();
Mark(roots[i]);
Sweep();
}
|
Mark
object들이 자신의 참조를 재귀하면서 도달할 수 있음을 표시해 나간다.
void Mark(Object* pObj)
{
if (!Marked(pObj)) // Marked returns the marked flag from object header
{
MarkBit(pObj); // Marks the flag in obj header
// Get list of references that the current object has
// and recursively mark them as well
ObjectCollection children = pObj->GetChildren();
{
Mark(children[i]); // recursively call mark
}
}
}
|
Sweep
스윕은 전체 메모리를 반복하여 시작하고 표시되지 않은 메모리 블록을 해제한다. 또한 Mark 표시를 지워서 후속 GC가 올바르게 표시 / 해제 할 수 있도록 한다. (메모리를 사전 표시 상태로 재설정).
void Sweep()
{
Object *pHeap = pHeapStart;
while(pHeap < pHeapEnd)
{
if (!Marked(pHeap))
Free(pHeap); // put it to the free object list
else
UnMarkBit(pHeap);
pHeap = GetNext(pHeap);
}
}
|
이 후에 메모리 단편화 문제를 해결하기 위해서 Compaction 과정이 추가로 진행된다.
메모리의 할당과 mark-and-sweep이 반복적으로 일어날 경우, 메모리 단편화가 발생하고 이는 할당 속도를 저해하는 원인이 된다. 해서 실제로 많은 시스템들에서 Sweep과 Compaction을 조합하여 후속적인 할당 요청이 속도가 줄어들지 않도록 의도하지만, 메모리가 압축되는 그 시점에서 object들의 주소가 변경되게 되고 이를 가리키는 주소를 모두 업데이트 해주어야 하기 때문에 매우 무거운 작업이 된다.
GC 실행시점?
간단하게는 언뜻 메모리가 할당 실패하는 경우에 이를 실행하게 되면 될듯 하지만, 가비지를 수집하는 과정에서도 메모리가 필요하므로 가비지 수집 과정 자체가 실패할 수 있다. 해서 실제적으로는 다양한 시점에 가비지 수집을 실행하게 된다.
1. 시스템의 메모리가 부족 ( GC를 실행하는데는 충분하나 곧 할당이 실패되는 시점 )
2. 메모리 단편화가 심해졌을 때 ( Compaction )
3. 매량의 메모리를 할당했을 때 ( GC 이후에 1MB를 할당시 GC를 실행 함)
장단점 정리
+ 순환 참조를 매우 자연스럽게 해결함
+ GC가 실행 되지 않았을 때에 추가적인 오버헤드가 없다.
+ Compaction 과정 추가로 단편화와 추가 메모리 요청을 최적화 한다.
- 모든 스레드를 정지시키기 때문에 아주 무거운 방법임 ( 대화형 프로그램에서 사용할 때 고려할 사항이 많아짐 )
- 메모리가 적을 때에 실행 되기 때문에, 메모리가 고갈 될 수록 반복적으로 GC를 수행하게 된다.
'DEV > Unity C#' 카테고리의 다른 글
[C#] structs and Interface (0) | 2019.06.01 |
---|---|
[C#] 액세스 한정자 (Access Modifiers) (0) | 2019.05.12 |
인터페이스 IEnumerator 와 IEnumerable의 차이 (0) | 2019.05.09 |
[C#] how to convert string[] to int[]? 배열 캐스트 변환에 대하여 (0) | 2019.05.02 |