Речі, які вам необхідно знати про прибиральник сміття в Python

9 хв. читання
28 листопада 2017

В цій статті описується прибиральник сміття (ПС) в Python 3.6.

Зазвичай вам не потрібно турбуватися про керування пам'яттю, коли об'єкти стають більше непотрібними, Python автоматично звільняє пам'ять від них. Проте, якщо у вас буде розуміння як працює прибиральник сміття, то ви зможете писати більш якісні програми на Python.

Керування пам'яттю

На відміну від інших мов програмування, Python не обов'язково повертає пам'ять до ОС. Замість цього, він має спеціальний розподільник для малих об'єктів (>= 512 байт), який утримує деякі частинки вже розподіленої пам'яті для подальшого використання в майбутньому. Кількість пам'яті, яку утримує Python, залежить від використаних патернів, а в деяких випадках вся розподілена пам'ять ніколи не звільняється.

Таким чином, якщо довготривалий Python процес використовує багато пам'яті, це не означає, що ви маєте витоки пам'яті.

Алгоритми збирання сміття

Стандартний прибиральник сміття в CPython має два компоненти: прибиральник з підрахунком посилань та прибиральник поколінь сміття, більш відомий як ПС модуль (gc module).

Алгоритм підрахунку посилань неймовірно простий та ефективний, але він не здатний виявляти цикли посилань. Саме тому Python має допоміжний алгоритм, що має назву прибиральник сміття циклічних поколінь і має справу з циклами посилань.

В той час коли підрахунок посилань є фундаментальним для Python і його неможливо вимкнути, циклічний прибиральник сміття є додатковим інструментом і може використовуватися вручну.

Підрахунок посилань

Підрахунок посилань – проста техніка, коли об'єкти звільняються, якщо в програмі більше немає посилань на них.

Кожна змінна в Python це посилання (вказівник) на об'єкт і не є його актуальним значенням. Для прикладу: присвоєння тільки додає нове посилання в правій частині.

Для слідкування за посиланнями, кожний об'єкт (навіть просто ціле число) має додаткове поле, що має назву лічильник посилань, який збільшується чи зменшується коли вказівник на об'єкт копіюється чи видаляється. Див. розділ «Об'єкти, типи та лічильник посилань» для більш детального пояснення.

Випадки, коли лічильник збільшується:

  • Оператор присвоєння
  • Передача аргументів
  • Включення об'єкту до списку

Якщо лічильник посилань досягає нуля, CPython автоматично викликає об'єкт-специфічну функцію звільнення пам'яті. Якщо об'єкт містить посилання до інших об'єктів, тоді їх лічильник посилань буде також зменшуватися. Таким чином інші об'єкти будуть звільняти пам'ять в процесі роботи програми. Для прикладу: коли список видаляється, то лічильник посилань для всіх елементів буде зменшуватися.

Змінні, задекларовані поза функцій, класів та блоків мають назву глобальні змінні. Зазвичай, такі змінні живуть до закінчення процесу Python. Тому лічильник посилань для таких об'єктів ніколи не досягне нуля.

Змінні задекларовані в блоках (у функціях, класах) мають локальну область видимості. Якщо інтерпретатор Python вийде з блоку, всі посилання створені всередині будуть знищені.

Ви завжди можете перевірити поточні посилання за допомогою функції sys.getrefcount.

Ось простий приклад:

foo = []

# 2 references, 1 from the foo var and 1 from getrefcount
print(sys.getrefcount(foo))

def bar(a):
    # 4 references
    # from the foo var, function argument, getrefcount and Python's function stack
    print(sys.getrefcount(a))

bar(foo)
# 2 references, the function scope is destroyed
print(sys.getrefcount(foo))

Основна причина чому CPython використовує підрахунок посилань – історична. Сьогодні точиться багато розмов про недоліки та слабкість такої техніки. Деякі вважають, що сучасні алгоритми прибирання сміття можуть бути більш ефективними без підрахунку посилань взагалі. Алгоритм підрахунку посилань має багато проблем: циклічні посилання, блокування потоків, перевантаження пам'яті та зниження продуктивності.

Основною перевагою для цього підходу може бути те, що об'єкти знищуються одразу після того, як вони вже більше не потрібні.

Прибиральник поколінь сміття

Для чого нам необхідно мати додатковий прибиральник сміття, коли в нас вже є підрахунок посилань?

На жаль, класичний підрахунок посилань має фундаментальну проблему – він не може виявляти циклічні посилання. Циклічні посилання виникають коли один чи більше об'єктів посилаються один на одного.

Ось два приклади:

Речі, які вам необхідно знати про прибиральник сміття в Python

Як ми бачимо, об'єкт «список» вказує сам на себе, крім того об'єкт 1 та об'єкт 2 вказують один на одного. Лічильник посилань для таких об'єктів завжди буде дорівнювати 1.

Щоб мати краще уявлення, ви можете пограти з простим прикладом на Python:

import gc

# We are using ctypes to access our unreachable objects by memory address.
class PyObject(ctypes.Structure):
    _fields_ = [("refcnt", ctypes.c_long)]


gc.disable()  # Disable generational gc

lst = []
lst.append(lst)

# Store address of the list
lst_address = id(lst)

# Destroy the lst reference
del lst

object_1 = {}
object_2 = {}
object_1['obj2'] = object_2
object_2['obj1'] = object_1

obj_address = id(object_1)

