Реализация сборки мусора на С++

Автор: Михаил Чащин
Источник: RSDN Magazine #1
Опубликовано: 18.11.2002
Версия текста: 1.0
Анализ
Дизайн
Пример реализации сборщика мусора
Пример использования GC
Выводы

Демонстрационный проект

В данной статье мы рассмотрим обобщённую реализацию сборки мусора на С++. Будут обсуждены два конкретных алгоритма сборки мусора – “Mark-Sweep” и “Mark-Compact”, и их реализация. Мы также рассмотрим ограничения, которые накладываются на приложения при использовании сборки мусора, и изменения в компиляторе C++, которые могли бы помочь избежать этих ограничений.

Сборка мусора, или Garbage Collection (сокращённо GC), представляет собой процесс утилизации памяти для повторного её использования. В задачу сборки мусора входит поиск всех объектов, которые более не используются системой, и их удаление с целью повторного использования памяти работающим приложением. Объекты приложения рассматриваются как мусор, если они ни прямо, ни косвенно не доступны работающей программе.

Использование сборки мусора, как одного из вариантов автоматического управления памятью, многими разработчиками в наши дни рассматривается как необходимый шаг к обеспечению надёжности программы, поскольку в этом случае код для работы с памятью собран в одном чётко определённом модуле, а не разбросан в разных частях приложения. В этом случае с одной стороны мы получаем целый ряд преимуществ, в числе которых решение проблемы повисших указателей (dangling pointers) и утечек памяти (memory leaks), но, с другой стороны, мы теряем гибкость, поскольку сборка мусора накладывает некоторые ограничения на разработку приложения.

Если сборка мусора не встроена в язык программирования, её очень тяжело использовать. Кроме этого, возможны потери как в скорости, так и в памяти. Поэтому общепризнанным является тот факт, что самого эффективного автоматического управления памятью в приложении можно добиться только в том случае, если это управление поддерживается самим языком программирования. К сожалению, сегодня стандартный C++ эту возможность не поддерживает. Поэтому программистам, желающим использовать сборку мусора, приходится реализовывать тот или иной алгоритм сборки мусора вручную. Самым распространённым вариантом является так называемый подсчёт ссылок. Правда, этот алгоритм содержит крупный недостаток – проблему утилизации цикличных графов.

Надо отметить, что автоматическая сборка мусора не включена в стандарт C++ по той простой причине, что программа, её использующая, будет всегда работать медленнее, чем если бы сборка мусора не использовалась вообще. Поэтому Бьёрном Страуструпом было предложено перепоручить обязанности сборки мусора внешним библиотекам, не затрагивая самого C++, что может позитивно сказаться на производительности приложений, поскольку программист сам может решить, где и когда ему стоит использовать автоматическое управление памятью. Это и является серьёзным отличием С++ от Java – при использовании Java у программистов просто нет выбора.

Сборка мусора логически состоит из двух частей:

  1. Поиск мусора – процесс поиска недостуных для приложения объектов;
  2. Удаление мусора – процесс утилизации памяти для дальнейшего её использования.

Обычно две эти части непосредственно связаны друг с другом и выполняются одновременно. Так, например, при использовании подсчёта ссылок, удаление объекта происходит сразу же, как только он становится недоступным. Для более сложных систем сборки мусора характерно чёткое разделение на начальный поиск всех недоступных объектов и последующее их удаление.

Таким образом, сборка мусора использует критерий доступности объекта при своей работе. Доступные объекты, в свою очередь, условно можно разделить на два вида: объекты, непосредственно доступные программе, и объекты, доступные косвенно, то есть доступные только через указатели внутри других объектов. Объекты первого вида часто называют периметром, а второго – объектами внутри периметра. Поэтому мы будем говорить, что всё, что находится за пределами периметра, недоступно программе и, следовательно, подлежит удалению.

В дальнейшем мы будем называть объекты, с которыми работает сборка мусора, управляемыми (managed) объектах, в отличие от неуправляемых (unmanaged) объектов, которые не подвержены процессу сборки мусора.

Анализ

Для начала разберемся, как найти периметр. Если объект на периметре непосредственно доступен в приложении, то у нас есть по крайней мере один указатель на этот объект. Если же таких указателей нет, объект не находится на периметре.

