Основні принципи налаштування Garbage Collection з нуля
У цій статті я не хотів би загострювати увагу на принципі роботи збирача сміття — про це чудово і наочно описано тут: habrahabr.ru/post/112676/. Хочеться більше перейти до практичних основ та кількісних характеристик з налаштування Garbage Collection в JVM і спробувати зрозуміти наскільки це може бути ефективним.
Кількісні характеристики оцінки ефективності GC
Розглянемо такі показники:
- Пропускна здатністьЗахід, що визначає здатність програми працювати в піковому навантаженні незалежно від пауз під час складання та розміру необхідної пам'яті
- Час відгукуЗахід GC, що визначає здатність програми справлятися з числом зупинок та флуктуацій роботи GC
- Розмір пам'ятіРозмір пам'яті, необхідний для ефективної роботи GC
Як правило, перелічені характеристики є компромісними та поліпшення однієї з них веде до витрат за іншими. Для більшості програм важливі всі три характеристики, але найчастіше одна або дві мають більше значення для програми — це буде відправною точкою в налаштуванні.
Основні принципи налаштування GC
Розглядають три основні фундаментальні правила щодо розуміння налаштування GC:
- Необхідно прагнути до того, щоб максимальна кількість об'єктів очищалася під час роботи малого GC(minor grabage collection). Цей принцип дозволяє зменшити кількість і частоту роботи повного збирача сміття (full garbage collection) - чия робота є основною причиною великих затримок у додатку
- Чим більше пам'яті виділено додатку, тим краще працює складання сміття і тим краще досягаються кількісні характеристики за пропускною здатністю та часом відгуку.
- Ефективноналаштувати можна лише 2 з 3 кількісних характеристик - пропускна спроможність, час відгуку, розмір виділеної пам'яті - під ефективним значенням розміру необхідної пам'яті розуміється її мінімізація
Запускати експеримент ми будемо на:
Для якого за замовчуванням увімкнено режим — server і UseParallelGC (багатопоточна робота фази малого складання сміття)
Для оцінки загальної величини паузи збирача сміття можна запускати в режимі:
І підсумовувати затримку за логом gc.log:
Де real = 0.01 secs - реальний час, витрачений на складання.
А можна скористатися утилітою VisualVm, із встановленим плагіном VisualGC, в якому наочно можна спостерігати розподіл пам'яті по різних областях GC(Eden, Survivor1, Survivor2, Old) та бачити статистику із запуску та тривалості складання сміття.
Визначення розміру пам'яті
Для початку ми повинні запустити програму з можливо більшим розміром пам'яті, ніж це реально необхідно додатку. Якщо ми не знаємо спочатку, скільки буде займати нашу програму в пам'яті - можна запустити програму без вказівки -Xmx і -Xms і HotSpot VM сама вибере розмір пам'яті. Якщо при старті програми ми отримаємо OutOfMemory (Java heap space або PermGen space), то ми можемо ітеративно збільшувати розмір доступної пам'яті (-Xmx або -XX: PermSize), поки помилки не підуть. Наступним кроком буде обчислення розміру живих даних, що довго живуть, — це розмір old і permanent областей купи після фази повного складання сміття. Цей розмір — зразковий обсяг пам'яті, необхідний для функціонування програми, для його отримання можна переглянути розмір областей після серії повного складання. Як правило розмір необхідної пам'яті для додатків -Xms та -Xmx у 3-4 рази більший, ніж обсяг живихданих. Так, для лога, зазначеного вище – величина old області після фази повного складання сміття – 349363K. Тоді запропоноване значення -Xmx та -Xms
1400 Мб. -XX:PermSize and -XX:MaxPermSize - в 1.5 разів більше, ніж PermGenSize після фази повного складання сміття - 13324K
20 Мб. Розмір young generation приймаю рівним 1-1.5 розміру об'єму живих даних
525 Мб. Тоді отримуємо рядок запуску jvm із такими параметрами:
У VisualVm отримуємо таку картину:
Усього за 30 секунд експерименту було зроблено 54 зборки — 31 малих і 23 повних — із загальним часом зупинки 3,227c. Ця величина затримки може задовольняти необхідним вимогам — подивимося, чи зможемо поліпшити ситуацію без зміни коду докладання.
Налаштування допустимого часу відгуку
Наступні параметри необхідно заміряти та враховувати під час налаштування часу відгуку:
- Вимірювання тривалості малого складання сміття
- Вимірювання частоти малого складання сміття
- Вимірювання тривалості найгіршого випадку повного складання сміття
- Вимірювання частоти найгіршого випадку повного складання сміття
Коригування розміру young та old generation
Час, необхідне реалізації фази малої складання сміття, безпосередньо залежить від кількості об'єктів у young generation, що менше його розмір — тим менше тривалість, але збільшується частота, т.к. область починає частіше заповнюватись. Спробуємо зменшити час кожного малого збирання, зменшивши розмір young generation, зберігши при цьому розмір old generation. Приблизно можна оцінити, що кожної секунди ми повинні очищати в young generation 50 потоків * 8 об'єктів * 1 Мб
400Мб. Запустимо з параметрами:
У VisualVm отримуємо таку картину:
На загальний час роботи малого складання сміття ми вплинути незмогли - 1,533с - збільшилася частота малих збірок, але загальний час погіршився - 3,661 через те, що збільшилася швидкість заповнення old generation і збільшилася частота повного складання сміття. Щоб подолати це – спробуємо збільшити розмір old generation – запустимо jvm із параметрами:
Загальна пауза тепер покращилася і становить 2,637 с, а загальне значення необхідної для додатку пам'яті при цьому зменшилося - таким чином ітеративно можна знайти правильний баланс між old і young generation для розподілу часу життя об'єктів у конкретному додатку.
Якщо час затримки, як і раніше, нас не влаштовує — можна перейти до concurrent garbage collector, включивши опцію -XX:+UseConcMarkSweepGC — алгоритм, який намагатиметься виконувати основну роботу з маркування об'єктів на видалення в окремому потоці паралельно потокам програми.
Налаштування Concurrent garbage collector
ConcMarkSweep GC вимагає більш уважного налаштування, - однією з основних цілей є зменшення кількості stop-the-world пауз за відсутності достатнього місця в old generation для розташування об'єктів - т.к. Ця фаза займає в середньому більше часу, ніж фаза повного складання сміття за допомогоювиходу GC. Як результат — може збільшитись тривалість найгіршого випадку складання сміття, необхідно уникати частих переповнень old generation. Як правило, при переході на ConcMarkSweep GC рекомендують збільшити розмір old generation на 20-30% - запустимо jvm з параметрами:
Загальна пауза скоротилася до 1923 с.
Коригування розміру survivor
Знизу під графіком ви бачите розподіл обсягу пам'яті програми за кількістю переходів між стадіями Eden, Survivor1 і Survivor2 перед тим, як вони потраплять до Old Generation. Справа в тому, що один із способівзменшення числа переповнень old generation у ConcMarkSweep GC - запобігти пряме перетікання об'єктів з young generation безпосередньо в old - минаючи survivor області.
Для стеження за розподілом об'єктів на етапах можна запустити jvm з параметром -XX:+PrintTenuringDistribution. У gc.log можемо спостерігати:
Загальний розмір об'єктів survivor — 40900584, CMS за замовчуванням використовує 50% бар'єр заповненості області survivor. Таким чином отримуємо розмір області
80 Мб. При запуску jvm він задається параметром -XX:SurvivorRatio, що визначається з формули:
Бажаючи залишити розмір eden space тим самим - отримуємо:

Розподіл став кращим, але загальний час сильно не змінився в силу специфіки програми, справа в тому, що після частих малих складання сміття розмір об'єктів, що вижили, завжди більше, ніж доступний розмір областей survivor, тому в нашому випадку ми можемо пожертвувати правильним розподілом на догоду розміру eden space:

В результаті ми зуміли скоротити розмір загальної паузи з 3,227 до 1,481 с на 30 з експерименту, трохи збільшивши при цьому загальне споживання пам'яті. Багато це чи мало — залежить від конкретної специфіки, зокрема, враховуючи тенденцію до зменшення вартості фізичної пам'яті та принцип максимізації пам'яті, що використовується — все одно важливо знайти баланс між різними областями GC і процес цей, швидше, творчий, ніж науковий.