# Destroy references
del object_1, object_2

# Uncomment if you want to manually run garbage collection process 
# gc.collect()

# Check the reference count
print(PyObject.from_address(obj_address).refcnt)
print(PyObject.from_address(lst_address).refcnt)

В наведеному прикладі del вираз видаляє посилання на наші об'єкти (зменшує лічильник посилань на 1). Після виконання Python-ом виразу з del, наші об'єкти більше недоступні з коду. Проте, такі об'єкти все ж сидять у пам'яті, оскільки вони досі вказують один на одного, а лічильники посилань кожного об'єкту дорівнює 1. Ви можете візуально дослідити такі взаємозв'язки за допомогою objgraph модуля.

Щоб вирішити цю проблему, у версії Python 1.5 був запропонований спеціальний алгоритм виявлення циклічних посилань. GC модуль відповідає за це і використовується винятково тільки для вирішення цієї проблеми.

Циклічні посилання трапляються тільки в об'єктах контейнерах (тобто в таких об'єктах, що містять в собі інші об'єкти): списки, словники, класи, кортежі. GC не відслідковує незмінювані типи, крім кортежів. Кортежі та словники містять тільки незмінювані об'єкти, але також, при певних умовах, не можуть бути простежені. Таким чином, техніка підрахунку посилань працює тільки з нециклічними посиланнями.

Коли запускається прибиральник поколінь сміття

На відміну від лічильника посилань, циклічний прибиральник сміття не працює в реальному часі, він запускається час від часу. Для зменшення частоту викликів ПС та пауз CPython використовує спеціальні евристичні алгоритми.

Всі контейнери розподіляються на три покоління. Кожний новий об'єкт включається в перше покоління. Якщо об'єкт виживає в циклі прибирання сміття, то він переміщається до старішого (вищого) покоління. Нижчі покоління прибираються частіше ніж вищі. Через те, що новостворені об'єкти помирають молодими, покращується продуктивність та зменшуються паузи під час роботи ПС.

Періодичність запуску прибиральника для кожного покоління визначається за допомогою індивідуальних лічильників та порогів. Лічильник містить кількість розподіленої пам'яті для об'єктів мінус кількість звільненої пам'яті з часу останнього прибирання. Кожен раз коли вам необхідно виділити пам'ять для нового об'єкта-контейнера, CPython буде перевіряти, чи лічильник першого покоління досяг порогу. Якщо так, то процес прибирання буде ініційований.

Якщо ми маємо два і більше покоління, що на даний момент часу досягли порогу, буде обраний найстаріший для прибирання. Це відбувається тому, що старі покоління також вміщають в себе попередні (молодші) покоління. Для запобігання зниження продуктивності для довготривалих об'єктів третє покоління має додаткові вимоги для того щоб бути обраним для прибирання сміття.

Стандартні значення порогів встановлені до 700, 10, 10 відповідно, але ви завжди можете перевірити їх за допомогою функції gc.get_treshold.

Як знайти цикли посилань

Важко пояснити алгоритм виявлення циклів посилань в декількох абзацах. Але, якщо спрощено, то ПС проходить через кожний об'єкт-контейнер і тимчасово прибирає всі посилання до інших об'єктів. Після повної ітерації, всі об'єкти, у яких лічильник посилань менше за 2 і є недоступними з коду Python прибираються як сміття.

Для повного розуміння алгоритму виявлення циклічних посилань я рекомендую прочитати оригінальну пропозицію від Neil Schemenauer та функцію прибирання з вихідного коду CPython. Також стануть в нагоді пост про прибиральник сміття та відповіді Quora.

Поради для збільшення продуктивності

Цикли можуть траплятися в реальному житті. Зазвичай ви можете з ними зустрітися в графах, зв'язаних списках чи в структурах, де вам необхідно відслідковувати взаємозв'язки між об'єктами. Якщо ваша програма має інтенсивне робоче навантаження та потребує низьких затримок, то ви просто мусите уникати циклічних посилань.

Для уникнення циклічних посилань у вашому коді, вам необхідно використовувати слабкі посилання, які вже імплементовані в модулі weakref. На відміну від звичайних посилань, weakref.ref не збільшують лічильник посилань та повертає None якщо об'єкт був знищений.

В деяких випадках, було б корисно вимикати ПС та використовувати його вручну. Автоматичне прибирання може бути вимкнене за допомогою функції gc.disable(). Для ручного запуску процесу прибирання сміття необхідно використовувати функцію gc.collect().

Як знаходити та відлагоджувати циклічні посилання

Відлагодження циклічних посилань може бути дуже болісним, особливо якщо ви використовуєте third-party бібліотеки.

Стандартний gc модуль забезпечує низку корисних засобів для відлагодження. Якщо ви виставляєте прапорці відлагодження в DEBUG_SAVEALL, всі знайдені недоступні об'єкти будуть додані до переліку gc.garbage.

import gc

gc.set_debug(gc.DEBUG_SAVEALL)

print(gc.get_count())
lst = []
lst.append(lst)
list_id = id(lst)
del lst
gc.collect()
for item in gc.garbage:
    print(item)
    assert list_id == id(item)

Після того як ви виявили проблематичне місце у вашому коді, ви можете візуально дослідити взаємозв'язок об'єктів за допомогою функції objgraph.

Речі, які вам необхідно знати про прибиральник сміття в Python

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.8K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація