Ищите понимание решений, связанных с языковым дизайном, собираемым мусором. Возможно, специалист по языку мог бы просветить меня? Я родом из C ++, так что эта область сбивает меня с толку.
Кажется, что почти все современные языки со сборкой мусора с поддержкой объектов OOPy, такие как Ruby, Javascript / ES6 / ES7, Actionscript, Lua и т. Д., Полностью опускают парадигму деструктора / финализации. Python, кажется, единственный с его class __del__()
методом. Почему это? Существуют ли функциональные / теоретические ограничения в языках с автоматической сборкой мусора, которые мешают эффективной реализации метода деструктора / финализации на объектах?
Мне крайне не хватает того, что эти языки рассматривают память как единственный ресурс, которым стоит управлять. Как насчет сокетов, файловых дескрипторов, состояний приложений? Без возможности реализовать пользовательскую логику для очистки ресурсов, не связанных с памятью, и состояний при финализации объекта, я должен засорять свое приложение myObject.destroy()
вызовами пользовательского стиля, размещать логику очистки вне моего «класса», нарушать попытки инкапсуляции и откладывать мои приложение к утечке ресурсов из-за человеческой ошибки, а не автоматически обрабатывается gc.
Какие решения по проектированию языка приводят к тому, что эти языки не имеют возможности выполнять пользовательскую логику при удалении объектов? Я должен представить, что есть веская причина. Я хотел бы лучше понять технические и теоретические решения, которые привели к тому, что эти языки не поддерживают уничтожение / финализацию объектов.
Обновить:
Возможно, лучший способ сформулировать мой вопрос:
Почему язык имеет встроенную концепцию экземпляров объектов с классом или классоподобными структурами вместе с пользовательскими экземплярами (конструкторами), но при этом полностью исключает функциональность уничтожения / финализации? Языки, которые предлагают автоматическую сборку мусора, являются основными кандидатами для поддержки уничтожения / завершения объекта, поскольку они знают со 100% уверенностью, когда объект больше не используется. Тем не менее, большинство из этих языков не поддерживают его.
Я не думаю, что это тот случай, когда деструктор никогда не будет вызван, так как это будет утечка памяти ядра, которую gcs разработан, чтобы избежать. Я мог видеть возможный аргумент, заключающийся в том, что деструктор / финализатор может быть вызван только в течение неопределенного времени в будущем, но это не помешало Java или Python поддерживать функциональность.
Каковы основные причины разработки языка, чтобы не поддерживать какую-либо форму завершения объекта?
finalize
/destroy
это ложь? Нет гарантии, что это когда-либо будет выполнено. И даже если вы не знаете, когда (с учетом автоматической сборки мусора) и, если необходимо, контекст все еще существует (возможно, он уже был собран). Поэтому безопаснее обеспечить согласованное состояние другими способами, и можно заставить программиста сделать это.Ответы:
Шаблон, о котором вы говорите, когда объекты знают, как очистить свои ресурсы, попадает в три соответствующие категории. Давайте не будем связывать деструкторы с финализаторами - только один связан с сборкой мусора:
Модели финализатора : метод очистки автоматически объявляется, определяется программистом, вызывается автоматически.
Финализаторы вызываются автоматически перед освобождением сборщиком мусора. Этот термин применяется, если используемый алгоритм сборки мусора может определять жизненные циклы объекта.
Модели деструктора : метод очистки автоматически объявляется, определяется программистом, вызывается автоматически только иногда.
Деструкторы могут вызываться автоматически для объектов, выделенных стеком (поскольку время жизни объекта является детерминированным), но должны вызываться явным образом на всех возможных путях выполнения для объектов, распределенных в куче (поскольку время жизни объекта является недетерминированным).
Шаблон Измельчитель : очистка метод , объявленный, определяется, и называется программистом.
Программисты создают метод удаления и вызывают его сами - вот где
myObject.destroy()
падает ваш пользовательский метод. Если утилизация абсолютно необходима, тогда утилиты должны вызываться на всех возможных путях выполнения.Финализаторы - это дроиды, которых вы ищете.
Шаблон финализатора (шаблон, о котором спрашивает ваш вопрос) - это механизм связывания объектов с системными ресурсами (сокеты, файловые дескрипторы и т. Д.) Для взаимного восстановления сборщиком мусора. Но финализаторы в основном зависят от используемого алгоритма сборки мусора.
Рассмотрим это ваше предположение:
Технически неверно (спасибо, @babou). Сборка мусора в основном касается памяти, а не объектов. Если алгоритм сбора данных обнаружит, что память объекта больше не используется, зависит от алгоритма и (возможно) от того, как ваши объекты ссылаются друг на друга. Давайте поговорим о двух типах сборщиков мусора во время выполнения. Есть много способов изменить и дополнить их базовыми методами:
Отслеживание GC. Эти следы памяти, а не объекты. Если они не увеличены, они не сохраняют обратных ссылок на объекты из памяти. Если они не дополнены, эти GC не будут знать, когда объект может быть завершен, даже если они знают, когда его память недоступна. Следовательно, вызовы финализатора не гарантируются.
Подсчет ссылок GC . Они используют объекты для отслеживания памяти. Они моделируют достижимость объекта с помощью ориентированного графа ссылок. Если в вашем графе ссылок объектов есть цикл, то для всех объектов в цикле никогда не будет вызываться их финализатор (очевидно, до завершения программы). Опять же, вызовы финализатора не гарантируются.
TLDR
Сборка мусора сложна и разнообразна. Вызов финализатора не может быть гарантирован до завершения программы.
источник
finalize()
приведет к повторной ссылке на очищаемый объект?). Однако неспособность гарантировать вызов финализатора до завершения программы не помешала Java поддерживать его. Не сказать, что ваш ответ неправильный, просто, возможно, неполный. Все еще очень хороший пост. Спасибо.В двух словах
Завершение не является простым делом для сборщиков мусора. Его легко использовать с GC для подсчета ссылок, но это семейство GC часто неполное, требующее компенсации утечек памяти путем явного запуска уничтожения и завершения некоторых объектов и структур. Отслеживание сборщиков мусора гораздо эффективнее, но значительно усложняет идентификацию объекта, подлежащего доработке и уничтожению, в отличие от простого определения неиспользуемой памяти, что требует более сложного управления, затрат времени и пространства и сложности реализация.
Введение
Я предполагаю, что вы спрашиваете, почему языки сборки мусора не обрабатывают автоматически уничтожение / финализацию в процессе сбора мусора, как указано в замечании:
Я не согласен с принятым ответом, данным kdbanman . Хотя изложенные факты в основном верны, хотя и сильно склонны к подсчету ссылок, я не верю, что они правильно объясняют ситуацию, о которой пойдет речь в этом вопросе.
Я не верю, что терминология, разработанная в этом ответе, является большой проблемой, и она с большей вероятностью может сбить с толку. Действительно, как представлено, терминология в основном определяется способом активации процедур, а не тем, что они делают. Дело в том, что во всех случаях необходимо завершить объект, который больше не нужен, с помощью некоторого процесса очистки и освободить все ресурсы, которые он использовал, память является лишь одним из них. В идеале все это должно выполняться автоматически, когда объект больше не используется, с помощью сборщика мусора. На практике GC может отсутствовать или иметь недостатки, и это компенсируется явным срабатыванием программой доработки и восстановления.
Явное тригеринг в программе является проблемой, так как он может затруднить анализ ошибок программирования, когда объект, который все еще используется, явно завершается.
Следовательно, гораздо лучше полагаться на автоматическую сборку мусора для восстановления ресурсов. Но есть две проблемы:
некоторые методы сбора мусора допускают утечки памяти, которые препятствуют полной утилизации ресурсов. Это хорошо известно для подсчета ссылок GC, но может появиться для других методов GC при использовании некоторых организаций данных без осторожности (точка не обсуждается здесь).
в то время как методика GC может быть полезна для идентификации ресурсов памяти, которые больше не используются, финализация объектов, содержащихся в них, может быть непростой, и это усложняет проблему восстановления других ресурсов, используемых этими объектами, что часто является целью завершения.
Наконец, важный момент, который часто забывают, заключается в том, что циклы GC могут быть вызваны чем угодно, не только нехваткой памяти, если предусмотрены надлежащие зацепки и если стоимость цикла GC считается оправданной. Следовательно, вполне нормально инициировать сборщик мусора, когда отсутствует какой-либо ресурс, в надежде освободить его.
Подсчет ссылок сборщиков мусора
Подсчет ссылок является слабой техникой сбора мусора , которая не будет правильно обрабатывать циклы. Он действительно будет слаб при разрушении устаревших структур и восстановлении других ресурсов просто потому, что он слаб при восстановлении памяти. Но финализаторы легче всего использовать с сборщиком мусора (GC) подсчета ссылок, поскольку GC ref-count восстанавливает структуру, когда его ref ref уменьшается до 0, когда его адрес известен вместе с его типом, либо статически или динамически. Следовательно, можно восстановить память точно после применения правильного финализатора и рекурсивного вызова процесса для всех указанных объектов (возможно, через процедуру финализации).
В двух словах, финализация легко реализуется с помощью RefCing GC, но страдает от «незавершенности» этого GC, действительно из-за круговых структур, в той же степени, в какой страдает восстановление памяти. Другими словами, при подсчете ссылок память управляется так же плохо, как и другие ресурсы, такие как сокеты, файловые дескрипторы и т. Д.
Действительно, неспособность Ref Count GC восстанавливать циклические структуры (в общем) может рассматриваться как утечка памяти . Вы не можете ожидать, что все GC, чтобы избежать утечек памяти. Это зависит от алгоритма GC и от динамически доступной информации о структуре типов (например, в консервативном GC ).
Отслеживание сборщиков мусора
Более мощное семейство GC, без таких утечек, - это семейство трассировки, которое исследует живые части памяти, начиная с хорошо идентифицированных корневых указателей. Все части памяти, которые не посещаются в этом процессе трассировки (которые на самом деле могут быть разложены различными способами, но я должен упростить), являются неиспользуемыми частями памяти, которые могут быть таким образом восстановлены 1 . Эти сборщики восстановят все части памяти, к которым программа больше не сможет получить доступ, независимо от того, что она делает. Он восстанавливает круговые структуры, и более продвинутые GC основаны на некоторой вариации этой парадигмы, иногда очень сложной. В некоторых случаях его можно комбинировать с подсчетом ссылок и компенсировать его недостатки.
Проблема в том, что ваше утверждение (в конце вопроса):
технически некорректно для отслеживания коллекторов.
Что известно со 100% уверенностью, это то, какие части памяти больше не используются . (Точнее, следует сказать, что они больше недоступны , поскольку некоторые части, которые больше не могут использоваться в соответствии с логикой программы, все еще считаются используемыми, если в программе все еще есть бесполезный указатель на них. данные.) Но необходима дальнейшая обработка и соответствующие структуры, чтобы узнать, какие неиспользуемые объекты могли храниться в этих теперь неиспользуемых частях памяти . Это не может быть определено из того, что известно о программе, так как программа больше не связана с этими частями памяти.
Таким образом, после прохода сборки мусора у вас остаются фрагменты памяти, которые содержат объекты, которые больше не используются, но априори нет способа узнать, что это за объекты, чтобы применить правильную финализацию. Кроме того, если коллектор трассировки является типом метки и развертки, может случиться так, что некоторые фрагменты могут содержать объекты, которые уже были завершены в предыдущем проходе GC, но с тех пор не использовались по причинам фрагментации. Однако это можно решить с помощью расширенной явной типизации.
В то время как простой сборщик просто восстановил бы эти фрагменты памяти, без дальнейших церемоний, для завершения требуется специальный проход, чтобы исследовать эту неиспользуемую память, идентифицировать содержащиеся в ней объекты и применять процедуры финализации. Но такое исследование требует определения типа объектов, которые там хранились, и определение типа также необходимо для применения надлежащего завершения, если таковое имеется.
Таким образом, это подразумевает дополнительные затраты времени GC (дополнительный проход) и, возможно, дополнительные затраты памяти, чтобы сделать правильную информацию о типе доступной во время этого прохода различными способами. Эти затраты могут быть значительными, поскольку часто требуется завершить работу только с несколькими объектами, тогда как время и пространство могут касаться всех объектов.
Другой момент заключается в том, что временные и пространственные накладные расходы могут касаться выполнения программного кода, а не только выполнения GC.
Я не могу дать более точный ответ, указывая на конкретные вопросы, потому что я не знаю специфику многих языков, которые вы перечислите. В случае с C печатание - очень сложная проблема, которая ведет к развитию консервативных коллекционеров. Я думаю, что это влияет и на C ++, но я не специалист по C ++. Это, кажется, подтверждается Гансом Бёмом, который проводил большую часть исследований по консервативному ГК. Консервативный GC не может систематически восстанавливать всю неиспользуемую память именно потому, что может отсутствовать точная информация о типе данных. По той же причине он не сможет систематически применять процедуры завершения.
Таким образом, можно делать то, что вы просите, как вы знаете из некоторых языков. Но это не бесплатно. В зависимости от языка и его реализации, это может повлечь за собой затраты, даже если вы не используете эту функцию. Различные методы и компромиссы могут рассматриваться для решения этих проблем, но это выходит за рамки разумного размера ответа.
1 - это абстрактное представление коллекции трассировки (включающее в себя как ГХ для копирования, так и для разметки и очистки), все меняется в зависимости от типа сборщика трассировки, а исследование неиспользуемой части памяти различается в зависимости от того, копируется или помечается и развертка используется.
источник
getting memory recycled
, которую я называюreclamation
, и выполняю некоторую очистку перед этим, такую как восстановление других ресурсов или обновление некоторых таблиц объектов, которые я вызываюfinalization
. Это казалось мне актуальными вопросами, но я, возможно, упустил момент в вашей терминологии, который был для меня новым.Шаблон деструктора объекта является фундаментальным для обработки ошибок в системном программировании, но не имеет ничего общего с сборкой мусора. Скорее, это связано с сопоставлением времени жизни объекта с областью видимости и может быть реализовано / использовано на любом языке, который имеет функции первого класса.
Пример (псевдокод). Предположим, у вас есть тип «сырой файл», такой как тип дескриптора файла Posix. Есть четыре основных операций,
open()
,close()
,read()
,write()
. Вы хотели бы реализовать «безопасный» тип файла, который всегда очищается после себя. (То есть, у этого есть автоматический конструктор и деструктор.)Я предполагаю, что на нашем языке есть обработка исключений
throw
,try
иfinally
(на языках без обработки исключений вы можете установить дисциплину, в которой пользователь вашего типа возвращает специальное значение для обозначения ошибки.)Вы устанавливаете функцию, которая принимает функцию, которая выполняет работу. Рабочая функция принимает один аргумент (дескриптор «безопасного» файла).
Вы также предоставляете реализации
read()
иwrite()
дляsafe_file
(которые просто вызываютraw_file
read()
иwrite()
). Теперь пользователь использует такойsafe_file
тип:Деструктор C ++ - это просто синтаксический сахар для
try-finally
блока. Практически все, что я здесь сделал, - это конвертировал то, во чтоsafe_file
будет компилироваться класс C ++ с конструктором и деструктором. Обратите внимание, что в C ++ нетfinally
исключений, в частности, потому что Страуструп считал, что использование явного деструктора лучше синтаксически (и он ввел его в язык до того, как в нем появились анонимные функции).(Это упрощение одного из способов, которыми люди занимались обработкой ошибок в Lisp-подобных языках в течение многих лет. Я думаю, что впервые столкнулся с этим в конце 1980-х или начале 1990-х, но я не помню, где.)
источник
safe_file
иwith_file_opened_for_read
(объект, который закрывается, когда выходит из области видимости ). Важно то, что он не имеет тот же синтаксис, что и конструкторы, не имеет значения. Lisp, Scheme, Java, Scala, Go, Haskell, Rust, Javascript, Clojure поддерживают достаточное количество функций первого класса, поэтому им не нужны деструкторы для предоставления той же полезной функции.Это не полный ответ на вопрос, но я хотел добавить пару замечаний, которые не были отражены в других ответах или комментариях.
Вопрос неявно предполагает, что мы говорим об объектно-ориентированном языке в стиле Simula, который сам по себе является ограничением. В большинстве языков, даже с объектами, не все является объектом. Механизм реализации деструкторов налагает затраты, которые не каждый плательщик языка готов заплатить.
C ++ имеет некоторые неявные гарантии порядка уничтожения. Например, если у вас есть древовидная структура данных, дочерние элементы будут уничтожены раньше, чем родительские. Это не относится к языкам GC, поэтому иерархические ресурсы могут высвобождаться в непредсказуемом порядке. Для ресурсов без памяти это может иметь значение.
источник
Когда разрабатывались две наиболее популярные платформы GC (Java и .NET), я думаю, что авторы ожидали, что финализация будет работать достаточно хорошо, чтобы избежать необходимости использования других форм управления ресурсами. Многие аспекты языкового и каркасного проектирования могут быть значительно упрощены, если нет необходимости во всех функциях, необходимых для обеспечения 100% надежного и детерминированного управления ресурсами. В C ++ необходимо различать понятия:
Указатель / ссылка, который идентифицирует объект, который принадлежит исключительно владельцу ссылки, и который не идентифицирован никакими указателями / ссылками, о которых владелец не знает.
Указатель / ссылка, которая идентифицирует разделяемый объект, который никому не принадлежит.
Указатель / ссылка, который идентифицирует объект, который принадлежит исключительно владельцу ссылки, но к которому можно получить доступ через «представления», владелец не может отслеживать.
Указатель / ссылка, которая идентифицирует объект, который обеспечивает представление объекта, который принадлежит кому-то еще.
Если языку / структуре GC не нужно беспокоиться об управлении ресурсами, все вышеперечисленное может быть заменено одним видом ссылки.
Я нахожу наивной идею, что завершение устранит необходимость в других формах управления ресурсами, но независимо от того, было ли такое ожидание разумным в то время, история показала, что во многих случаях требуется более точное управление ресурсами, чем обеспечивает завершение. , Мне кажется, что вознаграждение за признание владения на уровне языка / структуры было бы достаточно, чтобы оправдать стоимость (сложность должна где-то существовать, а перемещение ее на язык / структуру упростило бы пользовательский код), но признаю, что существуют значительные Преимущества разработки - наличие единственного «вида» ссылок - то, что работает, только если язык / структура не зависят от вопросов очистки ресурсов.
источник
Деструктор в C ++ фактически делает две вещи вместе взятыми. Это освобождает оперативную память и освобождает идентификаторы ресурсов.
Другие языки разделяют эти проблемы тем, что GC отвечает за освобождение ОЗУ, в то время как другая функция языка отвечает за освобождение идентификаторов ресурсов.
Вот что такое GC. Они делают только одну вещь, и это гарантирует, что вам не хватит памяти. Если ОЗУ бесконечно, все GC будут удалены, так как больше нет никакой реальной причины их существования.
Языки могут предоставлять различные способы освобождения идентификаторов ресурсов путем:
руководство
.CloseOrDispose()
разбросано по кодуруководство
.CloseOrDispose()
разбросано внутри ручного "finally
блока"ручные «блоки идентификаторов ресурсов» (т.е.
using
,with
,try
-с-ресурсы и т.д.) , который автоматизирует.CloseOrDispose()
после того , как блок сделангарантированный «блок идентификатора ресурса», который автоматизируется
.CloseOrDispose()
после того, как блок сделанМногие языки используют ручные (в отличие от гарантированных) механизмы, которые создают возможность для неэффективного управления ресурсами. Возьмите этот простой код NodeJS:
..где программист забыл закрыть открытый файл.
Пока программа продолжает работать, открытый файл будет зависать в подвешенном состоянии. Это легко проверить, попытавшись открыть файл с помощью HxD и убедившись, что это невозможно сделать:
Освобождение идентификаторов ресурсов в деструкторах C ++ также не гарантируется. Вы можете подумать, что RAII работает как гарантированные «блоки идентификаторов ресурсов», но в отличие от «блоков идентификаторов ресурсов», язык C ++ не останавливает утечку объекта, предоставляющего блок RAII , поэтому блок RAII никогда не может быть выполнен .
Потому что они управляют идентификаторами ресурсов, используя другие способы, как указано выше.
Потому что они управляют идентификаторами ресурсов, используя другие способы, как указано выше.
Потому что они управляют идентификаторами ресурсов, используя другие способы, как указано выше.
У Java нет деструкторов.
Документы Java упоминают :
... но размещение кода управления идентификатором ресурса в
Object.finalizer
значительной степени рассматривается как шаблон ( см. ). Этот код должен быть написан на сайте вызова.Для людей, которые используют анти-шаблон, их оправдание состоит в том, что они могли забыть опубликовать идентификаторы ресурсов на сайте вызовов. Таким образом, они делают это снова в финализаторе, на всякий случай.
Существует не так много вариантов использования для финализаторов, так как они используются для выполнения фрагмента кода между временем, когда больше нет сильных ссылок на объект, и временем, когда его память освобождается ГХ.
Возможный вариант использования - это когда вы хотите сохранить журнал времени между тем, как объект собран GC, и временем, когда больше нет сильных ссылок на объект, как таковое:
источник
нашел ссылку на это в Dr Dobbs wrt c ++, в которой есть более общие идеи, утверждающие, что деструкторы проблематичны в языке, где они реализованы. Похоже, грубая идея заключается в том, что главная цель деструкторов - справиться с освобождением памяти, и это трудно сделать правильно. память распределяется по кусочкам, но разные объекты связываются, и тогда ответственность / границы освобождения становятся не столь ясными.
поэтому решение для сборщика мусора было разработано несколько лет назад, но сборка мусора основана не на объектах, исчезающих из области видимости при выходе из метода (это концептуальная идея, которую сложно реализовать), а на сборщике, работающем периодически, несколько недетерминированно, когда приложение испытывает «давление памяти» (т.е. не хватает памяти).
другими словами, простое человеческое понятие «недавно неиспользованный объект» на самом деле является в некотором смысле вводящей в заблуждение абстракцией в том смысле, что ни один объект не может «мгновенно» стать неиспользованным. неиспользуемые объекты могут быть «обнаружены» только при запуске алгоритма сборки мусора, который пересекает граф ссылок на объекты, и наилучшие алгоритмы запускаются периодически.
возможно, существует лучший алгоритм сбора мусора, ожидающий обнаружения, который может почти мгновенно идентифицировать неиспользуемые объекты, что может затем привести к согласованному коду вызова деструктора, но он не был найден после многих лет исследований в этой области.
Похоже, что решение областей управления ресурсами, таких как файлы или соединения, состоит в том, чтобы иметь «менеджеров» объектов, которые пытаются справиться с их использованием.
источник