Стиснуті ООП в JVM

1. Огляд

JVM управляє пам’яттю для нас. Це знімає навантаження з управління пам’яттю з розробників, тому нам не потрібно маніпулювати вказівниками на об’єкти вручну , що, як доведено, займає багато часу та спричиняє помилки.

Під капотом JVM включає в себе безліч хитрих прийомів для оптимізації процесу управління пам'яттю. Однією хитрістю є використання стислих покажчиків , які ми збираємось оцінити в цій статті. Спочатку давайте подивимося, як JVM представляє об’єкти під час виконання.

2. Представлення об’єкта виконання

HotSpot JVM використовує структуру даних під назвою oop s або звичайні вказівники на об'єкти для представлення об'єктів. Ці файли еквівалентні власним вказівникам C. У instanceOop s є особливим видом ООП , яка представляє екземпляри об'єктів в Java . Більше того, JVM також підтримує кілька інших ой , які зберігаються у дереві джерела OpenJDK.

Давайте подивимося, як JVM розміщує instanceOop s в пам'яті.

2.1. Розмітка пам’яті об’єкта

Макет пам'яті instanceOop простий: це просто заголовок об’єкта, за яким слідують нуль або більше посилань на поля екземпляра.

Представлення JVM заголовка об’єкта складається з:

  • Слово одного знака служить багатьом цілям, таким як упереджене блокування , значення хешу ідентичності та GC . Це не oop, але з історичних причин він знаходиться в дереві джерела oop OpenJDK . Крім того, стан слова позначки містить лише uintptr_t, отже, його розмір варіюється від 4 до 8 байт у 32-розрядної та 64-розрядної архітектурах, відповідно.
  • Одне, можливо стиснене, слово Класа , яке представляє вказівник на метадані класу. До Java 7 вони вказували на Постійне Покоління , але починаючи з Java 8 і далі, вони вказують на Метапростір
  • 32-бітний розрив для забезпечення вирівнювання об’єкта. Це робить макет більш апаратним, як ми побачимо пізніше

Відразу після заголовка має бути нуль або більше посилань на поля екземпляра. У цьому випадку слово є рідним машинним словом, тож 32-біт на застарілих 32-бітах і 64-біт на більш сучасних системах.

Заголовок об’єкта масивів, крім слів mark і klass, містить 32-бітове слово, яке відображає його довжину.

2.2. Анатомія відходів

Припустимо, ми перейдемо від застарілої 32-розрядної архітектури до більш сучасної 64-розрядної машини. Спочатку ми можемо розраховувати на негайне підвищення продуктивності. Однак це не завжди так, коли задіяний JVM.

Головним винуватцем можливого погіршення продуктивності є 64-розрядні посилання на об'єкти. 64-розрядні посилання займають удвічі більше місця, ніж 32-розрядні посилання, тому це призводить до більшого споживання пам'яті загалом і частіших циклів GC. Чим більше часу приділяється циклам GC, тим менше фрагментів виконання процесора для наших потоків програм.

Отже, чи варто нам повернутися назад і знову використовувати ці 32-розрядні архітектури? Навіть якби це був варіант, ми не могли б мати більше 4 ГБ кучевого простору в 32-розрядних просторах процесів без трохи більше роботи.

3. Стиснуті ООП

Як виявляється, JVM може уникнути марнотрачення пам’яті, стискаючи вказівники на об’єкти або ой, так що ми можемо мати найкраще з обох світів: надаючи більше 4 ГБ кучи місця з 32-бітовими посиланнями на 64-бітних машинах!

3.1. Базова оптимізація

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

Оскільки JVM вже знає, що останні три біти завжди дорівнюють нулю, немає сенсу зберігати ці незначні нулі в купі. Натомість він припускає, що вони там є, і зберігає 3 інші більш значущі біти, які ми раніше не могли вмістити в 32-бітні. Тепер у нас є 32-розрядна адреса з 3 нулями, зміщеними праворуч, тому ми стискаємо 35-розрядний вказівник у 32-розрядний. Це означає, що ми можемо використовувати до 32 ГБ - 232 + 3 = 235 = 32 ГБ - простору купи без використання 64-розрядних посилань.

Для того, щоб зробити цю оптимізацію справною, коли JVM потрібно знайти об'єкт у пам'яті, він зміщує покажчик вліво на 3 біти (в основному додає ці 3 нулі назад до кінця). З іншого боку, під час завантаження покажчика на купу, JVM зміщує вказівник праворуч на 3 біти, щоб відкинути ті раніше додані нулі. В основному, JVM виконує трохи більше обчислень, щоб заощадити трохи місця. На щастя, зсув бітів - це справді тривіальна операція для більшості процесорів.

Для включення уп стиснення, ми можемо використовувати -XX: + UseCompressedOops настройка прапор. OOP стиснення за замовчуванням з Java 7 і далі щоразу , коли максимальний розмір купи менше , ніж 32 Гб. Коли максимальний розмір купи перевищує 32 ГБ, JVM автоматично вимкне стиснення oop . Таким чином, використанням пам'яті, що перевищує розмір купи 32 Гб, потрібно керувати по-різному.

3.2. Понад 32 ГБ

Також можна використовувати стислі вказівники, коли розмір купи Java перевищує 32 Гб. Хоча типове вирівнювання об'єкта за замовчуванням становить 8 байт, це значення можна налаштувати за допомогою прапорця налаштування -XX: ObjectAlignmentInBytes . Зазначене значення має дорівнювати двом і повинно знаходитися в межах 8 і 256 .

Ми можемо розрахувати максимально можливий розмір купи за допомогою стислих покажчиків наступним чином:

4 GB * ObjectAlignmentInBytes

Наприклад, коли вирівнювання об’єкта дорівнює 16 байтам, ми можемо використовувати до 64 ГБ кучевого простору зі стиснутими покажчиками.

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

3.3. Футуристичні ГК

ZGC, нове доповнення в Java 11, був експериментальним та масштабованим збирачем сміття з низькою затримкою.

Він може обробляти різні діапазони розмірів купи, зберігаючи паузи GC менше 10 мілісекунд. Оскільки ZGC повинен використовувати 64-розрядні кольорові вказівники, він не підтримує стиснені посилання . Отже, використовуючи GC з наднизькою затримкою, як ZGC, слід зважити, використовуючи більше пам'яті.

Починаючи з Java 15, ZGC підтримує покажчики стисненого класу, але все ще не має підтримки стиснених ООП.

Однак усі нові алгоритми GC не обмінюють пам'ять через низьку затримку. Наприклад, Shenandoah GC підтримує стиснуті посилання, крім того, що GC має низький час паузи.

Більше того, і Shenandoah, і ZGC завершені з Java 15.

4. Висновок

У цій статті ми описали проблему управління пам’яттю JVM у 64-розрядних архітектурах . Ми розглянули стислі вказівники та вирівнювання об’єктів , і ми побачили, як JVM може вирішити ці проблеми, дозволяючи нам використовувати більші розміри купи з менш марнотратними вказівниками та мінімум додаткових обчислень.

Для більш детального обговорення стислих посилань настійно рекомендуємо ознайомитись із ще одним чудовим твором Олексія Шипільова. Також, щоб побачити, як розподіл об’єктів працює в HotSpot JVM, перегляньте статтю «Розмітка об’єктів пам’яті в Java».