У даній статті я б не хотів загострювати увагу на принципі роботи збирача сміття - про це прекрасно і наочно описано тут: 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 для розташування об'єктів - тому що ця фаза займає в середньому більше часу, ніж фаза повного складання сміття при throughput GC. Як результат - може збільшитися тривалість гіршого випадку збірки сміття, необхідно уникати частих переповнень old generation. Як правило, - при переході на ConcMarkSweep GC рекомендують збільшити розмір old generation на 20-30% - запустимо jvm з параметрами:
Загальна пауза скоротилася до 1,923 с.
Коригування розміру 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 і процес цей, скоріше, творчий, ніж науковий.
Чим більше пам'яті виділено з додатком, тим краще працює прибирання сміття та тим краще досягаються кількісні характеристики по пропускній здатності і часу відгуку WUT? При збільшенні пам'яті можна отримати значне збільшення дисперсії (спрощено - розкиду) latency. При великій купі час повного складання (full gc) з s-t-w (wtop the world) паузою більше, що означає збільшення latency, якщо доходить до full gc.
Деякі товариші запускають jvm з величезним хіпом і періодично просто рестарту цілком, не чекаючи full gc.
З класики. У антипаттерн використання Apache Cassandra є прекрасна табличка, що показує до чого може призводити бездумне збільшення -Xms / Xmx.
www.datastax.com/documentation/cassandra/1.2/cassandra/architecture/architecturePlanningAntiPatterns_c.html
У нас на одному завданню при -Xmx від 12G до 16G на ноді Кассандри були моторошні гальма, високий latency, тайм-аути і втрати insert'ов, при 8G все стало працювати стабільно з нормальним потоком (6 нод по 500-600 MByte / s per node , на 2x SSD в RAID1).
В якійсь темі раніше згадував цей кейс, але там була неточність. Зазначений там потік (50 Mbit / s) ставився до вхідного потоку на нодах, до join'а зі словниками в пам'яті і деякої денормалізації даних з вихідного потоку.