Поиск…


замечания

По своей сути сборщик мусора Python (начиная с версии 3.5) является простой реализацией подсчета ссылок. Каждый раз, когда вы делаете ссылку на объект (например, a = myobject ), счетчик ссылок на этот объект (myobject) увеличивается. Каждый раз, когда ссылка удаляется, счетчик ссылок уменьшается, и как только счетчик ссылок достигает 0 , мы знаем, что ничто не содержит ссылку на этот объект, и мы можем его освободить!

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

Python агрессивно создает или очищает объекты в первый раз, когда они нуждаются в них. Если я выполняю назначение a = object (), в это время выделяется память для объекта (cpython иногда повторно использует определенные типы объектов, например списки под капотом, но в основном он не поддерживает пул свободных объектов и будет выполнять выделение, когда вам это нужно). Аналогично, как только refcount будет уменьшен до 0, GC очистит его.

Сбор урожая мусора

В 1960-х годах Джон МакКарти обнаружил фатальный недостаток в пересчете сбора мусора, когда он реализовал алгоритм refcounting, используемый Lisp: что произойдет, если два объекта ссылаются друг на друга в циклической ссылке? Как вы можете мусор собирать эти два объекта, даже если нет внешних ссылок на них, если они всегда будут ссылаться на друг друга? Эта проблема также распространяется на любую циклическую структуру данных, такую ​​как кольцевые буферы или любые две последовательные записи в двусвязном списке. Python пытается исправить эту проблему, используя слегка интересный поворот в другом алгоритме сбора мусора под названием Generational Garbage Collection .

По сути, каждый раз, когда вы создаете объект в Python, он добавляет его в конец дважды связанного списка. Иногда Python перебирает этот список, проверяет, к каким объектам относятся объекты в списке, и если они также находятся в списке (мы увидим, почему они могут не быть в данный момент), далее уменьшает их пересчеты. На данный момент (на самом деле, есть некоторые эвристики, которые определяют, когда все перемещается, но давайте предположим, что после того, как одна коллекция будет держать вещи простыми) все, что по-прежнему содержит refcount больше 0, получает продвижение в другой связанный список под названием «Generation 1», (поэтому все объекты не всегда находятся в списке 0 поколения), который с этим циклом применялся к нему реже. Это происходит, когда входит сборщик мусора поколения. По умолчанию в Python есть три поколения: три связанных списка объектов: первый список (поколение 0) содержит все новые объекты; если происходит цикл GC и объекты не собираются, они перемещаются во второй список (генерация 1), и если во втором списке происходит цикл GC, и они все еще не собраны, они перемещаются в третий список (генерация 2 ). Список третьего поколения (называемый «поколение 2», поскольку мы ноль-индексирование) - это сбор мусора гораздо реже, чем первые два, идея состоит в том, что если ваш объект долговечен, он вряд ли будет GCed и может никогда быть GCed в течение всего срока службы вашего приложения, поэтому нет смысла тратить время на его проверку при каждом запуске GC. Кроме того, отмечается, что большинство объектов собирают мусор относительно быстро. С этого момента мы будем называть эти «хорошие объекты», так как они умирают молодыми. Это называется «слабой гипотезой поколений», а также впервые наблюдалось в 60-х годах.

Быстрая сторона: в отличие от первых двух поколений, долговечный список третьего поколения - это не сбор мусора в обычном расписании. Он проверяется, когда отношение долгоживущих ожидающих объектов (те, которые находятся в списке третьего поколения, но еще не имеют GC-цикла), к общим долгоживущим объектам в списке больше 25%. Это связано с тем, что третий список неограничен (вещи никогда не перемещаются из него в другой список, поэтому они просто уходят, когда на самом деле собираются мусор), что означает, что для приложений, где вы создаете много долгоживущих объектов, циклы GC в третьем списке может затянуться. Используя соотношение, мы достигаем «амортизированная линейная производительность в общем количестве объектов»; aka, чем длиннее список, тем длиннее GC, но тем реже мы исполняем GC (вот оригинальное предложение 2008 года для этой эвристики Мартина фон Лёвиса для дальнейшего чтения). Акт выполнения сбора мусора в третьем поколении или «зрелом» списке называется «полной сборкой мусора».