Воспользуемся подсчётом ссылок для определения числа указателей на объект. Если счётчик ссылок больше нуля, то объект находится на периметре, если счётчик равен нулю, объект находится либо внутри периметра, либо за его пределами. Дабы избежать проблемы зацикливания, подсчёт ссылок необходимо осуществлять по следующей схеме: подсчитываются только те указатели, которые объявлены глобально в данной точке программы, те же указатели, которые содержатся в других объектах, не будут подсчитываться.

Следующий вопрос заключается в поиске объектов внутри периметра, то есть таких объектов, которые доступны косвенно через объекты на периметре. Запрашивая каждый объект на периметре, чтобы он перечислил все свои внутренние указатели на объекты, мы получим часть объектов внутри периметра (первый уровень). Если, в свою очередь, их также опросить обо всех объектах, на которые они имеют ссылки, мы получим ещё один набор объектов внутри периметра. Продолжая так до тех пор, пока существуют косвенные ссылки, мы найдём все объекты внутри периметра. Легко заметить, что объекты внутри периметра образуют граф, который может содержать зацикливающиеся ссылки. Поэтому, во избежание зацикливания, необходимо каким-то образом отличать уже опрошенные объекты от не опрошенных. Это можно сделать либо создав список опрошенных объектов, либо добавлением в объекты специального флага. Мы воспользуемся вторым способом. Установка этого флага будет говорить о том, что объект находится внутри периметра, и что он уже был опрошен о внутренних ссылках. Если мы установим этот флаг и у всех объектов на периметре, то поиск мусора, или объектов вне периметра, сведётся к простой проверке объектов на сброшенность флага. Последнее утверждение подразумевает, что у нас есть возможность перебирать все объекты.

Перебор объектов – задача несложная, если упорядочить их создание. Но, если мы также планируем производить дефрагментацию памяти, то есть планируется перемещать объекты, то одного организованного создания объектов будет недостаточно. Придётся также обновлять все указатели на переместившиеся объекты, а это означает, что и указатели должны создаваться упорядоченно. Чтобы упростить архитектуру сборки мусора, мы воспользуемся следующим приёмом: для каждого объекта будем использовать всего лишь один указатель, который будет обновляться каждый раз, когда объект перемещается. В то же время все указатели в приложении будут ссылаться не непосредственно на объект, а на его указатель. Это даёт нам возможность, с одной стороны, безболезненно производить дефрагментацию памяти, поскольку для каждого объекта существует только один указатель, который надо обновить, а с другой стороны, мы сможем инкапсулировать процесс подсчёта ссылок и создания объектов, если использовать идиому smart-указателей.

Итак, мы имеем следующее решение: все объекты создаются упорядоченно, чтобы их можно было перемещать, уменьшая тем самым фрагментацию памяти. Для каждого объекта существует ровно один smart-указатель, который инкапсулирует создание объекта, подсчёт ссылок, а также включает флаг, необходимый для определения принадлежности объекта к периметру. Все указатели на smart-указатели могут создаваться как угодно, но как только они создаются, они должны увеличивать счётчик ссылок smart-указателя, а при уничтожении соответственно его уменьшать. Поэтому такие указатели лучше всего также сделать smart-указателями (в дальнейшем мы будем называть их дескрипторами).

Все дескрипторы должны делится на два вида, чтобы избежать проблемы зацикливания при использовании механизма подсчёта ссылок. Первый вид – сильные дескрипторы – подсчитывают ссылки, и при своём создании создают smart-указатели, которые в свою очередь создают объекты, второй вид – слабые дескрипторы – не подсчитывают ссылки и используются только как указатели на объекты внутри других объектов. Слабые дескрипторы не создают smart-указателей и в общем случае ведут себя абсолютно так же как и обычные указатели.

Дизайн

Посмотрим на обобщённый дизайн (generic design) будущей системы. Начнём со smart-указателей. Как уже упоминалось выше, в их обязанность входит подсчёт ссылок, который может быть реализован по-разному, и в нашем случае не влияет на остальную функциональность класса smart-указателей. Поэтому представляется хорошей идеей вынести подсчёт ссылок в отдельный класс. Использование функциональности такого класса в классе smart-указателей возможно несколькими способами. Мы воспользуемся наследованием. Класс подсчёта ссылок должен реализовывать следующие функции:

Примером класса с таким интерфейсом может быть следующий шаблон:

Листинг 1. Шаблон класса для подсчёта ссылок.
template <typename T>
class UnsafeRefCounter
{
public:
  UnsafeRefCounter() : d_refcount(T(0)){}
  void AddRef(){++d_refcount;}
  void Release(){--d_refcount;}
  bool active() const{return d_refcount != T(0);}
private:
  T d_refcount;
};

Здесь параметр T шаблона – это тип, который выбирается программистом для переменной, хранящей количество ссылок. Например, если на один объект может существовать огромное количество указателей, то в шаблон лучше всего передать тип unsigned long. Если же нет, то может подойти и unsigned char.

Чтобы иметь возможность перечислять объекты периметра, необходимо уметь перечислять smart-указатели, у которых счётчик ссылок отличен от нуля. Поэтому нужно контролировать процесс создания smart-указателей. Эту функциональность мы также можем вынести в отдельный класс (назовём его SPAllocator), от которого в дальнейшем унаследуем класс smart-указателей. Ниже приведён список функций, которые образуют интерфейс для создания и перечисления smart-указателей:

Класс SPIterator – это вложенный в SPAllocator открытый класс. Прототип шаблона класса SPAllocator приведён в листинге 2.

Листинг 2. Прототип шаблона класса SPAllocator для smart-указателей.
template <class T> 
class SPAllocator
{
public:
  class SPIterator
  {
  public:
    void operator++();
// здесь T – это класс smart-указателя
    T* operator()();
    operator const void*() const;
  }
  static void* operator new(size_t);
  static void operator delete(void*);
  static SPIterator iterator();
};

При создании smart-указателя происходит непосредственное создание объекта. Удаление smart-указателя влечёт за собой удаление объекта. Кроме процедур создания и удаления может понадобиться процедура уплотнения памяти. Поэтому класс для управления жизнью объектов (назовём его ObjectAllocator) должен предоставлять следующие три функции:

Функция Allocate может автоматически вызывать сборку мусора при нехватке памяти, чтобы освободить хотя бы часть ресурсов. В то же время функция Compact должна передвигать объекты в памяти, и, следовательно, должна знать о smart-указателях. Поэтому класс ObjectAllocator – это шаблон, в качестве параметров которого выступают класс сборки мусора и класс smart-указателей (см. ниже). Прототип класса приведён в листинге 3.

Листинг 3. Прототип шаблона класса ObjectAllocator для smart-указателей.
template <class T, template <class> class GC> 
class ObjectAllocator
{
public:
  static void* Allocate(size_t);
  static void Deallocate(void*);
  static void Compact();
};

Итак, после разделения функциональности класса smart-указателя на непересекающиеся части, мы получаем обобщённый шаблон класса smart-указателя:

Листинг 4. Прототип smart-указателя.
template <class T, class R, 
          template <class> class S, 
          template <class> class GC,
          template <class, template <class> class> class W>
class SP : public R, public S<T>{…};

где T – тип создаваемого объекта, R – класс, инкапсулирующий подсчёт ссылок (например, UnsafeRefCounter из листинга 1), S – класс-шаблон, отвечающий за создание и перечисление smart-указателей (прототип такого класса – SPAllocator – приведён в листинге 2), GC – класс сборки мусора (см. ниже), W – класс, инкапсулирующий создание объекта (прототип такого класса – ObjectAllocator – приведён в листинге 3).

Чтобы иметь возможность перебирать smart-указатели, нам необходимо абстрагироваться от типов объектов, на которые они указывают, но в то же время иметь к ним доступ. Вынося общую для всех smart-указателей функциональность в родительский класс, мы получаем решение, описанное в листинге 5:

Листинг 5. Шаблон родительского класса smart-указателей.
template <class class T, class R, 
          template <class> class S, 
          template <class> class GC,
          template <class, template <class> class> class W>
class AbstractSP : public R, public S<AbstractSP>
{
public:
  typedef S<AbstractSP> SPAllocator;
  typedef W<AbstractSP> ObjectAllocator;

