stringtranslate.com

Финализатор

В информатике финализатор или метод финализации — это специальный метод , который выполняет финализацию , как правило, некоторую форму очистки. Финализатор выполняется во время уничтожения объекта , до того, как объект будет освобожден , и является дополнительным к инициализатору , который выполняется во время создания объекта , после выделения . Финализаторы настоятельно не рекомендуются некоторыми из-за трудности в правильном использовании и сложности, которую они добавляют, и вместо этого предлагаются альтернативы, в основном шаблон dispose [1] (см. проблемы с финализаторами).

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

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

Терминология финализатора и финализации в сравнении с деструктором и уничтожением различается у разных авторов и иногда неясна.

В общем случае деструктор — это метод, вызываемый детерминированно при уничтожении объекта, а его архетипом являются деструкторы C++; в то время как финализатор вызывается недетерминированно сборщиком мусора, а его архетипом являются методы Java . finalize

Для языков, реализующих сборку мусора с помощью подсчета ссылок , терминология различается: некоторые языки, такие как Objective-C и Perl , используют destructor , а другие языки, такие как Python, используют finalizer (согласно спецификации, Python является сборщиком мусора, но эталонная реализация CPython , начиная с версии 2.0, использует комбинацию подсчета ссылок и сборки мусора). Это отражает то, что подсчет ссылок приводит к полудетерминированному времени жизни объекта: для объектов, которые не являются частью цикла, объекты уничтожаются детерминированно, когда счетчик ссылок падает до нуля, но объекты, которые являются частью цикла, уничтожаются недетерминированно, как часть отдельной формы сборки мусора.

В определенном узком техническом использовании конструктор и деструктор являются терминами уровня языка, означающими методы, определенные в классе , в то время как инициализатор и финализатор являются терминами уровня реализации, означающими методы, вызываемые во время создания или уничтожения объекта . Так, например, исходная спецификация языка C# ссылалась на «деструкторы», хотя C# является сборщиком мусора, но спецификация для Common Language Infrastructure (CLI) и реализация ее среды выполнения как Common Language Runtime (CLR) ссылались на «финализаторы». Это отражено в заметках комитета языка C#, в которых, в частности, говорится: «Компилятор C# компилирует деструкторы в ... [вероятно] финализаторы экземпляров[ов]». [4] [5] Эта терминология сбивает с толку, и поэтому более поздние версии спецификации C# ссылаются на метод уровня языка как на «финализаторы». [6]

Другим языком, который не делает такого терминологического различия, является D. Хотя классы D являются сборщиками мусора, их функции очистки называются деструкторами. [7]

Использовать

Финализация в основном используется для очистки, освобождения памяти или других ресурсов: для освобождения памяти, выделенной с помощью ручного управления памятью ; для очистки ссылок, если используется подсчет ссылок (уменьшение счетчиков ссылок); для освобождения ресурсов, особенно в идиоме получения ресурсов как инициализации (RAII); или для отмены регистрации объекта. Объем финализации значительно различается в зависимости от языка: от обширной финализации в C++, которая имеет ручное управление памятью, подсчет ссылок и детерминированное время жизни объектов; до частого отсутствия финализации в Java, которая имеет недетерминированное время жизни объектов и часто реализуется с помощью трассирующего сборщика мусора. Также возможно, что явной (заданной пользователем) финализации будет мало или вообще не будет, но будет значительная неявная финализация, выполняемая компилятором, интерпретатором или средой выполнения; это распространено в случае автоматического подсчета ссылок, как в ссылочной реализации Python CPython или в автоматическом подсчете ссылок в реализации 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метод. [8]

В JavaScript FinalizationRegistry позволяет запрашивать обратный вызов, когда объект удаляется сборщиком мусора.

В Python финализатор — это метод, называемый __del__.

В Perl финализатор — это метод, называемый DESTROY.

UML-класс на языке C#, содержащий конструктор и финализатор.

В C# финализатор (в более ранних версиях стандарта его называли «деструктором») — это метод, имя которого представляет собой имя класса с ~префиксом, например ~Foo, — это тот же синтаксис, что и у деструктора C++ , и эти методы изначально назывались «деструкторами» по аналогии с C++, несмотря на то, что у них было разное поведение, но были переименованы в «финализаторы» из-за возникшей путаницы. [6]