Таким образом, сборщик мусора поколения ускоряет работу, не требуя, чтобы мы просматривали объекты, которые вряд ли нуждаются в GC все время, но как это помогает нам нарушать циклические ссылки? Наверное, не очень хорошо, оказывается. Функция фактического нарушения этих эталонных циклов начинается следующим образом :

/* Break reference cycles by clearing the containers involved.  This is
 * tricky business as the lists can be changing and we don't know which
 * objects may be freed.  It is possible I screwed something up here.
 */
static void
delete_garbage(PyGC_Head *collectable, PyGC_Head *old)

Причина, по которой сборщик мусора поколения помогает в этом, заключается в том, что мы можем сохранить длину списка как отдельный счетчик; каждый раз, когда мы добавляем новый объект к генерации, мы увеличиваем этот счет, и в любое время, когда мы перемещаем объект в другое поколение или dealloc, мы уменьшаем счет. Теоретически в конце цикла GC этот счет (для первых двух поколений в любом случае) всегда должен быть 0. Если это не так, все, что осталось в списке, является некоторой формой циклической ссылки, и мы можем ее отбросить. Однако есть еще одна проблема: что, если у оставшихся объектов есть магический метод Python __del__ на них? __del__ вызывается в любое время, когда объект Python уничтожается. Однако, если два объекта в круговой ссылке имеют методы __del__ , мы не можем быть уверены, что уничтожение одного из них не нарушит другие __del__ . Для надуманного примера предположим, что мы написали следующее:

class A(object):
    def __init__(self, b=None):
        self.b = b
 
    def __del__(self):
        print("We're deleting an instance of A containing:", self.b)
     
class B(object):
    def __init__(self, a=None):
        self.a = a
 
    def __del__(self):
        print("We're deleting an instance of B containing:", self.a)

и мы устанавливаем экземпляр A и экземпляр B, чтобы указать друг на друга, а затем они попадают в тот же цикл сбора мусора? Скажем, мы выбираем один случайным образом и исключаем наш экземпляр A в первую очередь; Будет __del__ метод __del__ A, он будет распечатан, тогда A будет освобожден. Затем мы приходим к B, мы называем его __del__ методом, и oops! Segfault! Больше не существует. Мы могли бы исправить это, вызвав все, что осталось от __del__ методов, а затем __del__ другой проход, чтобы фактически удалить все, однако это вводит другой вопрос: что, если один объект __del__ метод сохраняет ссылку на другой объект, который должен быть GCed и имеет ссылку на нас в другом месте? У нас все еще есть ссылочный цикл, но теперь невозможно вообще GC либо объект, даже если они больше не используются. Обратите внимание, что даже если объект не является частью круговой структуры данных, он может восстановить себя в своем собственном методе __del__ ; У Python есть проверка на это и остановка GCing, если refcount объектов увеличился после __del__ метода __del__ .

CPython справляется с этим, приклеивая эти объекты, не содержащие GC (что-либо с некоторой формой циклической ссылки и метод __del__ ), в глобальный список бесполезного мусора, а затем оставляя его там на всю вечность:

/* list of uncollectable objects */
static PyObject *garbage = NULL;

Подсчет ссылок

Подавляющее большинство управления памятью Python обрабатывается с подсчетом ссылок.

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

Когда счетчик ссылок достигает нуля, объект немедленно уничтожается и память немедленно освобождается. Таким образом, для большинства случаев сборщик мусора даже не нужен.

>>> import gc; gc.disable()  # disable garbage collector
>>> class Track:
        def __init__(self):
            print("Initialized")
        def __del__(self):
            print("Destructed")
>>> def foo():
        Track()
        # destructed immediately since no longer has any references
        print("---")
        t = Track()
        # variable is referenced, so it's not destructed yet
        print("---")
        # variable is destructed when function exits