  AbstractSP(void* object, size_t size) 
     : d_object(object), d_size(size), d_active(true) {}
  virtual ~AbstractSP()
  {
    d_object = 0;
    d_size = 0;
  }
  const void* const getObject() const
  {
    return d_object;
  }
  void setActiveFlag(bool value)
  {
    d_active = value;
  }
  bool getActiveFlag() const 
  {
    return d_active;
  }
private:
  AbstractSP() : d_object(0), d_size(0), d_active(false) {}
protected:
  void* d_object;  // адрес объекта
private:
  size_t d_size;  // размер объекта
  bool d_active;  // флаг принадлежности периметру или его внутренности
};

Листинг 6. Шаблон класса smart-указателей.
template <class class T, class R, 
          template <class> class S, 
          template <class> class GC,
          template <class, template <class> class> class W>
class SP : AbstractSP<R, S, GC, W>
{
public:
  SP() : AbstractSP<R, S, GC, W>
         (new (W<AbstractSP<R, S, GC, W> >::Allocate(sizeof(T))) T, 
          sizeof(T)){}
  T* operator->()
  {
    return static_cast<T*>(d_object);
  }
  ~SP()
  {
    static_cast<T*>(d_object)->~T(); // явный вызов деструктора объекта
    W<AbstractSP<R, S, GC, W> >::Deallocate(d_object); 
  }
private:
  SP(const SP&);
  SP& operator=(const SP&);	
};

При создании smart-указателя автоматически выделяется память для объекта. Это делается вызовом функции Allocate. Полученный адрес памяти передаётся в оператор new для конструирования объекта. Родительский класс хранит адрес и размер созданного объекта и, кроме этого, содержит флаг, необходимый для определения доступности объекта. Доступ к объекту осуществляется через перегруженный оператор «->». При удалении smart-указателя происходит вызов деструктора объекта, а затем высвобождение памяти с помощью вызова функции Deallocate.

Теперь перейдём к дескрипторам. Как уже говорилось выше, дескрипторы – это ещё один вид smart-указателей. Сильные дескрипторы при своём создании создают smart-указатели и принимают участие в подсчёте ссылок. Слабые дескрипторы smart-указателей не создают, а являются лишь контейнерами для их хранения, и не принимают участия в подсчёте ссылок. Прототипы сильных и слабых дескрипторов приведены в листингах 7 и 8.

Листинг 7. Прототип шаблона класса сильного дескриптора.
template <class class T, class R, 
          template <class> class S, 
          template <class> class GC,
          template <class, template <class> class> class W>
class SH
{
public:
  // при создании сильного дескриптора создаём smart-указатель
  SH() : d_sp(new SP<T, R, S, GC, W>)
  {
    d_sp->AddRef(); 
  }
  // копирование дескрипторов
  SH(const SH& handle) : d_sp(handle.d_sp) 
  {
    d_sp->AddRef();
  }
  // присваивание дескрипторов
  SH& operator=(const SH& handle)  
  {
    if (d_sp != handle.d_sp)
    {
      d_sp->Release();
      handle.d_sp->AddRef();	
      d_sp = handle.d_sp;
    }
    return *this;
  }
  SP<T, R, S, GC, W>& operator->()
  {
    return *static_cast<SP<T, R, S, GC, W>*>(d_sp);
  }
  // при удалении сильного дескриптора уменьшаем счётчик ссылок на объект
  ~SH()
  {
    d_sp->Release();
  }
private:
  AbstractSP<R, S, GC, W>* d_sp;
};

Листинг 8. Прототип шаблон класса слабого дескриптора.
template <class class T, class R, 
          template <class> class S, 
          template <class> class GC,
          template <class, template <class> class> class W>
class WH
{
public:
  // при создании слабого дескриптора smart-указатель не создаётся
  WH() : d_sp(0){}
  SP<T, R, S, GC, W>& operator->()
  {
    return *static_cast<SP<T, R, S, GC, W>*>(d_sp); 
  }
private:
  AbstractSP<R, S, W>* d_sp;
};

Здесь параметры шаблона точно такие же, как и параметры шаблонов smart-указателей.

