Рубрики
Без рубрики

gc – Сборщик мусора

Автор оригинала: Doug Hellmann.

Цель:

Управляет памятью, используемой объектами Python

gc раскрывает базовый механизм управления памятью Python, автоматический сборщик мусора. Модуль включает функции для управления работой сборщика и проверки объектов, известных системе, ожидающих сбора или застрявших в циклах обращения и не подлежащих освобождению.

Ссылки на трассировку

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

Например, get_referents () показывает объекты, на которые ссылаются входные аргументы.

gc_get_referents.py

import gc
import pprint


class Graph:

    def __init__(self, name):
        self.name  name
        self.next  None

    def set_next(self, next):
        print('Linking nodes {}.next = {}'.format(self, next))
        self.next  next

    def __repr__(self):
        return '{}({})'.format(
            self.__class__.__name__, self.name)


# Construct a graph cycle
one  Graph('one')
two  Graph('two')
three  Graph('three')
one.set_next(two)
two.set_next(three)
three.set_next(one)

print()
print('three refers to:')
for r in gc.get_referents(three):
    pprint.pprint(r)

В этом случае экземпляр Graph three содержит ссылки на свой словарь экземпляра (в атрибуте __dict__ ) и его класс.

$ python3 gc_get_referents.py

Linking nodes Graph(one).next = Graph(two)
Linking nodes Graph(two).next = Graph(three)
Linking nodes Graph(three).next = Graph(one)

three refers to:
{'name': 'three', 'next': Graph(one)}

В следующем примере используется Queue для выполнения обхода в ширину всех ссылок на объекты в поисках циклов. Элементы, вставленные в очередь, представляют собой кортежи, содержащие цепочку ссылок на данный момент и следующий объект для проверки. Он начинается с три и смотрит на все, к чему относится. Пропуск классов позволяет не смотреть на методы, модули и т. Д.

gc_get_referents_cycles.py

import gc
import pprint
import queue


class Graph:

    def __init__(self, name):
        self.name  name
        self.next  None

    def set_next(self, next):
        print('Linking nodes {}.next = {}'.format(self, next))
        self.next  next

    def __repr__(self):
        return '{}({})'.format(
            self.__class__.__name__, self.name)


# Construct a graph cycle
one  Graph('one')
two  Graph('two')
three  Graph('three')
one.set_next(two)
two.set_next(three)
three.set_next(one)

print()

seen  set()
to_process  queue.Queue()

# Start with an empty object chain and Graph three.
to_process.put(([], three))

# Look for cycles, building the object chain for each object
# found in the queue so the full cycle can be printed at the
# end.
while not to_process.empty():
    chain, next  to_process.get()
    chain  chain[:]
    chain.append(next)
    print('Examining:', repr(next))
    seen.add(id(next))
    for r in gc.get_referents(next):
        if isinstance(r, str) or isinstance(r, type):
            # Ignore strings and classes
            pass
        elif id(r) in seen:
            print()
            print('Found a cycle to {}:'.format(r))
            for i, link in enumerate(chain):
                print('  {}: '.format(i), end' ')
                pprint.pprint(link)
        else:
            to_process.put((chain, r))

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

$ python3 gc_get_referents_cycles.py

Linking nodes Graph(one).next = Graph(two)
Linking nodes Graph(two).next = Graph(three)
Linking nodes Graph(three).next = Graph(one)

Examining: Graph(three)
Examining: {'name': 'three', 'next': Graph(one)}
Examining: Graph(one)
Examining: {'name': 'one', 'next': Graph(two)}
Examining: Graph(two)
Examining: {'name': 'two', 'next': Graph(three)}

Found a cycle to Graph(three):
  0:  Graph(three)
  1:  {'name': 'three', 'next': Graph(one)}
  2:  Graph(one)
  3:  {'name': 'one', 'next': Graph(two)}
  4:  Graph(two)
  5:  {'name': 'two', 'next': Graph(three)}

Принудительный сбор мусора

Хотя сборщик мусора запускается автоматически, когда интерпретатор выполняет программу, его можно запустить в определенное время, когда нужно освободить много объектов или не выполняется много работы, и сборщик не повлияет на производительность приложения. Запуск сбора с помощью collect () .

gc_collect.py

import gc
import pprint


class Graph:

    def __init__(self, name):
        self.name  name
        self.next  None

    def set_next(self, next):
        print('Linking nodes {}.next = {}'.format(self, next))
        self.next  next

    def __repr__(self):
        return '{}({})'.format(
            self.__class__.__name__, self.name)


# Construct a graph cycle
one  Graph('one')
two  Graph('two')
three  Graph('three')
one.set_next(two)
two.set_next(three)
three.set_next(one)