>>> foo()
Initialized
Destructed
---
Initialized
---
Destructed

Чтобы еще раз продемонстрировать концепцию ссылок:

>>> def bar():
        return Track()
>>> t = bar()
Initialized
>>> another_t = t  # assign another reference
>>> print("...")
...
>>> t = None          # not destructed yet - another_t still refers to it
>>> another_t = None  # final reference gone, object is destructed
Destructed

Сборщик мусора для ссылочных циклов

Единственный раз, когда сборщик мусора нужен, если у вас есть эталонный цикл . Пример примера ссылочного цикла - это тот, в котором A относится к B и B, относится к A, тогда как ничто иное не относится ни к A, ни к B. Ни A, ни B не доступны из любой точки программы, поэтому их можно безопасно уничтожить, но их подсчеты ссылок равны 1 и поэтому они не могут быть освобождены только алгоритмом подсчета ссылок.

>>> import gc; gc.disable()  # disable garbage collector
>>> class Track:
        def __init__(self):
            print("Initialized")
        def __del__(self):
            print("Destructed")
>>> A = Track()
Initialized
>>> B = Track()
Initialized
>>> A.other = B
>>> B.other = A
>>> del A; del B  # objects are not destructed due to reference cycle
>>> gc.collect()  # trigger collection
Destructed
Destructed
4

Обратный цикл может быть произвольным долго. Если A указывает на B, то точка C указывает на ... указывает на Z, который указывает на A, тогда ни от A до Z не будет собрано, пока фаза сбора мусора:

>>> objs = [Track() for _ in range(10)]
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
>>> for i in range(len(objs)-1):
...     objs[i].other = objs[i + 1]
...
>>> objs[-1].other = objs[0]  # complete the cycle
>>> del objs                  # no one can refer to objs now - still not destructed
>>> gc.collect()
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
20

Эффекты команды del

Удаление имени переменной из области с помощью del v или удаление объекта из коллекции с помощью del v[item] или del[i:j] или удаление атрибута с помощью del v.name или любой другой способ удаления ссылок на объект, не вызывает вызовы деструктора или любую свободную память и сам по себе. Объекты уничтожаются только тогда, когда их количество ссылок достигает нуля.

>>> import gc
>>> gc.disable()  # disable garbage collector
>>> class Track:
        def __init__(self):
            print("Initialized")
        def __del__(self):
            print("Destructed")
>>> def bar():
    return Track()
>>> t = bar()
Initialized
>>> another_t = t  # assign another reference
>>> print("...")
...
>>> del t          # not destructed yet - another_t still refers to it
>>> del another_t  # final reference gone, object is destructed
Destructed

Повторное использование примитивных объектов

Интересно отметить, что может помочь оптимизировать ваши приложения, так это то, что примитивы фактически также пересчитываются под капот. Давайте посмотрим на цифры; для всех целых чисел от -5 до 256, Python всегда использует один и тот же объект:

>>> import sys
>>> sys.getrefcount(1)
797
>>> a = 1
>>> b = 1
>>> sys.getrefcount(1)
799

Обратите внимание, что пересчет увеличивается, что означает, что a и b ссылаются на один и тот же базовый объект, когда они ссылаются на 1 примитив. Однако для больших чисел Python фактически не использует повторно базовый объект:

>>> a = 999999999
>>> sys.getrefcount(999999999)
3
>>> b = 999999999
>>> sys.getrefcount(999999999)
3

Поскольку refcount для 999999999 не изменяется при назначении ему a и b мы можем заключить, что они относятся к двум различным базовым объектам, даже если им присваивается один и тот же примитив.

Просмотр пересчета объекта

>>> import sys
>>> a = object()
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> del b
>>> sys.getrefcount(a)
2

Сильно освобождение объектов

Вы можете принудительно освободить объекты, даже если их refcount не равен 0 в обоих Python 2 и 3.

Обе версии используют модуль ctypes для этого.