Надо отметить, что полная реализация дескрипторов включает поддержку инициализации слабых дескрипторов сильными, и наоборот. Поддерживается также присваивание между дескрипторами. И, кроме этого, существует возможность преобразования и присваивания дескрипторов одного типа объектов к дескриптору другого типа объектов, при условии, что один тип можно привести к другому. Приводимость типов определяется во время компиляции.

Для определения принадлежности объекта периметру мы воспользуемся идеей общего базового класса. То есть, все управляемые объекты должны будут наследоваться от одного и того же родительского класса, предоставляя функциональность, необходимую для использования их в сборке мусора. Каждый объект должен знать все объекты, на которые он ссылается, и предоставлять интерфейс для перебора этих объектов. Интерфейс перебора (предоставляющий список) реализован в виде абстрактного класса-шаблона итератора, предназначенного для использования в цикле for. Этот шаблон и шаблон родительского класса представлены в листингах 9 и 10, сответственно.

Листинг 9. Шаблон абстрактного класса итератора.
template <class T>
class Iterator
{
public:
  virtual void operator++() = 0;            // переход к следующему элементу
  virtual T* operator()() = 0;              // получение текущего элемента
  virtual operator const void*() const = 0; // проверка конца списка
  virtual ~Iterator(){}
};

Листинг 10. Шаблон родительского класса всех управляемых классов приложения.
template <class T>
class GCObject
{
public:
  virtual Iterator<T>* iterator() = 0;
  virtual ~GCObject(){}
};

В качестве параметра шаблона GCObject передаётся класс AbstractSP.

Осталось посмотреть, как устроен сам сборщик мусора. Это класс, который ищет мусор и удаляет его, а затем дефрагментирует память. Класс-сборщик мусора предоставляет только одну функцию static void Collect().

Сам класс – это тоже шаблон, который принимает в качестве параметра AbstractSP. Он пользуется типами, определёнными внутри него, для доступа к итератору smart-указателей (тип SPAllocator) и функции дефрагментации памяти (тип ObjectAllocator).

Функция Collect выполняет следующие шаги:

  1. Помечает все объекты, не находящиеся на периметре, как недоступные.
  2. Ищет объекты, находящиеся внутри периметра, и помечает их как доступные.
  3. Удаляет все недоступные объекты.
  4. Вызывает функцию дефрагментации памяти.

Готовый код класса сборщика мусора приведён в листинге 11.

Листинг 11. Шаблон класса сборщика мусора.
template <class T>
class GC
{
public:
  static void Collect()
  {
    T::SPAllocator::SPIterator i;

    // помечаем объекты периметра как доступные,
    // все остальные – как недоступные
    for (i = T::SPAllocator::iterator(); i; ++i) 
    {
      T* p = i();
      // Функция active возвращает true, если объект находиться на периметре.
      // Функция setActiveFlag помечает объект как уже проверенный 
      // на принадлежность периметру или его внутренности.
      p->setActiveFlag(p->active());
    }

    // определяем объекты внутри периметра
    for (i = T::SPAllocator::iterator(); i; ++i)
    {
      T* p = i();
      if (p->getActiveFlag())
        MarkActive(p);
    }

    // удаляем мусор
    for (i = T::SPAllocator::iterator(); i; ++i)
    {
      T* p = i();
      if (!p->getActiveFlag())
        delete p;
    }

    // дефрагментируем память
    T::ObjectAllocator::Compact();
  }
private:

  // функция поиска объектов внутри периметра
  static void MarkActive(T* p)
  {
    Iterator<T>* i;
    // запрашиваем каждый управляемый объект о его внутренних ссылках
    for (i = static_cast<const GCObject<T>*>(p->getObject())->iterator(); 
         (*i); 
         ++i)
    {
      // получаем внутренний указатель на объект
      T* p2 = const_cast<T*>((*i)()); 
      // если этот объект ещё не отмечен как доступный...
      if (!p2->getActiveFlag())   
      {
        // помечаем его как доступный
        p2->setActiveFlag(true);
        // и проводим поиск объектов, доступных из данного
        MarkActive(p2);
      }
    }
    delete i;
  }
};

Пример реализации сборщика мусора