В C++/CLI, где есть как деструкторы, так и финализаторы, деструктор — это метод, имя которого представляет собой имя класса с ~префиксом, например ~Foo(как в C#), а финализатор — это метод, имя которого представляет собой имя класса с !префиксом, например !Foo.

В Go финализаторы применяются к одному указателю путем вызова runtime.SetFinalizerфункции в стандартной библиотеке. [9]

Выполнение

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

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

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

В других случаях, особенно в языках CLR, таких как C#, финализация отслеживается отдельно от самих объектов, и объекты могут многократно регистрироваться или отменяться для финализации.

Проблемы

В зависимости от реализации финализаторы могут вызывать значительное количество проблем, и поэтому их использование настоятельно не рекомендуется рядом органов власти. [10] [11] К таким проблемам относятся: [10]

Кроме того, финализаторы могут не запуститься из-за того, что объекты остаются доступными после того, как они должны быть мусором, либо из-за ошибок программирования, либо из-за неожиданной достижимости. Например, когда Python перехватывает исключение (или исключение не перехватывается в интерактивном режиме), он сохраняет ссылку на стековый фрейм, где было вызвано исключение, что сохраняет активными объекты, на которые ссылаются из этого стекового фрейма.

В Java финализаторы в суперклассе также могут замедлить сборку мусора в подклассе, поскольку финализатор может потенциально ссылаться на поля в подклассе, и, таким образом, поле не может быть удалено сборщиком мусора до следующего цикла после запуска финализатора. [10] Этого можно избежать, используя композицию вместо наследования .

Управление ресурсами

Распространенным антишаблоном является использование финализаторов для освобождения ресурсов по аналогии с идиомой получения ресурсов и инициализации (RAII) в C++: получить ресурс в инициализаторе (конструкторе) и освободить его в финализаторе (деструкторе). Это не работает по ряду причин. В основном финализаторы могут никогда не вызываться, и даже если они вызваны, то не могут быть вызваны своевременно — таким образом, использование финализаторов для освобождения ресурсов, как правило, приводит к утечкам ресурсов . Кроме того, финализаторы не вызываются в предписанном порядке, в то время как ресурсы часто необходимо освобождать в определенном порядке, часто в обратном порядке, в котором они были получены. Кроме того, поскольку финализаторы вызываются по усмотрению сборщика мусора, они часто будут вызываться только при давлении управляемой памяти (когда доступно мало управляемой памяти), независимо от давления ресурсов — если дефицитные ресурсы удерживаются мусором, но доступно много управляемой памяти, сборка мусора может не произойти, таким образом не вернув эти ресурсы.

Таким образом, вместо использования финализаторов для автоматического управления ресурсами, в языках со сборкой мусора вместо этого необходимо вручную управлять ресурсами, как правило, с помощью шаблона dispose . В этом случае ресурсы все еще могут быть получены в инициализаторе, который вызывается явно при создании экземпляра объекта, но освобождаются в методе dispose. Метод dispose может быть вызван явно или неявно языковыми конструкциями, такими как C#'s using, -with-resources в Java tryили Python's with.

Однако в некоторых случаях для освобождения ресурсов используются как шаблон dispose, так и финализаторы. Это в основном встречается в языках CLR, таких как C#, где финализация используется в качестве резервной копии для освобождения: когда ресурс получен, приобретающий объект ставится в очередь на финализацию, так что ресурс освобождается при уничтожении объекта, даже если ресурс не освобождается при ручном освобождении.

Детерминированное и недетерминированное время жизни объектов

В языках с детерминированным временем жизни объектов, в частности, в C++, управление ресурсами часто осуществляется путем привязки времени жизни владения ресурсами к времени жизни объекта, получения ресурсов во время инициализации и освобождения их во время финализации; это известно как получение ресурсов при инициализации (RAII). Это гарантирует, что владение ресурсами является инвариантом класса , и что ресурсы освобождаются немедленно при уничтожении объекта.

Однако в языках с недетерминированным временем жизни объектов — которые включают все основные языки со сборкой мусора, такие как C#, Java и Python — это не работает, потому что финализация может быть несвоевременной или может не произойти вообще, и, таким образом, ресурсы могут не быть освобождены в течение длительного времени или даже вообще, что приводит к утечкам ресурсов . В этих языках ресурсы вместо этого обычно управляются вручную с помощью шаблона dispose : ресурсы все еще могут быть получены во время инициализации, но освобождаются путем вызова disposeметода. Тем не менее, использование финализации для освобождения ресурсов в этих языках является распространенным антишаблоном , и если забыть вызвать, disposeвсе равно произойдет утечка ресурсов.

В некоторых случаях обе техники объединяются, используя явный метод dispose, но также освобождая все еще удерживаемые ресурсы во время финализации в качестве резервной копии. Это обычно встречается в C# и реализуется путем регистрации объекта для финализации всякий раз, когда ресурс приобретается, и подавления финализации всякий раз, когда ресурс освобождается.

Воскрешение объекта

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

Кроме того, воскрешение объекта означает, что объект не может быть уничтожен, и в патологических случаях объект всегда может воскресить себя во время финализации, делая себя неуничтожимым. Чтобы предотвратить это, некоторые языки, такие как Java и Python (начиная с Python 3.4), финализируют объекты только один раз и не финализируют воскрешенные объекты. [ требуется цитата ] Конкретно это делается путем отслеживания того, был ли объект финализирован на основе объект-за-объектом. Objective-C также отслеживает финализацию (по крайней мере, в последних [ когда? ] версиях Apple [ требуется разъяснение ] ) по аналогичным причинам, рассматривая воскрешение как ошибку.

Другой подход используется в .NET Framework , в частности C# и Visual Basic .NET , где финализация отслеживается по "очереди", а не по объекту. В этом случае, если указанный пользователем финализатор предоставлен, по умолчанию объект финализируется только один раз (он ставится в очередь на финализацию при создании и выводится из очереди после финализации), но это можно изменить, вызвав модуль GC. Финализацию можно предотвратить, вызвав GC.SuppressFinalize, который выводит объект из очереди, или повторно активировать, вызвав GC.ReRegisterForFinalize, который ставит объект в очередь. Они особенно используются при использовании финализации для управления ресурсами в качестве дополнения к шаблону удаления или при реализации пула объектов .

Контраст с инициализацией

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

Помимо назначения начальных значений, инициализация в основном используется для получения ресурсов или регистрации объекта в какой-либо службе (например, обработчике событий ). Эти действия имеют симметричные действия освобождения или отмены регистрации, и они могут симметрично обрабатываться в финализаторе, что делается в RAII. Однако во многих языках, особенно в тех, где есть сборка мусора, время жизни объекта асимметрично: создание объекта происходит детерминировано в некоторой явной точке кода, но уничтожение объекта происходит недетерминировано, в некоторой неуказанной среде, по усмотрению сборщика мусора. Эта асимметрия означает, что финализация не может эффективно использоваться как дополнение к инициализации, поскольку она не происходит своевременно, в указанном порядке или в указанной среде. Симметрия частично восстанавливается также путем утилизации объекта в явной точке, но в этом случае утилизация и уничтожение не происходят в одной и той же точке, и объект может находиться в состоянии «утилизирован, но все еще жив», что ослабляет инварианты класса и усложняет использование.

Переменные обычно инициализируются в начале своего жизненного цикла, но не финализируются в конце своего жизненного цикла – хотя если переменная имеет объект в качестве своего значения, объект может быть финализирован. В некоторых случаях переменные также финализируются: расширения GCC позволяют финализировать переменные.

Связь сfinally

Как отражено в названии, «финализация» и finallyконструкция выполняют схожие цели: выполняют некое финальное действие, обычно очистку, после того, как что-то другое завершилось. Они различаются тем, когда они происходят — finallyпредложение выполняется, когда выполнение программы покидает тело связанного tryпредложения — это происходит во время раскручивания стека, и, таким образом, есть стек ожидающих finallyпредложений, в порядке — в то время как финализация происходит, когда объект уничтожается, что происходит в зависимости от метода управления памятью, и в целом есть просто набор объектов, ожидающих финализации — часто в куче — которая не обязательно должна происходить в каком-либо определенном порядке.

Однако в некоторых случаях они совпадают. В C++ уничтожение объектов детерминировано, и поведение предложения finallyможет быть получено с помощью локальной переменной с объектом в качестве значения, область действия которой является блоком, соответствующим телу предложения try— объект финализируется (уничтожается), когда выполнение выходит из этой области действия, точно так же, как если бы было finallyпредложение. По этой причине в C++ нет finallyконструкции — разница в том, что финализация определяется в определении класса как метод деструктора, а не в месте вызова в finallyпредложении.

Наоборот, в случае finallyпредложения в сопрограмме , как в генераторе Python, сопрограмма может никогда не завершиться — только когда-либо уступая — и, таким образом, при обычном выполнении finallyпредложение никогда не выполняется. Если интерпретировать экземпляры сопрограммы как объекты, то предложение finallyможно считать финализатором объекта, и, таким образом, оно может быть выполнено, когда экземпляр подвергается сборке мусора. В терминологии Python определение сопрограммы — это функция-генератор, в то время как ее экземпляр — это итератор-генератор, и, таким образом, finallyпредложение в функции-генераторе становится финализатором в итераторах-генераторах, созданных из этой функции.

История

Понятие финализации как отдельного шага в уничтожении объекта восходит к Монтгомери (1994), [13] по аналогии с более ранним различием инициализации в построении объекта в Мартине и Оделле (1992). [14] Литература до этого момента использовала «уничтожение» для этого процесса, не различая финализацию и освобождение, и языки программирования, относящиеся к этому периоду, такие как C++ и Perl, используют термин «уничтожение». Термины «финализировать» и «финализация» также используются во влиятельной книге Design Patterns (1994). [a] [15] Введение Java в 1995 году содержало finalizeметоды, которые популяризировали этот термин и связали его со сборкой мусора, и языки с этого момента обычно проводят это различие и используют термин «финализация», особенно в контексте сборки мусора.

Примечания

  1. ^ Опубликовано в 1994 году, авторские права защищены в 1995 году.

Ссылки

  1. ^ Jagger, Perry & Sestoft 2007, стр. 542, «В C++ деструктор вызывается определенным образом, тогда как в C# финализатор — нет. Чтобы получить определенное поведение из C#, следует использоватьDispose.
  2. ^ Бём, Ханс-Дж. (2002). Деструкторы, финализаторы и синхронизация. Симпозиум по принципам языков программирования (POPL).
  3. ^ Jagger, Perry & Sestoft 2007, стр. 542, Деструкторы C++ против финализаторов C# Деструкторы C++ являются определенными в том смысле, что они запускаются в известные моменты времени, в известном порядке и из известного потока. Таким образом, они семантически сильно отличаются от финализаторов C#, которые запускаются в неизвестные моменты времени, в неизвестном порядке, из неизвестного потока и по усмотрению сборщика мусора.
  4. ^ Полностью: "Мы собираемся использовать термин "деструктор" для члена, который выполняется при освобождении экземпляра. Классы могут иметь деструкторы; структуры не могут. В отличие от C++, деструктор не может быть вызван явно. Уничтожение недетерминировано — вы не можете достоверно знать, когда деструктор будет выполнен, за исключением того, что он выполняется в какой-то момент после того, как все ссылки на объект будут освобождены. Деструкторы в цепочке наследования вызываются по порядку, от большего потомка к меньшему. Нет необходимости (и нет способа) для производного класса явно вызывать базовый деструктор. Компилятор C# компилирует деструкторы в соответствующее представление CLR. Для этой версии это, вероятно, означает финализатор экземпляра, который выделяется в метаданных. CLR может предоставлять статические финализаторы в будущем; мы не видим никаких препятствий для использования статических финализаторов в C#.", 12 мая 1999 г.
  5. ^ В чем разница между деструктором и финализатором?, Эрик Липперт, Блог Эрика Липперта: Невероятные приключения в кодировании, 21 января 2010 г.
  6. ^ ab Jagger, Perry & Sestoft 2007, стр. 542, «В предыдущей версии этого стандарта то, что сейчас называется «финализатором», называлось «деструктором». Опыт показал, что термин «деструктор» вызывал путаницу и часто приводил к неправильным ожиданиям, особенно у программистов, знающих C++. В C++ деструктор вызывается определенным образом, тогда как в C# финализатор — нет. Чтобы получить определенное поведение из C#, следует использовать Dispose.«
  7. ^ Деструкторы классов Деструкторы классов в D
  8. ^ java.lang, Объект класса: финализация
  9. ^ "Пакет времени выполнения - среда выполнения - PKG.go.dev".
  10. ^ abc "MET12-J. Не используйте финализаторы", Dhruv Mohindra, The CERT Oracle Secure Coding Standard for Java, 05. Методы (MET) Архивировано 04.05.2014 на Wayback Machine
  11. ^ object.__del__(self), Справочник по языку Python, 3. Модель данных: «... __del__()методы должны выполнять абсолютный минимум, необходимый для поддержания внешних инвариантов».
  12. ^ Ханс-Дж. Бём, Финализация, потоки и модель памяти на основе технологии Java™, конференция JavaOne, 2005.
  13. ^ Монтгомери 1994, стр. 120, «Как и в случае с созданием экземпляра объекта, проектирование завершения объекта может выиграть от реализации двух операций для каждого класса — операции финализации и операции завершения . Операция финализации разрывает связи с другими объектами, обеспечивая целостность структуры данных».
  14. ^ Монтгомери 1994, стр. 119, «Рассмотрите возможность реализации создания экземпляра класса как операции создания и инициализации , как предлагают Мартин и Оделл. Первая выделяет хранилище для новых объектов, а вторая конструирует объект в соответствии со спецификациями и ограничениями».
  15. ^ "Каждый новый класс имеет фиксированные накладные расходы на реализацию (инициализация, финализация и т. д.)", " деструктор В C++ — операция, которая автоматически вызывается для финализации объекта, который должен быть удален."

Дальнейшее чтение

Внешние ссылки