ПРЕДУПРЕЖДЕНИЕ: это сделает вашу среду Python нестабильной и подверженной сбою без следа! Использование этого метода также может привести к проблемам безопасности (весьма маловероятно). Только освободите объекты, которые, как вы уверены, никогда больше не будете ссылаться. Когда-либо.

Python 3.x 3.0
import ctypes
deallocated = 12345
ctypes.pythonapi._Py_Dealloc(ctypes.py_object(deallocated))
Python 2.x 2.3
import ctypes, sys
deallocated = 12345
(ctypes.c_char * sys.getsizeof(deallocated)).from_address(id(deallocated))[:4] = '\x00' * 4

После запуска любая ссылка на теперь освобожденный объект приведет к тому, что Python либо произведет неопределенное поведение, либо сбой - без обратной трассировки. Вероятно, была причина, почему сборщик мусора не удалял этот объект ...

Если вы освободите None , вы получите специальное сообщение - Fatal Python error: deallocating None перед сбоем.

Управление сборкой мусора

Существует два подхода к влиянию при выполнении очистки памяти. Они влияют на то, как часто выполняется автоматический процесс, а другой вручную запускает очистку.

Сборщик мусора можно манипулировать путем настройки порогов сбора, которые влияют на частоту, с которой работает сборщик. Python использует систему управления памятью на основе поколений. Новые объекты сохраняются в новейшем поколении - generation0 и с каждой сохранившейся коллекцией, объекты продвигаются в более старые поколения. Достигнув последнего поколения - generation2 , они больше не продвигаются.

Пороги могут быть изменены с помощью следующего фрагмента:

import gc
gc.set_threshold(1000, 100, 10) # Values are just for demonstration purpose

Первый аргумент представляет собой пороговое значение для сбора генерации 0 . Каждый раз, когда количество распределений превышает количество освобождений на 1000, будет вызываться сборщик мусора.

Предыдущие поколения не очищаются при каждом запуске, чтобы оптимизировать процесс. Второй и третий аргументы являются необязательными и контролируют частоту очистки старых поколений. Если генерация 0 обрабатывалась 100 раз без чистки поколения1 , то генерация 1 будет обрабатываться. Точно так же объекты в генерации 2 будут обрабатываться только тогда, когда те, что были в генерации 1, были очищены 10 раз, не касаясь поколения2 .

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

Вручную запуск коллекции можно выполнить в следующем фрагменте:

import gc
gc.collect()

Сбор мусора автоматически запускается на основе количества распределений и освобождений, а не от потребленной или доступной памяти. Следовательно, при работе с большими объектами память может истощиться до запуска автоматической очистки. Это делает хороший вариант использования для ручного вызова сборщика мусора.

Хотя это возможно, это не рекомендуется. Лучше всего избегать утечки памяти. Во всяком случае, в больших проектах обнаружение утечки памяти может быть задачей, и ручное инициирование сбора мусора может быть использовано в качестве быстрого решения до дальнейшей отладки.

Для долгосрочных программ сбор мусора может запускаться в зависимости от времени или на основе событий. Примером первого является веб-сервер, который запускает коллекцию после определенного количества запросов. Для более позднего веб-сервера, который запускает сборку мусора при получении определенного типа запроса.

Не ждите, пока сбор мусора будет очищен

Тот факт, что сбор мусора будет очищен, не означает, что вы должны дождаться очистки цикла сбора мусора.

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

например:

В следующем коде вы предполагаете, что файл будет закрыт в следующем цикле сбора мусора, если f была последней ссылкой на файл.

>>> f = open("test.txt")
>>> del f

Более явный способ очистки - это вызвать f.close() . Вы можете сделать это еще более изящным, то есть с помощью оператора with , также известного как менеджер контекста :

>>> with open("test.txt") as f:
...     pass
...     # do something with f
>>> #now the f object still exists, but it is closed

Оператор with позволяет отступывать код под открытым файлом. Это делает его явным и легче понять, как долго файл остается открытым. Он также всегда закрывает файл, даже если в блоке while создается исключение.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow