Другой подход
к амортизации задержек используется
в «сборщиках мусора» реального
времени среды Java (JRTS — Java Real-Time
Specification). В них дефрагментация
выполняется эпизодически и лишь
в самом крайнем случае, когда
не может быть найден свободный
блок памяти нужного размера. Кроме того,
«сборка мусора» выполняется в течение
фиксированных интервалов времени (квантов),
которые обязательно чередуются с квантами
работы программы [3].
В модели
с автоматической «сборкой мусора»
программный код завершения жизни объекта
(метод Finalize, или, говоря иначе, деструктор)
выполняется асинхронно в контексте «сборщика
мусора». Момент и порядок вызова этого
метода у того или иного объекта никак
не детерминированы, что порождает проблему,
если объект управляет некоторым ресурсом,
например сетевым соединением. Открытие
соединения происходит при создании и
инициализации объекта, т. е. предсказуемо,
а закрытие соединения — во время «сборки
мусора», т. е. непредсказуемо и далеко
не сразу после потери последней ссылки
на объект. В результате лимит сетевых
соединений или других ресурсов может
временно исчерпаться.
Для решения
указанной проблемы в среде
.NET используется детерминированное
завершение жизни объектов через
интерфейс IDisposable. Этот интерфейс
имеет единственный метод Dispose, который
реализуется в объектах, управляющих ресурсами.
Метод Dispose, как правило, освобождает ресурсы
и отменяет работу процедуры-завершителя
(метода Finalize), чтобы ускорить освобождение
памяти. После вызова метода Dispose объект
не уничтожается, а остается в памяти до
тех пор, пока не пропадут все ссылки на
него.
Применение
интерфейса IDis-posable на практике выявило
новые проблемы. Оказалось, что
после вызова метода Dispose в программе
могут оставаться ссылки на объект,
находящийся уже в некорректном состоянии.
Программе никто не запрещает обращаться
по этим физически доступным, но логически
«зависшим» ссылкам и вызывать у некорректного
объекта различные методы. Метод Dispose может
вызываться повторно, в том числе рекурсивно.
Кроме того, в программе с несколькими
вычислительными потоками может происходить
асинхронный вызов метода Dispose для одного
и того же объекта.
Для решения
проблем этого метода программистам
было предписано делать следующее:
1) определять в объекте булевский флаг,
позволяющий выяснить, работал ли в объекте
код завершения; 2) блокировать объект
внутри метода Dispose на время работы кода
завершения; 3) игнорировать повторные
вызовы метода Dispose, проверяя упомянутый
булевский флаг; 4) в начале public-методов
проверять, находится ли объект уже в завершенном
состоянии, и если это так, то создавать
исключение класса ObjectDisposedException.
Заканчивая
критику «сборщиков мусора», укажем
еще на одну серьезную проблему
с ними — легальную «утечку
памяти». Если в модели с ручным освобождением
объектов «утечка памяти» возникает из-за
невыполненных операторов delete, то в модели
с автоматической «сборкой мусора» —
из-за невыполненного обнуления ссылок.
Такой вид «утечек памяти» характерен
для программного кода, в котором одними
объектами регистрируются обработчики
событий в других объектах. Программисты
порой забывают отключать обработчики
событий, в результате ассоциированные
объекты остаются в памяти, несмотря на
кажущееся отсутствие ссылок на них в
программе.
Модель с автоматической
«сборкой мусора» и принудительным
освобождением памяти
Отвлечемся
на время от проблем реализации
и сформулируем некую идеальную
с точки зрения программиста
модель утилизации
динамической
памяти. На наш взгляд, в этой модели
должны сочетаться: 1) быстрая автоматическая
«сборка мусора» и 2) безопасное принудительное
освобождение памяти.
Наличие «сборки
мусора» означает, что программист
может быть уверен: система следит
за потерей ссылок на объекты
и устраняет «утечку памяти». Наличие
безопасного принудительного освобождения
памяти означает, что программист вправе
уничтожить объект; при этом память объекта
возвращается системе, а все имеющиеся
на него ссылки становятся недействительными
(например, обнуляются).
Эта модель,
называемая нами моделью с автоматической
«сборкой мусора» и принудительным освобождением
памяти, на самом деле не нова и уже давно
применяется в компьютерах «Эльбрус»
(на основе одноименного процессора) и
AS/400 (на основе процессора PowerPC), которые
обеспечивают очень эффективную реализацию
этой модели за счет аппаратной поддержки.
На каждое
машинное слово в этих компьютерах
отводится два дополнительных
бита, называемых битами тегов.
Значения этих битов показывают,
свободно ли машинное слово
или занято, и если занято, то хранится
ли в нем указатель или скалярное значение.
Этими битами управляют аппаратура и операционная
система, прикладным программам они недоступны.
Программа не может создать ссылку сама,
например, превратив в нее число или другие
скалярные данные. Созданием объектов
занимается система, которая размещает
в памяти объекты и создает ссылки на них.
При уничтожении объектов соответствующие
теги памяти устанавливаются в состояние,
запрещающее доступ. Попытка обратиться
к свободной памяти по «зависшему» указателю
приводит к аппаратному прерыванию (подобно
обращению по нулевому указателю). Поскольку
вся память помечена тегами, «сборщику
мусора» нет необходимости анализировать
информацию о типах, чтобы разобраться,
где внутри объектов располагаются ссылки
на другие объекты. Что более важно, ему
почти не нужно тратить время на поиск
недостижимых объектов, поскольку освобожденная
память помечена с помощью тех же тегов
[4, 5].
Ниже сформулированы
базовые принципы модели с
автоматической «сборкой мусора» и
принудительным освобождением памяти
на уровне спецификации для языков программирования:
Выделение
динамической памяти выполняется
оператором/процедурой new (это действие
считается элементарным в системе).
Выделенная память автоматически
инициализируется нулями и всегда привязывается
к типу созданного в памяти объекта.
Уничтожение
объекта — освобождение занимаемой
им динамической памяти —
выполняется автоматически при
пропадании всех ссылок на
объект. Для дефрагментации освободившихся
участков памяти периодически выполняется
«сборка мусора», в результате которой
объекты сдвигаются, а ссылки на них корректируются.
Объекты можно
уничтожать принудительно с помощью
оператора/процедуры delete. В результате
этого действия все ссылки
на объект становятся недействительными,
а попытка последующего доступа к объекту
приводит к исключительной ситуации. Дефрагментация
освобожденной этим способом памяти выполняется
во время «сборки мусора». При этом оставшиеся
ссылки корректируются и получают некоторое
зарезервированное недействительное
значение, например, 1 («зависшие» ссылки
можно было бы обнулять, но в этом случае
стерлась бы разница между нулевой и «зависшей»
ссылкой, что ухудшило бы диагностику
ошибок).
В эти принципы
вписываются как аппаратно-приближенные
языки с ручным освобождением памяти (скажем,
Cи и C++), так и высоко-уровневые языки с
автоматической «сборкой мусора» (Oberon,
Java, C#).
Главный вопрос,
который пока остается открытым,
— можно ли реализовать эту
модель программно, чтобы она эффективно
работала для популярных аппаратных архитектур,
в которых нет тегирования памяти. Подумаем,
каким образом этого можно достичь.
Первое простейшее
решение состоит в том, чтобы
по каждому вызову оператора
delete выполнять просмотр памяти с
корректировкой недействительных ссылок.
Просмотр занимает значительно меньше
времени, чем полная «сборка мусора» с
дефрагментацией памяти. Решение подходит
для мобильных и встроенных устройств
с небольшим объемом ОЗУ и без поддержки
виртуальной памяти.
Второе решение
основано на использовании средств
аппаратной поддержки виртуальной
памяти, которая существует в
большинстве современных компьютерных
архитектур. Виртуальная память
практически всегда имеет страничную
организацию. Страницы памяти
могут быть выгружены на диск и помечены
как отсутствующие. Обращение к данным
в выгруженной странице приводит к аппаратному
прерыванию. Это прерывание обрабатывает
ОС, которая подгружает запрошенную страницу
с диска и замещает ею одну из редко используемых
страниц. В этом механизме нас интересует
возможность аппаратно перехватывать
обращения к страницам виртуальной памяти.
На самом деле страницы могут оставаться
в памяти и на диск не выгружаться. Идея
состоит в том, чтобы при вызове оператора
delete помечать страницы, в которых располагается
удаляемый объект, как отсутствующие.
Обращение к данным на этих страницах
будет вызывать аппаратное прерывание.
Обрабатывая это прерывание, система проверяет,
куда именно выполняется обращение: к
освобожденному участку памяти или к занятому.
Если обращение выполняется к занятому
участку страницы, то запрос удовлетворяется
и работа продолжается в штатном режиме.
Если к освобожденному — то создается
программная исключительная ситуация.
Это решение
имеет очевидный недостаток —
при большом количестве обращений к «живым»
объектам, расположенным на одной странице
рядом с удаленными объектами, будут возникать
холостые аппаратные прерывания, которые
снизят производительность системы. Для
борьбы с этой проблемой система должна
подсчитывать частоту холостых прерываний
и, если ее значение превысит некоторый
порог, досрочно запускать процесс обнуления
недействительных ссылок.
Заключение
По мнению
авторов, наиболее перспективной
моделью утилизации динамической
памяти представляется модель с
автоматической «сборкой мусора» и принудительным
освобождением памяти. Эта модель может
быть эффективно реализована при наличии
аппаратного тегирования оперативной
памяти. Она может быть реализована и в
отсутствие тегирования, возможные варианты
такой реализации предложены в статье