Рассмотрим пример реализации класса, управляющего созданием, удалением и перебором smart-указателей (листинг 12). Он основан на идее пула (в этом же номере есть статья QuickHeap, довольно подробно разбирающая идею пула). Пул состоит из блоков, размер которых передаётся в качестве параметра шаблону PoolProxy . Этот шаблон является всего лишь оболочкой для класса PoolAllocation, который и выполняет всю работу.

Листинг 12. Реализация пула smart-указателей.
template <unsigned int BlockSize>
class PoolProxy
{
public:
  // шаблон класса-аллокатора smart-указателей
  template <class T>
  class PoolAllocation
  {
  private:
    // Блок организован как однонаправленный связный список
    struct Block
    {
      Block(Block* next) : d_next(next)
      {
        for (unsigned int i = 0; i < BlockSize - 1; ++i)
          d_slots[i].d_object = &d_slots[i + 1];
        d_slots[BlockSize - 1].d_object = 0;
      }
      ~Block()
      {
        if (d_next) 
          delete d_next;
      }
      Block* d_next;
      T d_slots[BlockSize];
    };

    // класс пула
    class Pool
    {
      friend class SPIterator;
      public:
        Pool() : d_block_list(new Block(0)), 
                 d_free_list(&d_block_list->d_slots[0]){}
        ~Pool()
        {
          delete d_block_list;
        }
        // Функция возвращает адрес памяти, 
        // выделенной в пуле для smart-указателя
        T* Allocate()
        {
          // если в пуле нет больше свободной памяти, то создаём новый блок
          if (!d_free_list)
          {
            d_block_list = new Block(d_block_list);
            d_free_list = &d_block_list->d_slots[0];
          }
          T* p = d_free_list;
          d_free_list = static_cast<T*>(p->d_object);
          return p;
        }
        // функция возвращает память в пул для повторного использования
        void Deallocate(T* p)
        {
          p->d_object = d_free_list;
          d_free_list = p;
          p->d_size = 0;
        }
      private:
        Block* d_block_list;
        T* d_free_list;
    };
    static Pool& pool()
    {
      static Pool d_pool = Pool();
      return d_pool;
    }
  public:
    // класс-итератор для перебора smart-указателей внутри пула
    class SPIterator
    {
    public:
      SPIterator() : d_slot(0), d_block(pool().d_block_list){}
      void operator++()
      {
        while (d_block)
        {
          if (d_slot >= BlockSize)
          {
            if (!(d_block = d_block->d_next))
              break;
            d_slot = 0;
          }
          if (d_block->d_slots[d_slot].d_size)
            break;
          d_slot++;
        }
      }
      T* operator()()
      {
        T* p = &d_block->d_slots[d_slot++];
        this->operator++();
        return p;
      }
      operator const void*() const
      {
        return d_block;
      }
    private:
      unsigned int d_slot;
      Block* d_block;
    };
    static void* operator new(size_t)
    {
      return pool().Allocate();
    }
    static void operator delete(void* p)
    {
      pool().Deallocate(static_cast<T*>(p));
    }
    static SPIterator iterator()
    {
      return SPIterator();
    }
  };
};

Теперь рассмотрим реализацию класса, ответственного за управление жизнью объектов. Реализация зависит от выбранного алгоритма. Мы рассмотрим два из них.

Первый носит название “Mark-Sweep” (на русский язык название алгоритма можно перевести как “Пометить-Подмести”). Это алгоритм, который выделяет память при вызове функции Allocate, а затем возвращает её при вызове функции Deallocate. Вызовы этих функций происходят соответственно при создании smart-указателя и при его удалении. Функция Compact в классической реализации этого алгоритма не делает ничего, что является его главным недостатком, поскольку допустима возможность фрагментации памяти. Самый простейший пример реализации класса приведён в листинге 13.

Листинг 13. Класс, управляющий жизнью объектов, для алгоритма "Mark-Sweep"
template <class T, template <class> class GC>
class MarkSweepObjectAllocator
{
public:
  static void* Allocate(size_t size)
  {
    return ::operator new(size);
  }
  static void Deallocate(void* object)
  {
    ::operator delete(object);
  }
  static void Compact(){}
};

