Принципи роботи Garbage collection

У цій статті пригадаємо, що таке Garbage collection (GC), навіщо він потрібен взагалі і які проблеми вирішує. Детально розглянемо режими роботи GC в .NET, зрозуміємо, як працює кожен з них, їх особливості та відмінності. Торкнемося специфіку застосування деяких режимів GC в .NET.

Вивчимо питання моніторингу роботи GC, які доступні для цього інструменти і як ними користуватися.

Введення

Взагалі, звідки взялася ця тема? Вона з'явилася з-за поведінки наших сервісів, в тому числі і на production. Ми побачили, що деякі програми почали забирати 30% CPU. Не могли зрозуміти, чому це відбувається — адже за кодом все було добре. Провели аналіз метрик, про які поговоримо пізніше, і з'ясували, що GC споживає на збірку сміття близько 30%. І тут виникло питання — що ж з цим робити. З'явилося поле для оптимізації. І ми домоглися хороших результатів, коли після всіляких маніпуляцій знизили споживання CPU до 10% до 5%. Як цього можна досягти, я розповім нижче.

Коли я задався питанням і почав готувати цю статтю, мені було цікаво, а коли у нас з'явився перший мову, який вже підтримував збирання сміття. Я навіть трохи здивувався, тому що це був 1964 рік. 50 років тому люди вже замислювалися про те, що розробникам слід звільняти від занять з пам'яттю. Це була мова APL. З мов, які підтримують збірку сміття, можна назвати Erlang (1990 рік), Eifel, Smalltalk (1972 рік), звичайно ж, C# і будь-яка сучасна мова, яка виходить зараз, наприклад Go. Це вже must have.

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

Що таке Garbage Collection

GC (Garbage Collection — збірка сміття) — високорівнева абстракція, яка позбавляє розробників від необхідності піклуватися про звільнення керованої пам'яті.

Давайте згадаємо основні тези по збірці сміття. В .NET збірка сміття заснована на трасуванні.

Існує поняття кореневих елементів програми. Кореневим елементом(root) називається осередок у пам'яті, в якій міститься посилання на розміщується в купі об'єкт. Строго кажучи, кореневими можуть називатися такі елементи:

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

Фази збирання сміття:

  1. Маркування (mark phase).
  2. Чистка (sweep phase).
  3. Стиснення (compact phase).

Покоління об'єктів: нульовий, перший, друге покоління.

Нульове і перше покоління ще називають ефемерними поколіннями. Вони потрібні для прискорення реакції нашої програми.

