stringtranslate.com

Финализатор

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

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

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

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

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

Для языков, которые реализуют сбор мусора посредством подсчета ссылок , терминология различается: в некоторых языках, таких как Objective-C и Perl, используется деструктор , а в других языках, таких как Python, используется финализатор (согласно спецификации, 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 (JNI) и ByteBufferобъектами в новом вводе-выводе (NIO). Последнее может вызвать проблемы из-за того, что сборщик мусора не может отслеживать эти внешние ресурсы, поэтому они не будут собираться достаточно агрессивно, а также может вызвать ошибки нехватки памяти из-за исчерпания неуправляемой памяти — этого можно избежать, обрабатывая собственную память. в качестве ресурса и используя шаблон удаления , как описано ниже.

Финализаторы, как правило, гораздо менее необходимы и гораздо реже используются, чем деструкторы. Они гораздо менее необходимы, поскольку сбор мусора автоматизирует управление памятью , и гораздо реже используются, потому что они, как правило, не выполняются детерминированно — их можно не вызывать своевременно или даже вообще, а среду выполнения невозможно предсказать — и, следовательно, любые очистка, которая должна выполняться детерминированным способом, вместо этого должна выполняться каким-либо другим методом, чаще всего вручную с помощью шаблона удаления . Примечательно, что и 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]

Выполнение

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

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

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

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

Проблемы

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

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

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

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

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

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

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

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

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

Однако в языках с недетерминированным временем жизни объектов (к которым относятся все основные языки со сборкой мусора, такие как C#, Java и Python) это не работает, поскольку финализация может быть несвоевременной или вообще не произойти, и, таким образом, ресурсы может не выпускаться долгое время или вообще не выпускаться, что приводит к утечкам ресурсов . Вместо этого в этих языках ресурсы обычно управляются вручную с помощью шаблона удаления : ресурсы по-прежнему могут быть получены во время инициализации, но освобождаются путем вызова метода 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, используют термин «уничтожение». Термины «финализация» и «финализация» также используются во влиятельной книге « Шаблоны проектирования» (1994). [a] [15] Появление Java в 1995 году содержало finalizeметоды, которые популяризировали этот термин и связали его со сборкой мусора, и языки с этого момента обычно проводят это различие и используют термин «финализация», особенно в контексте сборки мусора. .

Примечания

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

Рекомендации

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

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

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