보수적 GC란
C나 C++을 설명할 때 언급되지만, 실제로는 Unity에서 쓰는 보수적(conservative) Garbage Collector 방식이다.
보수적이라는 말은?
일반 GC는 컴파일러가 "이 스택 슬롯은 포인터고 저건 정수다"라는 타입 정보를 넘겨준다. 하지만 C에는 그런 정보가 없다. 그래서 BDWGC는 스택, 레지스터, 전역 데이터 영역, 힙 객체 내부를 워드 단위로 훑으면서 힙 범위 안의 유효한 주소처럼 보이는 값은 모두 포인터로 간주한다.
유효한 주소와 포인터?
힙은 원래 새로 생긴 객체 등을 담는 곳이다. 여기서 포인터는 해당 객체 안에 들어 있는 정보다.
struct Node {
int value; // 4바이트
struct Node *next; // 8바이트
};GC_MALLOC(sizeof(struct Node))로 만든 객체는 힙에서 16바이트짜리 덩어리다.
이 내부 16바이트는 실제로 아래와 같이 구성돼 있다.
주소 0x600100 에 있는 Node 객체 (16바이트)
┌────────────────────────┬────────────────────────┐
│ 00 00 00 2A (+패딩) │ 00 00 00 00 00 60 01 80│
└────────────────────────┴────────────────────────┘
value = 42 next = 0x600180
즉 포인터는 주소를 담고 있는 값(워드)이고, 그 값은 객체 안에 앉아 있다.
그러면 이제 객체가 노드가 되고 포인터가 간선이 된다.
[스택] [힙]
head ─────────────────▶ Node@600100
├ value: 42 ← 숫자, 무시
└ next: 0x600180 ─┐
▼
Node@600180
├ value: 7
└ next: 0x000000 (NULL)
- 스택/레지스터/전역 영역을 워드 단위로 훑는다. 여긴 힙이 아니라 루트다. 여기서
0x600100이라는 값을 발견한다. - "힙 범위 안이고 객체 시작점이네" →
Node@600100마크. - 마크한 객체 내부의 16바이트를 다시 워드 단위로 훑는다. 42는 힙 범위 밖이라 무시하고,
0x600180은 유효한 객체 주소다. Node@600180마크.
여기서 힙은 스캔 대상이며 동시에 가리켜지는 대상이다.
여기서 문제는
- 스택 슬롯에 남은 옛 포인터, interior pointer, 그리고 그 하나가 하위 그래프 전체를 붙잡는 상황이다.
- 또한
0x600100을0x700000으로 이동시킨다면 해당 객체를 가리키고 있던 모든 포인터를 옮겨야 한다.- 여기서 대칭이 깨진다.
struct Weird {
long n; // 값: 0x600100 (진짜 정수)
struct Foo *p; // 값: 0x600100 (진짜 포인터)
};이면 마킹 시 이 두 값을 모두 포인터 취급한다. 그러면 두 값이 모두 바뀌게 되고 데이터가 조용히 손상된다.
엥 근데 그냥 애초에 저 객체를 보면 되지 않나?
할 수 있다. 하지만 이건 일반 GC들이 하는 것이고, 여기서 소개하는 GC는 이 작업을 해내지 못한다.
그렇기 때문에
BDWGC는 의심 가는 모든 힙 주소를 포인터로 친다. 그렇기 때문에 가비지임에도 못 버리는 경우가 많다.
시리즈에서 이어 읽기 · 파이썬 내부 파헤치기 →