Введение

В программировании, специальный метод, выполняемый во время уничтожения объекта, видеоигра, процесс, связанный с оптическими дисковыми носителями. В компьютерной науке, финализатор или метод финализации – это специальный метод, который выполняет финализацию, обычно в виде некоторой очистки. Финализатор выполняется во время уничтожения объекта, перед его освобождением, и является дополнением к инициализатору, который выполняется во время создания объекта после выделения памяти. Некоторые специалисты настоятельно не рекомендуют использовать финализаторы из-за сложности их правильного применения и усложнения кода, предлагая взамен альтернативные подходы, в основном шаблон утилизации (см. проблемы с финализаторами). Термин "финализатор" чаще всего используется в объектно-ориентированных и функциональных языках программирования, использующих сборку мусора, прототипом которого является Smalltalk. Это противопоставляется деструктору, который является методом, вызываемым для финализации в языках с детерминированным временем жизни объектов, например, C++. Как правило, эти подходы взаимоисключающие: язык либо имеет финализаторы (при автоматической сборке мусора), либо деструкторы (при ручном управлении памятью), хотя в редких случаях язык может иметь и то, и другое, как в C++/CLI и D, а также при использовании подсчета ссылок (вместо трассировки сборки мусора) терминология может отличаться. В техническом контексте термин "финализатор" также может использоваться для обозначения деструкторов, поскольку они также выполняют финализацию, и существуют некоторые более тонкие различия – см. раздел "Терминология". Термин "final" также может указывать на класс, который нельзя наследовать; это не связано с финализацией.

Терминология

Терминология, используемая для обозначения finalizer и finalization в противовес destructor и destruction, различается у разных авторов и порой остаётся неясной. В общем понимании, деструктор – это метод, который вызывается детерминированно при уничтожении объекта, и классическим примером служат деструкторы C++; в то время как финализер вызывается недетерминированно сборщиком мусора, и классическим примером являются методы `finalize` в Java. Для языков, реализующих сборку мусора посредством подсчёта ссылок, терминология варьируется: некоторые языки, такие как Objective-C и Perl, используют термин «деструктор», а другие, например Python, – «финализер» (согласно спецификации, Python использует сборку мусора, однако референсная реализация CPython, начиная с версии 2.0, использует комбинацию подсчёта ссылок и сборки мусора). Это отражает тот факт, что подсчёт ссылок приводит к полудетерминированному времени жизни объекта: для объектов, не входящих в цикл, уничтожение происходит детерминированно при достижении счётчика ссылок нуля, а объекты, входящие в цикл, уничтожаются недетерминированно как часть отдельного механизма сборки мусора. В некоторых узкоспециализированных технических контекстах конструктор и деструктор являются терминами, относящимися к языку, то есть методами, определёнными в классе, в то время как инициализатор и финализатор – терминами, относящимися к реализации, то есть методами, вызываемыми при создании или уничтожении объекта. Например, в оригинальной спецификации языка C# использовался термин «деструкторы», хотя C# использует сборку мусора, а в спецификации Common Language Infrastructure (CLI) и реализации среды выполнения Common Language Runtime (CLR) использовался термин «финализаторы». Это отражено в заметках комитета по языку C#, где в частности говорится: «Компилятор C# компилирует деструкторы в [вероятно] экземпляры финализаторов». Эта терминология вводит в заблуждение, поэтому в более поздних версиях спецификации C# метод, относящийся к языку, называется «финализатором».

Использование

Финализация в основном используется для очистки, для освобождения памяти или других ресурсов: для освобождения памяти, выделенной с помощью ручного управления памятью; для очистки ссылок, если используется подсчёт ссылок (уменьшение счётчиков ссылок); для освобождения ресурсов, особенно в идиоме RAII (приобретение ресурса – инициализация); или для отмены регистрации объекта. Объём финализации значительно варьируется в зависимости от языка, от обширной финализации в C++, который использует ручное управление памятью, подсчёт ссылок и детерминированные сроки жизни объектов; до часто отсутствующей финализации в Java, где сроки жизни объектов недетерминированы и часто используется сборщик мусора с трассировкой. Также возможно, что явной (определённой пользователем) финализации может быть мало или не быть вовсе, но при этом существует значительная неявная финализация, выполняемая компилятором, интерпретатором или средой выполнения; это часто встречается при автоматическом подсчёте ссылок, как в эталонной реализации CPython для Python, или при автоматическом подсчёте ссылок в реализации Objective-C от Apple, которые автоматически разрывают ссылки в процессе финализации. Финализатор может содержать произвольный код; одним из сложных применений является автоматическое возвращение объекта в пул объектов. Освобождение памяти во время финализации распространено в языках, таких как C++, где ручное управление памятью является стандартным, но также происходит и в управляемых языках, когда память выделяется за пределами управляемой кучи (внешне по отношению к языку); в Java это происходит с объектами Java Native Interface (JNI) и ByteBuffer в New I/O (NIO). Последнее может вызывать проблемы, поскольку сборщик мусора не может отслеживать эти внешние ресурсы, поэтому они могут не собираться достаточно агрессивно, что может привести к ошибкам нехватки памяти из-за исчерпания неуправляемой памяти. Этого можно избежать, рассматривая собственную память как ресурс и используя шаблон `Dispose`, как обсуждается ниже. Финализаторы, как правило, гораздо менее необходимы и гораздо реже используются, чем деструкторы. Они менее необходимы, поскольку сборка мусора автоматизирует управление памятью, и реже используются, поскольку их выполнение обычно не является детерминированным – они могут быть вызваны не вовремя или вообще не быть вызваны, и предсказать среду выполнения невозможно – поэтому любую очистку, которую необходимо выполнить детерминированным образом, следует выполнять другим способом, чаще всего вручную с помощью шаблона `Dispose`. Важно отметить, что Java и Python не гарантируют, что финализаторы когда-либо будут вызваны, и поэтому на них нельзя полагаться для очистки. Из-за отсутствия контроля программиста над их выполнением обычно рекомендуется избегать финализаторов для любых операций, кроме самых тривиальных. В частности, операции, часто выполняемые в деструкторах, обычно не подходят для финализаторов. Распространённая антипаттерн – писать финализаторы так, как если бы они были деструкторами, что является ненужным и неэффективным из-за различий между финализаторами и деструкторами. Это особенно распространено среди программистов на C++, поскольку деструкторы широко используются в идиоматическом C++, следуя идиоме RAII (приобретение ресурса – инициализация).