Второй алгоритм носит название “Mark-Compact” (дословный перевод – “Пометить-Уплотнить”). Этот алгоритм также выделяет память для объектов в пуле в функции Allocate, но функция Deallocate не возвращает память обратно в пул. Более того, она вообще ничего не делает. Освобождение памяти происходит косвенно при её уплотнении в функции Compact, когда объекты, оставшиеся после сборки мусора, переносятся в начало пула, а вся оставшаяся часть пула считается доступной для дальнейшего использования (листинг 14).

Листинг 14. Шаблон класса, управляющего жизнью объектов, для алгоритма "Mark-Compact".
template <unsigned long SpaceSize>
class MarkCompactObjectAllocator
{
public:
  template <class T, template <class> class GC>
  class ObjectAllocator
  {
  public:
    static void* Allocate(size_t size)
    {
      if (next_byte() + size > SpaceSize)
      {
        // пытаемся собрать мусор, чтобы освободить память,
        // если памяти всё равно недостаточно, генерируем исключение
        GC<T>::Collect();
        if (next_byte() + size > SpaceSize)
          throw;
      }
      void* p = &bytes()[next_byte()];
      next_byte() += size;
      return p;
    }
    static void Deallocate(void*){}
    static void Compact()
    {
      next_byte() = 0;
      T::SPAllocator::SPIterator i;
      for (i = T::SPAllocator::iterator(); i; ++i)
      {
        T* p = i();
        void* space = Allocate(p->d_size);
        if (space < p->d_object)
          p->d_object = memcpy(space, p->d_object, p->d_size);
      }
    }
  private:
    static unsigned long& next_byte()
    {
      static unsigned long d_next_byte = 0;
      return d_next_byte;
    }
    static unsigned char* bytes()
    {
      static unsigned char d_bytes[SpaceSize];
      return d_bytes;
    }
  };
};

Пример использования GC

Рассмотрим пример использования GC. Для начала определим классы управляемых объектов. Все они должны наследоваться от класса GCObject и уметь предоставлять список внутренних ссылок.

Листинг 15. Примеры классов управляемых объектов.
typedef AbstractSP<UnsafeRefCounter<unsigned int>,
                   PoolProxy<1000>::PoolAllocation,
                   GC,
                   MarkSweepObjectAllocator> asp_t;

template <class T>
class A : public GCObject<T>
{
public:
  // класс-итератор, в обязанность которого входит перечисление всех объектов,
  // на которые ссылается класс A
  class AIterator : public Iterator<T>
  {
    public:
      virtual void operator++(){}
      virtual const T* operator()(){return 0;}
      virtual operator const void*() const {return 0;}
  };
  virtual Iterator<T>* iterator() const {return new AIterator;}
  A(){std::cout << "A ctor" << std::endl;}
  ~A(){std::cout << "A dtor" << std::endl;}
  int x;
};

template <class T>
class C : public A<T>
{
public:
  C(){std::cout << "C ctor" << std::endl;}
  ~C(){std::cout << "C dtor" << std::endl;}
};

template <class T>
class B : public GCObject<T>
{
public:
  class BIterator : public Iterator<T>
  {
  public:
    BIterator(const B<T>* b) : d_b(b), d_flag(true) {}
    virtual void operator++(){d_flag = false;}
    virtual const T* operator()()
    {
      return d_b->c.GetSP();
    }
    virtual operator const void*() const {return d_flag ? &d_flag : 0;}
  private:
    const B* d_b;
    bool d_flag;
  };
  virtual Iterator<T>* iterator() const {return new BIterator(this);}
  B(){std::cout << "B ctor" << std::endl;}
  ~B(){std::cout << "B dtor" << std::endl;}

  WH<C<asp_t>, UnsafeRefCounter<unsigned int>, 
     PoolProxy<1000>::PoolAllocation, 
     GC, 
     MarkSweepObjectAllocator> c;
};

Теперь посмотрим, что мы можем делать с управляемыми объектами (Листинг 16).

Листинг 16. Пример использования управляемых объектов.
// определяем типы сильных дескрипторов для классов из листинга 15
typedef SH<A<asp_t>, UnsafeRefCounter<unsigned int>, 
           PoolProxy<1000>::PoolAllocation, GC, 
           MarkSweepObjectAllocator> SHA;