# Remove references to the graph nodes in this module's namespace
one  two  three  None

# Show the effect of garbage collection
for i in range(2):
    print('\nCollecting {} ...'.format(i))
    n  gc.collect()
    print('Unreachable objects:', n)
    print('Remaining Garbage:', end' ')
    pprint.pprint(gc.garbage)

В этом примере цикл очищается, как только сбор выполняется в первый раз, поскольку ничто не относится к узлам Graph , кроме них самих. collect () возвращает количество найденных «недостижимых» объектов. В данном случае значение равно 6 , потому что есть три объекта с их словарями атрибутов экземпляров.

$ python3 gc_collect.py

Linking nodes Graph(one).next = Graph(two)
Linking nodes Graph(two).next = Graph(three)
Linking nodes Graph(three).next = Graph(one)

Collecting 0 ...
Unreachable objects: 6
Remaining Garbage: []

Collecting 1 ...
Unreachable objects: 0
Remaining Garbage: []

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

Искать объект, содержащий ссылку на другой объект, немного сложнее, чем увидеть, на что ссылается объект. Поскольку код, запрашивающий ссылку, сам должен содержать ссылку, некоторые источники перехода необходимо игнорировать. В этом примере создается цикл графа, затем он обрабатывает экземпляры Graph и удаляет ссылку в «родительском» узле.

gc_get_referrers.py

import gc
import pprint


class Graph:

    def __init__(self, name):
        self.name  name
        self.next  None

    def set_next(self, next):
        print('Linking nodes {}.next = {}'.format(self, next))
        self.next  next

    def __repr__(self):
        return '{}({})'.format(
            self.__class__.__name__, self.name)

    def __del__(self):
        print('{}.__del__()'.format(self))


# Construct a graph cycle
one  Graph('one')
two  Graph('two')
three  Graph('three')
one.set_next(two)
two.set_next(three)
three.set_next(one)

# Collecting now keeps the objects as uncollectable,
# but not garbage.
print()
print('Collecting...')
n  gc.collect()
print('Unreachable objects:', n)
print('Remaining Garbage:', end' ')
pprint.pprint(gc.garbage)

# Ignore references from local variables in this module, global
# variables, and from the garbage collector's bookkeeping.
REFERRERS_TO_IGNORE  [locals(), globals(), gc.garbage]


def find_referring_graphs(obj):
    print('Looking for references to {!r}'.format(obj))
    referrers  (r for r in gc.get_referrers(obj)
                 if r not in REFERRERS_TO_IGNORE)
    for ref in referrers:
        if isinstance(ref, Graph):
            # A graph node
            yield ref
        elif isinstance(ref, dict):
            # An instance or other namespace dictionary
            for parent in find_referring_graphs(ref):
                yield parent


# Look for objects that refer to the objects in the graph.
print()
print('Clearing referrers:')
for obj in [one, two, three]:
    for ref in find_referring_graphs(obj):
        print('Found referrer:', ref)
        ref.set_next(None)
        del ref  # remove reference so the node can be deleted
    del obj  # remove reference so the node can be deleted

# Clear references held by gc.garbage
print()
print('Clearing gc.garbage:')
del gc.garbage[:]

# Everything should have been freed this time
print()
print('Collecting...')
n  gc.collect()
print('Unreachable objects:', n)
print('Remaining Garbage:', end' ')
pprint.pprint(gc.garbage)

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

$ python3 gc_get_referrers.py

Linking nodes Graph(one).next = Graph(two)
Linking nodes Graph(two).next = Graph(three)
Linking nodes Graph(three).next = Graph(one)

Collecting...
Unreachable objects: 0
Remaining Garbage: []

Clearing referrers:
Looking for references to Graph(one)
Looking for references to {'name': 'three', 'next': Graph(one)}
Found referrer: Graph(three)
Linking nodes Graph(three).next = None
Looking for references to Graph(two)
Looking for references to {'name': 'one', 'next': Graph(two)}
Found referrer: Graph(one)
Linking nodes Graph(one).next = None
Looking for references to Graph(three)
Looking for references to {'name': 'two', 'next': Graph(three)}
Found referrer: Graph(two)
Linking nodes Graph(two).next = None

Clearing gc.garbage:

Collecting...
Unreachable objects: 0
Remaining Garbage: []
Graph(one).__del__()
Graph(two).__del__()
Graph(three).__del__()

Пороги сбора и поколения

Сборщик мусора поддерживает три списка объектов, которые он видит при запуске, по одному для каждого «поколения», отслеживаемого сборщиком. По мере того, как объекты исследуются в каждом поколении, они либо собираются, либо стареют в последующих поколениях, пока, наконец, не достигнут стадии, на которой они хранятся постоянно.

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