Синтаксис

Языки программирования, использующие финализаторы, включают C++/CLI, C#, Clean, Go, Java, JavaScript и Python. Синтаксис значительно варьируется в зависимости от языка. В Java финализатор — это метод с именем `finalize`, переопределяющий метод `Object.finalize`. В JavaScript `FinalizationRegistry` позволяет запросить обратный вызов при сборке мусора объекта. В Python финализатор — это метод, называемый `__del__`. В Perl финализатор — это метод с именем `DESTROY`. В C# финализатор (называемый "деструктором" в более ранних версиях стандарта) — это метод, имя которого совпадает с именем класса и имеет префикс `~`, например, `~Foo` — это тот же синтаксис, что и у деструктора C++, и эти методы изначально назывались "деструкторами" по аналогии с C++, несмотря на различное поведение, но были переименованы в "финализаторы" из-за вызванной этим путаницы. В C++/CLI, где есть как деструкторы, так и финализаторы, деструктор — это метод, имя которого совпадает с именем класса и имеет префикс `~`, например, `~Foo` (как в C#), а финализатор — это метод, имя которого совпадает с именем класса и имеет префикс `!`, например, `!Foo`. В Go финализаторы применяются к одному указателю путем вызова функции `runtime.SetFinalizer` из стандартной библиотеки.

Реализация

Финализатор вызывается, когда объект подлежит сборке мусора – после того, как объект стал недоступным, но до освобождения его памяти. Финализация происходит недетерминированно, по усмотрению сборщика мусора, и может никогда не произойти. Это отличается от деструкторов, которые вызываются детерминированно, как только объект перестает использоваться, и всегда вызываются, за исключением случаев аварийного завершения программы. Финализаторы чаще всего являются методами экземпляров, поскольку им требуется выполнять операции, специфичные для объекта. Сборщик мусора также должен учитывать возможность "воскрешения" объекта. Чаще всего это реализуется путем сначала выполнения финализаторов, затем проверки, были ли какие-либо объекты "воскрешены", и, если да, отмены их уничтожения. Эта дополнительная проверка потенциально затратна – простая реализация повторно проверяет весь мусор, даже если у одного объекта есть финализатор, – и, таким образом, замедляет и усложняет сборку мусора. По этой причине объекты с финализаторами могут собираться реже, чем объекты без финализаторов (только в определенных циклах), что усугубляет проблемы, вызванные зависимостью от своевременной финализации, такие как утечки ресурсов. Если объект "воскрешен", возникает вопрос, будет ли его финализатор вызван снова при следующем уничтожении. В отличие от деструкторов, финализаторы потенциально могут быть вызваны несколько раз. Если финализаторы вызываются для "воскрешенных" объектов, объекты могут неоднократно "воскрешаться" и становиться неразрушимыми; это происходило в реализации CPython для Python до версии 3.4, а также в языках CLR, таких как C#. Чтобы избежать этого, во многих языках, включая Java, Objective-C (по крайней мере, в последних реализациях Apple) и Python, начиная с версии 3.4, объекты финализируются не более одного раза, что требует отслеживания факта финализации объекта. В других случаях, особенно в языках CLR, таких как C#, финализация отслеживается отдельно от самих объектов, и объекты могут многократно регистрироваться или отменяться регистрация для финализации.

Проблемы

В зависимости от реализации, финализаторы могут вызывать значительное количество проблем и поэтому настоятельно не рекомендуются многими специалистами. Эти проблемы включают в себя:

Финализаторы могут приводить к взаимоблокировкам (дедлокам), если используются механизмы синхронизации, такие как блокировки, из-за неопределенного порядка их выполнения и возможного параллельного запуска. Финализаторы, запускаемые при завершении программы, не могут полагаться на стандартную среду выполнения и поэтому могут завершиться с ошибкой из-за неверных предположений – по этой причине финализаторы часто не выполняются при завершении. Более того, финализаторы могут не выполниться, если объекты остаются доступными дольше, чем ожидалось, либо из-за ошибок в коде, либо из-за непредвиденной доступности. Например, когда Python перехватывает исключение (или исключение не перехватывается в интерактивном режиме), он сохраняет ссылку на кадр стека, в котором произошло исключение, что удерживает в памяти объекты, на которые ссылается этот кадр стека. В Java финализаторы в родительском классе также могут замедлить сборку мусора в дочернем классе, поскольку финализатор потенциально может ссылаться на поля дочернего класса, из-за чего сборку мусора этих полей нельзя выполнить до следующего цикла, после запуска финализатора. Появление Java в 1995 году представило методы `finalize`, которые популяризировали этот термин и связали его со сборкой мусора, и с тех пор языки программирования обычно проводят такое различие и используют термин «финализация», особенно в контексте сборки мусора.