У даній статті я б не хотів загострювати увагу на принципі роботи збирача сміття - про це прекрасно і наочно описано тут: habrahabr.ru/post/112676/. Хочеться більше перейти до практичних основ і кількісним характеристикам по налаштуванню Garbage Collection в JVM - і спробувати зрозуміти наскільки це може бути еффектівним.Колічественние характеристики оцінки ефективності GCРассмотрім наступні показники: Пропускна здатність Міра, яка визначає здатність додатки працювати в піковому навантаженні не залежно від пауз під час збірки і розміру необхідної пам'яті Час відгуку Міра GC, що визначає здатність додатки справлятися з числом зупинок і флуктуацій роботи GC Розмір использу емой пам'яті Розмір пам'яті, який необхідний для ефективної роботи GC Як правило, перераховані характеристики є компромісними і поліпшення однієї з них веде до витрат по іншим. Для більшості додатків важливі всі три характеристики, але найчастіше одна або дві мають більше значення для додатка - це і буде відправною точкою в налаштуванні.
Основні принципи настройки GC Розглядають три основних фундаментальних правила по розумінню настройки GC:
import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;
public class MemoryConsumer implements Runnable
private static final int OBJECT_SIZE = 1024 × 1024; private static final int OBJECTS_NUMBER = 8; private static final int ADD_PROCESS_TIME = 1000; private static final int NUMBER_OF_REQUEST_THREADS = 50; private static final long EXPERIMENT_TIME = 30000; private static volatile boolean stop = false;
public static void main (String [] args) throws InterruptedException
start (); Thread.sleep (EXPERIMENT_TIME); stop ();>
private static void start () java -XX: + PrintGCTimeStamps -XX: + PrintGCDetails -verbose: gc -Xloggc: gc.log ru.skuptsov.MemoryConsumer І підсумовувати затримку по балці gc.log: 0.167: [Full GC [PSYoungGen: 21792K → 13324K (152896K)] [PSOldGen: 341095K → 349363K (349568K)] 362888K → 362687K (502464K) [PSPermGen: 2581K → 2581K (21248K)], 0.0079385 secs] [Times: user = 0.01 sys = 0.00, real = 0.01 secs] Де 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 з такими параметрами: java -Xms1400m -Xmx1400m -Xmn525m -XX: PermSize = 20m ru.skuptsov.MemoryConsumer В VisualVm отримуємо таку картину:
Всього за 30 сек експерименту було вироблено 54 збірки - 31 малих і 23 повних - із загальним часом зупинки 3,227c. Дана величина затримки може не задовольняти необхідним вимогам - подивимося, чи зможемо ми поліпшити ситуацію без зміни коду програми.
Налаштування допустимого часу відгуку Наступні параметри необхідно заміряти і враховувати при налаштуванні часу відгуку: Вимірювання тривалості малої збірки сміття Вимірювання частоти малої збірки сміття Вимірювання тривалості гіршого випадку повної збірки сміття Вимірювання частоти гіршого випадку повної збірки сміття Коригування розміру young і old generation Час, необхідний для здійснення фази малої збірки сміття, безпосередньо залежить від числа об'єктів в young generation, чим менше його розмір - тим менше тривалість, але при цьому зростає част та, тому що область починає частіше заповнюватися. Спробуємо зменшити час кожної малої збірки, зменшивши розмір young generation, зберігши при цьому розмір old generation. Приблизно можна оцінити, що кожну секунду ми повинні очищати в young generation 50потоков * 8об'ектов * 1 Мб
400Мб. Запустимо з параметрами: java -Xms1275m -Xmx1275m -Xmn400m -XX: PermSize = 20m ru.skuptsov.MemoryConsumer В VisualVm отримуємо таку картину:
На загальний час роботи малої збірки сміття ми вплинути не змогли - 1,533с - збільшилася частота малого складання, але загальний час погіршився - 3,661 через те, що збільшилася швидкість заповнення old generation і збільшилася частота виклику повної збірки сміття. Щоб побороти це - спробуємо збільшити розмір old generation - запустимо jvm з параметрами:
java -Xms1400m -Xmx1400m -Xmn400m -XX: PermSize = 20m ru.skuptsov.MemoryConsumer
Загальна пауза тепер покращилася і становить 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 з параметрами: java -Xms1680m -Xmx1680m -Xmn400m -XX: + UseConcMarkSweepGC -XX: PermSize = 20m ru.skuptsov.MemoryConsumer
Загальна пауза скоротилася до 1,923 с.
Коригування розміру survivor Знизу під графіком ви бачите розподіл обсягу пам'яті програми по числу переходів між стадіями Eden, Survivor1 і Survivor2 перед тим як вони потраплять в Old Generation. Справа в тому, що один із способів зменшення числа переповнень old generation в ConcMarkSweep GC - запобігти прямому перетікання об'єктів з young generation безпосередньо в old - минаючи survivor області.Для стеження за розподілом об'єктів по етапах можна запустити jvm з параметром -XX: + PrintTenuringDistribution. У gc.log можемо спостерігати:
Desired survivor size 20971520 bytes, new threshold 1 (max 4) - age 1: 40900584 bytes, 40900584 total Загальна розмір survivor об'єктів - 40900584, CMS за замовчуванням використовує 50% бар'єр заповнювання області survivor. Таким чином отримуємо розмір області
80 Мб. При запуску jvm він задається параметром -XX: SurvivorRatio, який визначається з формули: survivor space size = -Xmn / (- XX: SurvivorRatio = + 2) Отримуємо
java -Xms1680m -Xmx1680m -Xmn400m -XX: SurvivorRatio = 3 -XX: + UseConcMarkSweepGC -XX: PermSize = 20m ru.skuptsov.MemoryConsumer Бажаючи залишити розмір eden space тим же - отримуємо: java -Xms1760m -Xmx1760m -Xmn480m -XX: SurvivorRatio = 5 -XX: + UseConcMarkSweepGC -XX: PermSize = 20m ru.skuptsov.MemoryConsumer
Розподіл стало краще, але загальний час сильно не змінилося в силу специфіки додатку, справа в тому, що після частих малого складання сміття розмір вижили об'єктів завжди більше, ніж доступний розмір областей survivor, тому в нашому випадку ми можемо пожертвувати правильним розподілом на догоду розміру eden space:
java -Xms1760m -Xmx1760m -Xmn480m -XX: SurvivorRatio = 100 -XX: + UseConcMarkSweepGC -XX: PermSize = 20m ru.skuptsov.MemoryConsumer
Підсумок В результаті ми зуміли скоротити розмір загальної паузи з 3,227 с до 1,481 с на 30 з експерименту, трохи збільшивши при цьому загальне споживання пам'яті. Багато це чи мало - залежить від конкретної специфіки, зокрема, з огляду на тенденцію до зменшення вартості фізичної пам'яті і принцип максимізації використовуваної пам'яті - все одно важливо знайти баланс між різними областями GC і процес цей, скоріше, творчий, ніж науковий.