typedef SH<B<asp_t>, UnsafeRefCounter<unsigned int>, 
           PoolProxy<1000>::PoolAllocation, GC, 
           MarkSweepObjectAllocator> SHB;
typedef SH<C<asp_t>, UnsafeRefCounter<unsigned int>, 
           PoolProxy<1000>::PoolAllocation, GC, 
           MarkSweepObjectAllocator> SHC;
void f(SHA a)
{
  std::cout << a->x << std::endl; // выводим значение x объекта класса A
}

int main()
{
  {
    SHC c;        // создаём объект класса C
    c->x = 100;   // присваиваем переменной x значение 100
    SHA a;        // создаём объект класса A
    SHB b;        // создаём объект класса B
    b->c = c;     // инициализируем слабый дескриптор объекта класса B
    a = b->c;     // присваивание сильного дескриптора слабому
    f(b->c);      // инициализация сильного дескриптора слабым
  }
  GC<asp_t>::Collect(); // сборка мусора
  return 0;
}

Результат работы этой программы представлен ниже:

A ctor
C ctor
A ctor
B ctor
100
C dtor
A dtor
A dtor
B dtor

Выводы

Теперь мы можем ответить на ряд интересных вопросов. Когда при использовании GC происходит удаление объекта? Тогда, когда объект находится вне периметра и вызывается процедура сборки мусора. Нужны ли деструкторы у управляемых объектов? В общем случае нет, поскольку время вызова деструктора заранее не определено, и поэтому мы более не можем полагаться на деструкторы в задачах освобождения ресурсов. Один из путей решения проблемы освобождения ресурсов состоит в определении в классе GCObject виртуальной функции Finalize(), которую программист может переопределить в управляемых классах для того, чтобы при последующем её вызове освободить ресурсы вручную. Но, как легко можно заметить, это противоречит принципу автоматического управления ресурсами. Ведь GC решает только проблему работы с памятью, а управление остальными ресурсами перекладывается на плечи программиста.

Кроме того, данная реализация GC предъявляет достаточно жёсткие требования к коду, использующему её.

Для начала, каждый объект, который участвует в процессе автоматического управления памятью, обязан наследоваться от класса GCObject и реализовывать перебор всех внутренних ссылок на другие объекты. Данное ограничение связано с ограничениями, накладываемыми C++. Если бы C++ имел механизмы, схожие с информацией о типах COM, или с Reflection в Java/CLR, он смог бы организовать перебор ссылок с помощью универсального кода. Единственной проблемой при этом будет то, что C++ разрабатывался как гибкий универсальный язык, и любая возможность в нем априори необязательна. GC тоже обязано быть расширением, а не единственно возможным вариантом. Поэтому необходимо отличать ссылки на обыкновенные объекты от ссылок на объекты, управляемые GC. На сегодняшний момент MС++ (managed C++) от Microsoft поддерживает атрибут __gc, который служит признаком того, что объекты управляются GC. __gc применяется только в MC++ и требует поддержки CLR. В принципе, подобный механизм можно было бы встроить и в обычный C++, но, как уже говорилось выше, главный разработчик этого языка Бьёрн Страуструп противится этой идее, утверждая, что C++ обладает всем необходимым для реализации GC в отдельной библиотеке. Данная статья доказывает непрактичность (хотя и возможность) такого подхода.

Если бы компилятор С++ взял на себя выявление глобальных ссылок и ссылок, размещенных в стеках потоков, отпала бы также надобность в smart-указателях и дескрипторах, что могло бы существенно упростить код.

Надо так же отметить неудобство использования конструкторов управляемых объектов. В этой реализации GC невозможно использовать конструкторы с параметрами. Добавление такой возможности сильно усложнило бы реализацию smart-указателей и дескрипторов.

В рассмотренной реализации сборки мусора не учтены многие моменты, которые присущи полноценным реализациям. Например, не рассмотрена проблема взаимодействия неуправляемых объектов с управляемыми. Также не рассмотрено применение сборки мусора в многопоточных приложениях.

Отметим, что кроме рассмотренных алгоритмов, существует множество других более эффективых способов управления памятью, к числу которых относится, например, целое семейство инкрементных сборщиков мусора.


Эта статья опубликована в журнале RSDN Magazine #1. Информацию о журнале можно найти здесь