GC 관련 언어 기능
가비지 컬렉션의 주요 용도는 무한한 양의 균일한 메모리를 할당에 사용할 수 있다는 단순한 추상화를 지원하는 것입니다. 따라서 객체를 마음대로 생성할 수 있고 개념적으로 “영원히 살 수 있습니다”. 그러나 때로는 이러한 관점을 변경하는 것이 바람직할 수 있습니다. 참조된 객체가 회수되는 것을 방지하지 않는 포인터를 갖거나, 객체가 회수될 때 특별한 루틴을 트리거하는 것이 바람직한 경우가 있습니다. 또한 여러 개의 힙을 두어, 서로 다른 힙에 할당된 객체를 다르게 처리하는 것이 바람직할 수도 있습니다.
약한 포인터(Weak Pointers)
가비지 컬렉터 추상화의 간단한 확장은 프로그램이 객체에 대한 포인터를 보유하면서도 그 포인터가 객체의 수집을 방해하지 않도록 하는 것입니다. 객체가 수집되는 것을 막지 않는 포인터를 약한 포인터라고 하며, 다양한 상황에서 유용합니다.
일반적인 응용 사례 중 하나는 특정 종류의 모든 객체를 열거할 수 있게 해주는 테이블을 유지 관리하는 것입니다. 예를 들어, 시스템의 모든 파일 객체 테이블을 만들어 장애 허용을 위해 주기적으로 버퍼를 플러시하는 것이 바람직할 수 있습니다. 또 다른 일반적인 응용 사례는 객체에 대한 보조 정보 컬렉션을 유지 관리하는 것인데, 이 정보 자체만으로는 쓸모가 없으며 설명된 객체를 살아있게 유지해서는 안 됩니다. (예: 객체에 대한 속성 테이블 및 문서 문자열 - 설명의 유용성은 설명된 객체가 다른 방식으로 흥미로운지에 달려 있지, 그 반대는 아닙니다.)
약한 포인터는 일반적으로 가비지 컬렉터가 알고 있는 특수 데이터 구조를 사용하여 구현되며, 약한 포인터 필드의 위치를 기록합니다. 가비지 컬렉터는 먼저 시스템의 다른 모든 포인터를 순회하여 정상적인 경로를 통해 도달 가능한 객체를 결정합니다. 그런 다음 약한 포인터를 순회합니다. 참조 대상이 정상적인 경로로 도달한 경우, 약한 포인터는 정상적으로 처리됩니다. (복사 수집기에서는 객체의 새 위치를 반영하도록 업데이트됩니다.) 그러나 참조 대상이 도달하지 않은 경우, 약한 포인터는 특별히 처리되며, 일반적으로 참조 대상이 더 이상 존재하지 않음을 알리기 위해 비포인터 값(null 등)으로 대체됩니다.
종료화(Finalisation)
약한 포인터 개념과 밀접하게 관련된 것이 종료화 개념, 즉 객체가 회수될 때 자동으로 수행되는 작업입니다. 이는 객체가 힙 메모리가 아닌 다른 리소스(예: 파일 또는 네트워크 연결)를 관리할 때 특히 일반적입니다. 예를 들어, 해당 힙 객체가 회수될 때 파일을 닫는 것이 중요할 수 있습니다. 주석 및 문서의 예에서는 객체 자체가 회수되면 객체의 설명을 삭제하는 것이 바람직한 경우가 많습니다.
따라서 종료화는 가비지 컬렉터를 일반화하여 다른 리소스가 힙 메모리와 거의 동일한 방식으로, 그리고 유사한 프로그램 구조로 관리될 수 있도록 합니다. 이를 통해 특정 종류의 객체를 “정상” 객체와 매우 다르게 처리하는 대신 더 일반적이고 재사용 가능한 코드를 작성할 수 있습니다. (리스트를 반복하면서 각 항목에 임의의 함수를 적용하는 루틴을 생각해 보세요. 파일 디스크립터가 가비지 수집되는 경우, 힙 객체 리스트에 사용되는 것과 동일한 반복 루틴을 파일 디스크립터 리스트에도 사용할 수 있습니다. 리스트가 도달 불가능하게 되면 가비지 컬렉터가 리스트 구조 자체와 함께 파일 디스크립터를 회수합니다.)
종료화는 일반적으로 종료화 가능한 객체를 어떤 방식으로 표시하고 약한 포인터에 사용되는 것과 유사한 데이터 구조에 등록하여 구현됩니다. (실제로 동일한 데이터 구조를 사용할 수 있습니다.) 그러나 객체가 1차 순회에서 도달하지 않은 경우 포인터를 단순히 null로 만드는 대신, 수집이 완료된 후 특별한 처리를 위해 포인터가 기록됩니다. 수집이 완료되고 힙이 다시 일관성을 갖게 되면, 참조된 객체의 종료화 작업이 호출됩니다.
종료화는 다양한 상황에서 유용하지만 주의해서 사용해야 합니다. 종료화는 비동기적으로 발생하기 때문에(즉, 컬렉터가 객체가 도달 불가능하다는 것을 알아차리고 조치를 취할 때마다), 경쟁 조건 및 기타 미묘한 버그를 만들 수 있습니다.
서로 다르게 관리되는 다중 힙
일부 시스템에서는 편의를 위해 가비지 수집 힙이 제공되고, 메모리 사용을 매우 정밀하게 제어할 수 있도록 별도의 명시적 관리 힙이 제공됩니다.
Modula-3 및 확장된 버전의 C++과 같은 일부 언어에서는 가비지 수집 힙이 명시적으로 관리되는 힙과 공존합니다. 이는 가비지 컬렉션을 지원하면서도 프로그래머가 최대 성능이나 예측 가능성을 위해 일부 객체의 할당 해제를 명시적으로 제어할 수 있게 합니다.
대규모 영속적 또는 분산 공유 메모리와 같은 다른 시스템에서는 분산(예: 공유 vs. 비공유), 액세스 권한 및 리소스 관리에 대한 다양한 정책을 가진 여러 힙을 두는 것이 바람직할 수도 있습니다.
많은 언어 구현이 내부적으로 이러한 기능을 가지고 있었지만, 일반 프로그래머에게는 다소 숨겨져 있었으며, 이에 대한 공개된 정보가 거의 없고 프로그래머 인터페이스의 표준화도 거의 없습니다. (용어도 표준화되어 있지 않으며, 이러한 힙은 “heaps”, “zones”, “areas”, “arenas”, “segments”, “pools”, “regions” 등 다양하게 불립니다.)
OCaml/Lisp과 관련된 흥미로운 역사적 사실
1. Lisp과 가비지 컬렉션의 탄생
Lisp(1958)은 가비지 컬렉션을 최초로 구현한 프로그래밍 언어 중 하나입니다. John McCarthy가 Lisp을 설계할 때, 초기에는 수동 메모리 관리를 염두에 두었지만, 실제 구현 과정에서 그의 대학원생들(특히 Daniel Edwards와 Timothy Hart)이 mark-and-sweep 가비지 컬렉터를 개발했습니다. 이는 프로그래밍 언어 역사에서 획기적인 순간이었으며, 이후 모든 고수준 언어의 메모리 관리에 영향을 미쳤습니다. 흥미롭게도 McCarthy는 가비지 컬렉션이 이론적으로 가능하다는 것을 알고 있었지만, 실제 구현은 “나중에 할 일”로 생각했습니다.
2. OCaml의 세대별 가비지 컬렉터
OCaml은 효율적인 세대별(generational) 가비지 컬렉터를 사용하는데, 이는 힙을 “minor heap”과 “major heap”으로 나눕니다. Minor heap은 작고 자주 수집되며, 대부분의 객체가 짧은 수명을 갖는다는 “세대별 가설(generational hypothesis)”을 활용합니다. 이 설계는 OCaml이 함수형 프로그래밍 언어임에도 불구하고 실시간 애플리케이션에서도 사용될 수 있을 만큼 예측 가능한 성능을 제공하는 핵심 이유입니다. OCaml의 GC는 stop-and-copy와 incremental mark-and-sweep을 조합하여, 긴 pause time을 피합니다.
3. Weak Pointers와 Ephemeron
Lisp과 OCaml 모두 약한 참조(weak reference)를 지원하지만, OCaml은 더 정교한 개념인 “ephemeron”을 도입했습니다. Ephemeron은 키-값 쌍으로, 키가 강하게(strongly) 도달 가능한 경우에만 값이 유지됩니다. 이는 단순한 약한 포인터보다 더 정밀한 제어를 제공하며, 캐시 구현이나 메모이제이션(memoization)에 특히 유용합니다. Common Lisp의 weak-pointer와 weak-hash-table 개념이 오랜 역사를 가지고 있지만, ephemeron은 이를 더욱 발전시킨 형태로 볼 수 있습니다.