gc_get_threshold.py

import gc

print(gc.get_threshold())

Возвращаемое значение – кортеж с порогом для каждого поколения.

$ python3 gc_get_threshold.py

(700, 10, 10)

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

gc_threshold.py

import gc
import pprint
import sys

try:
    threshold  int(sys.argv[1])
except (IndexError, ValueError, TypeError):
    print('Missing or invalid threshold, using default')
    threshold  5


class MyObj:

    def __init__(self, name):
        self.name  name
        print('Created', self.name)


gc.set_debug(gc.DEBUG_STATS)

gc.set_threshold(threshold, 1, 1)
print('Thresholds:', gc.get_threshold())

print('Clear the collector by forcing a run')
gc.collect()
print()

print('Creating objects')
objs  []
for i in range(10):
    objs.append(MyObj(i))
print('Exiting')

# Turn off debugging
gc.set_debug(0)

Различные пороговые значения вводят очистку сборки мусора в разное время, показанное здесь, потому что отладка включена.

$ python3 -u gc_threshold.py 5

Thresholds: (5, 1, 1)
Clear the collector by forcing a run
gc: collecting generation 2...
gc: objects in each generation: 575 1279 4527
gc: objects in permanent generation: 0
gc: done, 0.0009s elapsed

Creating objects
gc: collecting generation 0...
gc: objects in each generation: 3 0 6137
gc: objects in permanent generation: 0
gc: done, 0.0000s elapsed
Created 0
Created 1
Created 2
gc: collecting generation 0...
gc: objects in each generation: 3 2 6137
gc: objects in permanent generation: 0
gc: done, 0.0000s elapsed
Created 3
Created 4
Created 5
gc: collecting generation 1...
gc: objects in each generation: 4 4 6137
gc: objects in permanent generation: 0
gc: done, 0.0000s elapsed
Created 6
Created 7
Created 8
gc: collecting generation 0...
gc: objects in each generation: 4 0 6144
gc: objects in permanent generation: 0
gc: done, 0.0000s elapsed
Created 9
Exiting

Меньший порог приводит к более частому запуску разверток.

$ python3 -u gc_threshold.py 2

Thresholds: (2, 1, 1)
Clear the collector by forcing a run
gc: collecting generation 2...
gc: objects in each generation: 575 1279 4527
gc: objects in permanent generation: 0
gc: done, 0.0026s elapsed
gc: collecting generation 0...
gc: objects in each generation: 1 0 6137
gc: objects in permanent generation: 0
gc: done, 0.0000s elapsed

Creating objects
gc: collecting generation 0...
gc: objects in each generation: 3 0 6137
gc: objects in permanent generation: 0
gc: done, 0.0000s elapsed
Created 0
gc: collecting generation 1...
gc: objects in each generation: 2 2 6137
gc: objects in permanent generation: 0
gc: done, 0.0000s elapsed
Created 1
Created 2
gc: collecting generation 0...
gc: objects in each generation: 2 0 6140
gc: objects in permanent generation: 0
gc: done, 0.0000s elapsed
Created 3
gc: collecting generation 0...
gc: objects in each generation: 3 1 6140
gc: objects in permanent generation: 0
gc: done, 0.0000s elapsed
Created 4
Created 5
gc: collecting generation 1...
gc: objects in each generation: 2 3 6140
gc: objects in permanent generation: 0
gc: done, 0.0000s elapsed
Created 6
gc: collecting generation 0...
gc: objects in each generation: 3 0 6144
gc: objects in permanent generation: 0
gc: done, 0.0000s elapsed
Created 7
Created 8
gc: collecting generation 0...
gc: objects in each generation: 2 2 6144
gc: objects in permanent generation: 0
gc: done, 0.0000s elapsed
Created 9
Exiting

Отладка

Отладка утечек памяти может быть сложной задачей. gc включает несколько параметров, позволяющих раскрыть внутреннюю работу и облегчить работу. Параметры представляют собой битовые флаги, предназначенные для объединения и передачи в set_debug () для настройки сборщика мусора во время работы программы. Информация об отладке печатается в sys.stderr .

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

gc_debug_stats.py

import gc

gc.set_debug(gc.DEBUG_STATS)

gc.collect()
print('Exiting')

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

$ python3 gc_debug_stats.py

gc: collecting generation 2...
gc: objects in each generation: 826 471 4529
gc: objects in permanent generation: 0
gc: done, 24 unreachable, 0 uncollectable, 0.0007s elapsed
Exiting
gc: collecting generation 2...
gc: objects in each generation: 1 0 5552
gc: objects in permanent generation: 0
gc: done, 0.0005s elapsed
gc: collecting generation 2...
gc: objects in each generation: 107 0 5382
gc: objects in permanent generation: 0
gc: done, 1406 unreachable, 0 uncollectable, 0.0008s elapsed
gc: collecting generation 2...
gc: objects in each generation: 0 0 3307
gc: objects in permanent generation: 0
gc: done, 151 unreachable, 0 uncollectable, 0.0002s elapsed

Включение DEBUG_COLLECTABLE и DEBUG_UNCOLLECTABLE заставляет сборщик сообщать о том, можно или нельзя собрать каждый исследуемый им объект. Если просмотра объектов, которые не могут быть собраны, недостаточно для понимания того, где хранятся данные, включите DEBUG_SAVEALL , чтобы gc сохранял все найденные объекты без каких-либо ссылок в Список мусора .

gc_debug_saveall.py

import gc

flags  (gc.DEBUG_COLLECTABLE |
         gc.DEBUG_UNCOLLECTABLE |
         gc.DEBUG_SAVEALL
         )

gc.set_debug(flags)


class Graph:

    def __init__(self, name):
        self.name  name
        self.next  None

    def set_next(self, next):
        self.next  next

    def __repr__(self):
        return '{}({})'.format(
            self.__class__.__name__, self.name)


class CleanupGraph(Graph):

    def __del__(self):
        print('{}.__del__()'.format(self))


# Construct a graph cycle
one  Graph('one')
two  Graph('two')
one.set_next(two)
two.set_next(one)

# Construct another node that stands on its own
three  CleanupGraph('three')

# Construct a graph cycle with a finalizer
four  CleanupGraph('four')
five  CleanupGraph('five')
four.set_next(five)
five.set_next(four)

# Remove references to the graph nodes in this module's namespace
one  two  three  four  five  None

# Force a sweep
print('Collecting')
gc.collect()
print('Done')

# Report on what was left
for o in gc.garbage:
    if isinstance(o, Graph):
        print('Retained: {} 0x{:x}'.format(o, id(o)))

# Reset the debug flags before exiting to avoid dumping a lot
# of extra information and making the example output more
# confusing.
gc.set_debug(0)

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

$ python3 -u gc_debug_saveall.py

CleanupGraph(three).__del__()
Collecting
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
CleanupGraph(four).__del__()
CleanupGraph(five).__del__()
Done
Retained: Graph(one) 0x109771828
Retained: Graph(two) 0x10979f320
Retained: CleanupGraph(four) 0x1097af4e0
Retained: CleanupGraph(five) 0x10983eb70

Для простоты DEBUG_LEAK определяется как комбинация всех других параметров.

gc_debug_leak.py

import gc

flags  gc.DEBUG_LEAK

gc.set_debug(flags)


class Graph:

    def __init__(self, name):
        self.name  name
        self.next  None

    def set_next(self, next):
        self.next  next

    def __repr__(self):
        return '{}({})'.format(
            self.__class__.__name__, self.name)


class CleanupGraph(Graph):

    def __del__(self):
        print('{}.__del__()'.format(self))


# Construct a graph cycle
one  Graph('one')
two  Graph('two')
one.set_next(two)
two.set_next(one)

# Construct another node that stands on its own
three  CleanupGraph('three')

# Construct a graph cycle with a finalizer
four  CleanupGraph('four')
five  CleanupGraph('five')
four.set_next(five)
five.set_next(four)

# Remove references to the graph nodes in this module's namespace
one  two  three  four  five  None

# Force a sweep
print('Collecting')
gc.collect()
print('Done')

# Report on what was left
for o in gc.garbage:
    if isinstance(o, Graph):
        print('Retained: {} 0x{:x}'.format(o, id(o)))

# Reset the debug flags before exiting to avoid dumping a lot
# of extra information and making the example output more
# confusing.
gc.set_debug(0)

Имейте в виду, что, поскольку DEBUG_SAVEALL включен с помощью DEBUG_LEAK , даже те объекты, на которые нет ссылок, которые обычно были бы собраны и удалены, сохраняются.

$ python3 -u gc_debug_leak.py

CleanupGraph(three).__del__()
Collecting
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
gc: collectable 
CleanupGraph(four).__del__()
CleanupGraph(five).__del__()
Done
Retained: Graph(one) 0x10be10828
Retained: Graph(two) 0x10be3e320
Retained: CleanupGraph(four) 0x10be4e4e0
Retained: CleanupGraph(five) 0x10beddb70

Смотрите также