Компресирани ООП в JVM

1. Общ преглед

JVM управлява паметта за нас. Това премахва тежестта за управление на паметта от разработчиците, така че не е нужно да манипулираме ръчните указатели , което е доказано, че отнема много време и е склонно към грешки.

Под капака JVM включва много изящни трикове за оптимизиране на процеса на управление на паметта. Един трик е използването на компресирани указатели , които ще оценим в тази статия. Първо, нека видим как JVM представя обекти по време на изпълнение.

2. Представяне на обект по време на изпълнение

HotSpot JVM използва структура от данни, наречена oop s или обикновени указатели на обекти, за да представи обекти. Тези oops са еквивалентни на родните C указатели. На instanceOop те са особен вид обектно-ориентиран , който представлява случаите обект в Java . Нещо повече, JVM също поддържа няколко други oops, които се съхраняват в дървото на източника на OpenJDK.

Нека да видим как JVM поставя instanceOop s в паметта.

2.1. Оформление на паметта на обекта

Оформлението на паметта на instanceOop е просто: това е само заглавката на обекта, последвана веднага от нула или повече препратки към полетата на екземпляра.

Представянето на JVM на заглавка на обект се състои от:

  • Думата с една марка служи за много цели като Biased Locking , Identity Hash Values и GC . Това не е oop, но поради исторически причини се намира в дървото на изходния файл на oop на OpenJDK . Също така състоянието на думата марка съдържа само uintptr_t, следователно размерът му варира между 4 и 8 байта, съответно в 32-битова и 64-битова архитектура
  • Една, евентуално компресирана, класова дума , която представлява указател към метаданните на класа. Преди Java 7 те сочеха към Постоянното поколение , но от Java 8 нататък сочат към Метапространството
  • 32-битова пролука за налагане на подравняването на обекта. Това прави оформлението по-хардуерно, както ще видим по-късно

Веднага след заглавката трябва да има нула или повече препратки към полетата на екземпляра. В този случай думата е естествена машинна дума, така че 32-битова на стари 32-битови машини и 64-битова на по-модерни системи.

Заглавката на обекта на масиви, в допълнение към марки и класови думи, съдържа 32-битова дума, която представя нейната дължина.

2.2. Анатомия на отпадъците

Да предположим, че ще преминем от наследствена 32-битова архитектура към по-модерна 64-битова машина. Отначало може да очакваме незабавно повишаване на производителността. Това обаче не винаги е така, когато участва JVM.

Основният виновник за тази възможна деградация на производителността е 64-битовите препратки към обекти. 64-битовите препратки заемат два пъти повече пространство от 32-битовите препратки, така че това води до по-голяма консумация на памет като цяло и по-чести GC цикли. Колкото повече време е посветено на GC цикли, толкова по-малко срезове за изпълнение на процесора за нишките ни за приложения.

И така, трябва ли да се върнем обратно и да използваме тези 32-битови архитектури отново? Дори това да е опция, не бихме могли да разполагаме с повече от 4 GB пространство за купчина в 32-битови процесни пространства без малко повече работа.

3. Компресирани ООП

Както се оказва, JVM може да избегне загуба на памет чрез компресиране на указателите на обекта или oops, така че можем да имаме най-доброто от двата свята: позволявайки повече от 4 GB пространство за купчина с 32-битови препратки в 64-битови машини!

3.1. Основна оптимизация

Както видяхме по-рано, JVM добавя допълнение към обектите, така че размерът им да е кратен на 8 байта. С тези подложки последните три бита в oops винаги са нула. Това е така, защото числата, кратни на 8, винаги завършват на 000 в двоични.

Тъй като JVM вече знае, че последните три бита са винаги нула, няма смисъл да съхранявате тези незначителни нули в купчината. Вместо това предполага, че са там и съхранява 3 други по-значими бита, които не бихме могли да поберем в 32-битови преди. Сега имаме 32-битов адрес с 3 изместени вдясно нули, така че компресираме 35-битов указател в 32-битов. Това означава, че можем да използваме до 32 GB - 232 + 3 = 235 = 32 GB - пространство за купчина, без да използваме 64-битови препратки.

За да накара тази оптимизация да работи, когато JVM трябва да намери обект в паметта, той премества показалеца наляво с 3 бита (основно добавя тези 3 нули обратно в края). От друга страна, когато зарежда указател към купчината, JVM премества показалеца надясно с 3 бита, за да отхвърли добавените преди това нули. По принцип JVM извършва малко повече изчисления, за да спести малко място. За щастие, превключването на битове е наистина тривиална операция за повечето процесори.

За да активираме oop компресия, можем да използваме -XX: + UseCompressedOops флаг за настройка. В обектно-ориентиран компресията е поведението по подразбиране от Java 7 г. насам, когато размерът на максималната грамада е по-малко от 32 GB. Когато максималният размер на купчината е повече от 32 GB, JVM автоматично ще изключи oop компресията. Така че използването на паметта над 32 Gb размер на купчината трябва да се управлява по различен начин.

3.2. Над 32 GB

Възможно е също така да използвате компресирани указатели, когато размерите на Java купчината са по-големи от 32 GB. Въпреки че подравняването на обекта по подразбиране е 8 байта, тази стойност може да се конфигурира с помощта на флага за настройка -XX: ObjectAlignmentInBytes . Посочената стойност трябва да бъде степен два и трябва да бъде в диапазона 8 и 256 .

Можем да изчислим максимално възможния размер на купчината с компресирани указатели, както следва:

4 GB * ObjectAlignmentInBytes

Например, когато подравняването на обекта е 16 байта, можем да използваме до 64 GB пространство на купчина със компресирани указатели.

Моля, обърнете внимание, че с увеличаване на стойността на подравняването, неизползваното пространство между обектите също може да се увеличи. В резултат на това може да не осъзнаем никакви ползи от използването на компресирани указатели с големи размери на Java.

3.3. Футуристични GC

ZGC, ново допълнение в Java 11, беше експериментален и мащабируем колектор за боклук с ниска латентност.

Той може да обработва различни диапазони на размера на купчината, като същевременно поддържа GC паузи под 10 милисекунди. Тъй като ZGC трябва да използва 64-битови цветни указатели, той не поддържа компресирани препратки . Така че, като се използва ултра ниска латентност GC като ZGC, трябва да се прецени използването на повече памет.

От Java 15 ZGC поддържа указатели за компресиран клас, но все още липсва поддръжка за компресирани OOP.

Всички нови GC алгоритми обаче няма да обменят паметта за ниска латентност. Например, Shenandoah GC поддържа компресирани референции, освен че е GC с ниско време на пауза.

Освен това, както Shenandoah, така и ZGC са финализирани от Java 15.

4. Заключение

В тази статия описахме проблем с управлението на паметта на JVM в 64-битови архитектури . Разгледахме компресирани указатели и подравняване на обекти и видяхме как JVM може да се справи с тези проблеми, позволявайки ни да използваме по-големи размери на купчината с по-малко разточителни указатели и минимум допълнителни изчисления.

За по-подробна дискусия относно компресираните референции е силно препоръчително да разгледате още едно страхотно парче от Алексей Шипилев. Също така, за да видите как работи разпределението на обекти в HotSpot JVM, разгледайте статията „Разположение на паметта на обекти в Java“.