Для роботи програми CLR ініціалізує 2 сегмента віртуального адресного простору — Small object heap (об'єкти до 85 КБ) і Large object heap (об'єкти понад 85 КБ, в деяких випадках масиви та зв'язані списки (linked list), що не досягли цього розміру).

Конфігурування GC досить просте, що відображено на рисунку:

Малюнок 1. App.config

Конфігурувати режими роботи GC можна шляхом додавання в app.config секції, показаної на слайді вище, з допомогою параметрів gcConcurrent, gcServer.

Режим робочої станції

Малюнок 2. Процес складання сміття в режимі робочої станції

Якщо ми відкриємо будь-яку книгу .NET, будь-яку статтю .NET, де у нас описано, як працює Garbage Collection, зазвичай це звучить так: додаток працює, не вистачає пам'яті для того, щоб виділити наступний об'єкт, і відбувається запуск GC. При цьому всі потоки програми призупиняються. Це найпростіший процес збирання сміття — workstation non-concurrent mode.

Недоліком цього підходу є те, що під час збирання сміття додаток не займається нічим іншим, крім сміття. Чи можна цього уникнути і як-то підвищити відгук нашого додатка?

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

Паралельна збірка сміття

Малюнок 3. Паралельна збірка сміття

Для цього існує режим паралельної сміття (workstation concurrent GC).

Паралельна збірка сміття .NET 1.0–3.5

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

По завершенні циклу збірки сміття призупиненим потоків знову дозволялося продовжити роботу. На щастя, в .NET 3.5 збирач сміття був добре оптимізований, і тому пов'язані з ним короткі перерви в роботі з додатком рідко ставали помітними.

Як і оптимізація, паралельна збірка сміття дозволяла проводити очищення об'єктів, які не були виявлені в жодному з ефемерних поколінь, в окремому потоці. Це скорочувало (але не усувало) необхідність призупинення активних потоків виконуючою середовищем .NET. Тим більше, паралельна збірка сміття дозволяла розміщувати об'єкти в купі під час складання об'єктів неэфемерных поколінь.

Фонова збірка сміття

Малюнок 4. Фонова збірка сміття

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

Механізм складання сміття .NET 4.0 був поліпшений так, щоб на призупинення потоку, пов'язаного з деталями збору сміття, було потрібно менше часу. Завдяки цим змінам процес очищення невикористовуваних об'єктів покоління 0 і 1 став оптимальним. Він дозволяє отримувати більш високий рівень продуктивності додатків.

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

Режим сервера

Особливості роботи GC в режимі сервера.

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

Малюнок 6. Візуалізація роботи Garbage Collection в режимі сервера

На малюнку 6 показано візуалізація того, як все це працює в режимі сервера. Як бачимо, головна відмінність полягає в тому, що збірка сміття виконується для кожного доступного процесора.

Малюнок 7. Server Background Mode

Починаючи з .NET Framework 4.5, фонова збірка сміття сервера є типовим режимом для збирання сміття сервера. Цей режим функціонує аналогічно фонової збірці сміття робочої станції, описаної вище, проте з деякими відмінностями. Для фонової сміття робочої станції використовується один виділений потік фонової сміття, тоді як для фонової сміття сервера використовується кілька потоків — зазвичай по одному виділеному потоку для кожного логічного процесора.

Інструменти моніторингу

GC class

Що можна зробити з допомогу GC class з коду докладно описано в статті , але варто відразу відзначити, що це буде просто логування потрібної нам інформації в лог, а потім аналіз цієї інформації з допомогою будь-яких доступних засобів. Не дуже хороший спосіб — це не вихід із ситуації.

Performance Monitor

Одним з найбільш потужних інструментів для виявлення проблем з продуктивністю в Windows є вбудовані лічильники продуктивності, так звані Performance counters. Оснащення Performance monitor — основний інструмент для управління ними.

Performance Перегляду

Performance Viewer заснований на відслідковування подій Windows. Трохи пізніше поговоримо про те, що це таке, навіщо це потрібно і що можна взагалі моніторити з його допомогою.

SOS Debugging Extension

SOS Debugging Extension варто відзначити, але вже мало хто використовує цей інструмент.

dotMemory

Платний представник від JetBrains. Варто відзначити, що його open source конкуренти на поточний момент мало в чому йому поступаються.

Concurrency Visualizer

Concurrency Visualizer — розширення для Visual Studio. До моніторингу пам'яті відноситься дуже опосередковано. При цьому воно дуже інформативне, так як дозволяє побачити безліч параметрів роботи програми в многопоточной середовищі. За допомогою цієї утиліти можна проаналізувати, коли потоки припиняються, відновлюють свою роботу і т. д.

Performance Monitor

Малюнок 8. Лічильники Performance Monitor

Які лічильники (counter) пропонує Performance Monitor? Перший лічильник, на який варто звернути увагу — це відсоток часу, який було витрачено самим GC. Цей лічильник робить виміри між двома збірками сміття, вважає цикли процесора, цикли, які були витрачені в загальному і які були витрачені на складання сміття. Наприклад, якщо між двома збірками пройшов 1 мільйон циклів процесора і при цьому з них 300 тисяч витрачено на прибирання сміття, то, відповідно, наше додаток 30% часу витрачає просто для того, щоб збирати сміття.

На яку величину потрібно звертати увагу? Це досить складне питання. Наприклад, ми отримали цифру 17. Що мені з цією цифрою робити далі? З досвіду рекомендую звертати увагу на значення 50%. Якщо 50% — значить половину часу ми витрачаємо даремно. Якщо цей час витрачається ще в дата-центрах, то витрачаються гроші. І з цим треба щось робити. Якщо ми бачимо цифру в 10 %, то для того, щоб опустити її на 5, треба витратити стільки грошей, що навіть не варто в це вкладати.

Наступний параметр, на який варто звертати увагу — Allocated bytes/second. Він показує кількість байт у секунду, які ми можемо аллоцировать в пам'яті. Можемо подивитися, який розмір займає нульове покоління, перше, друге покоління, скільки займає Large Object Heap, як перетікають об'єкти з нульового покоління в перше, з першого — по друге, кількість тих, що вижили об'єктів і т. д.

Finalization Survivors — це лічильник, який показує кількість об'єктів, які пішли з черги фіналізації та готові до того, щоб почалася їх чищення.

Приклад, як використовувати цей інструмент, показаний на малюнку 9.

Малюнок 9. Робота з Performance Monitor

Performance Перегляду

На мій погляд, це один із кращих інструментів на поточний момент. Також радує, що виробники вже почали замислюватися про те, що ж робити з Linux, що дуже актуально для додатків, написаних під .NET Core. Вже зараз на їх сайті є невеликий туторіал, як знімати метрики з докер хостів. Сподіваюся, вони будуть продовжувати розвиватися, і ми отримаємо дуже хороший інструмент.

Інструмент дозволяє моніторити практично всі аспекти, які потрібні розробнику для аналізу: CPU, стек, є можливість зробити дамп пам'яті і проаналізувати його, можна подивитися статистику по GC.

Малюнок 10. Робота з Performance Перегляду

Інструмент досить простий (див. малюнок 10): натискаємо collect і збираємо потрібні нам метрики. Порада для тих, хто буде використовувати — не збирайте метрики довго. Зробив велику помилку: зібрав метрики за хвилину і потім чекав поки распарсится хвилин сім, потім кинув. Повинно вистачити 5-10 секунд, щоб зрозуміти, що відбувається з вашим додатком і що з ним робити. Інструмент може показати все, що пов'язано з GC. На слайді виділені дані, які стосуються GC-статистики. Показується режим GC, в якому запущено наш додаток, час, який було витрачено на паузи GC, процесорний час, який було витрачено, кількість збірок сміття в кожному з поколінь, мінімальні паузи, піки.

Події трасування

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

Що взагалі можна моніторити в .NET в середовищі CLR? GC, Runtime, Exceptions, Thread pool, Stack і т. д. Детально про всіх метриках можна почитати тут .

Зараз ми розглянемо Garbage Collection в події (event), і що вони нам дозволяють моніторити. Вони дозволяють нам збирати відомості, які як раз і відносяться до збирання сміття: коли вона почалася, коли закінчилася, в якому поколінні. Як довго тривала не покажуть — потрібно обчислювати, і це нетривіальне завдання. Нетривіальна тому, що якщо ми подивимося на режим робочої станції, коли у нас немає ніяких конкурентних режимів, то там все просто: потоки зупинилися, призупинилися, поновилися. І цю дельту ми можемо зловити по різниці. Коли ми згадуємо высокоприоритетную збірку сміття, то тут вже все далеко не тривіально. Тому вже краще користуватися тими інструментами, які у нас є.

На GitHub є бібліотеки, які дозволяють навчитися працювати з даними подіями. Приміром, TraceEvent Library дозволяє нам написати програму, яка буде виконувати трасування іншого додатка. І всю цю інформацію спокійно збирати, дебажити і щось з нею робити.

На малюнку 11 показаний невеликий приклад, як можна запустити трасування подій використовуючи TraceEvent Library.

Малюнок 11. Приклад коду

На малюнку 12 відбувається магія в частині того, як ми збираємо всі ці лічильники. А ось що з усього цього вийшло вже відображено на рисунку 13.

Малюнок 12. Приклад коду

Ми отримали наступну інформацію: коли у нас почав виконуватися GC і скільки часу зайняла пауза на GC, яка за рахунком збірка сміття, з яким поколінням працював GC, в якому режимі працює наша програма.

GC-візуалізація

Малюнок 13. Візуалізація GC

Є досить цікавий блог , який веде Мет Уоррен. В ньому можна знайти дуже багато цікавої та корисної інформації: як працює Garbage Collection, що ж відбувається насправді «під капотом».

Малюнок 14. Візуалізація GC від Мета Уоррена

На малюнку 14 відображена візуалізація роботи GC, заснована на відслідковування подій, написана автором блогу. Всім, кому цікаво зрозуміти, як же працює GC, рекомендую розібратися з ним.

Малюнок 15. Таблиця даних у різних режимах тестування

У наступній таблиці зібрані метрики, отримані в результаті тестування одного і того ж додатка в різних режимах. Було запущено програму, основним завданням якого була генерація memory-трафіку. Що ми бачимо? Серверний режим, дійсно, зменшує паузи роботи GC, зменшує кількість запусків ітерацій сміття, але це все робиться за рахунок більш інтенсивного використання CPU і за рахунок більш інтенсивного споживання пам'яті. Про це завжди потрібно пам'ятати. Якщо у нас десктопное додаток, в якому нам потрібен максимальний відгук, то цей режим явно не для нього.

Висновки

Кожен з нас рано чи пізно стикається з проблемами неоптимальною роботи написаного програми, причини можуть бути різні. При їх аналізі досить часто ми не дивимося на те, як в таких випадках працює GC, як його робота впливає на роботу додатка, оптимальний режим GC обрано саме для поточного додатка. А адже багато відповідей якраз і можуть бути отримані при такому аналізі.

Корисні посилання

Опубліковано: 30/11/18 @ 11:00
Розділ Блоги

Рекомендуємо:

Що робить бізнес-аналітик на discovery-фазі: аналіз потреб клієнта
Масове видалення пунктів меню в WordPress
Візуалізація даних у роботі аналітика: типи діаграм і яку вибрати
DOU Hobby: рух Zero waste — океан без пластику і життя без мотлоху
Як я працюю: Богдан Гусєв, CTO Talkable