0,0
рейтинг
17 января 2013 в 14:58

Разработка → Опции JVM. Как это работает

JAVA*
С каждым днем слово java все больше и больше воспринимается уже не как язык, а как платформа благодаря небезызвестному invokeDynamic. Именно поэтому сегодня я бы хотел поговорить про виртуальную java машину, а именно — об так называемых Performance опциях в Oracle HotSpot JVM версии 1.6 и выше (server). Потому что сегодня почти не встретить людей, которые знают что-то больше чем -Xmx, -Xms и -Xss. В свое время, когда я начал углубляться в тему, то обнаружил огромное количество интересной информации, которой и хочу поделится. Отправной точкой, понятное дело, послужила официальная документация от Oracle. А дальше — гугл, эксперименты и общение:

-XX:+DoEscapeAnalysis


Начну, пожалуй, с самой интересной опции — DoEscapeAnalysis. Как многие из Вас знают, примитивы и ссылки на объекты создаются не в куче, а выделяются на стеке потока (256КБ по умолчанию для Hotspot). Вполне очевидно, что язык java не позволяет создавать объекты на стеке на прямую. Но это вполне себе может проделывать Ваша JVM 1.6 начиная с 14 апдейта.

Про то, как работает сам алгоритм можно прочитать тут (PDF). Если коротко, то:

  • Если область видимости объекта не выходит за область метода, в котором он создается, то такой объект может быть создан на фрейме стека вместо кучи (на самом деле не сам объект, а его поля, на совокупность которых заменяется объект);
  • Если объект не покидает область видимости потока, то к такому объекту другие потоки не имеют доступа и следовательно все операции синхронизации над объектом могут быть удалены.


Для реализации данного алгоритма строится и используется так называемый — граф связей (connection graph), по которому на этапе анализа (алгоритмов анализа — несколько) осуществляется проход для нахождения пересечений с другими потоками и методами.
Таким образом после прохода графа связей для любого объекта возможно одно из следующих следующих состояний:

  • GlobalEscape — объект доступен из других потоков и из других методов, например статическое поле.
  • ArgEscape — объект был передан как аргумент или на него есть ссылка из объекта аргумента, но сам он не выходит из области видимости потока в котором был создан.
  • NoEscape — объект не покидает область видимости метода и его создание может быть вынесено на стек.


После этапа анализа, уже сама JVM проводит возможную оптимизацию: в случае если объект NoEscape, то он может быть создан на стеке; если объект NoEscape или ArgEscape, то операции синхронизации над ним могут быть удалены.

Следует уточнить, что на стеке создается не сам объект а его поля. Так как JVM заменяет цельный объект на совокупность его полей (спасибо Walrus за уточнение).

Вполне очевидно, что благодаря такого рода анализу, производительность отдельных частей программы может возрасти в разы. В синтетических тестах, на подобии этого:

    for (int i = 0; i < 1000*1000*1000; i++) {
        Foo foo = new Foo();
    }

скорость выполнения может увеличится в 8-15 раз. Хотя, на казалось бы, очевидных случаях из практики о которых недавно писалось (тут и тут) EscapeAnalys не работает. Подозреваю, что это связано с размером стека.

Кстати, EscapeAnalysis как раз частично ответственен за известный спор про StringBuilder и StringBuffer. То есть, если Вы вдруг в методе использовали StringBuffer вместо StringBuilder, то EscapeAnalysis (в случае срабатывания) устранит блокировки для StringBuffer'а, после чего StringBuffer вполне превращается в StringBuilder.

-XX:+AggressiveOpts


Опция AggressiveOpts является супер опцией. Не в том плане, что она резко увеличивает производительность Вашего приложения, а в том смысле, что она всего лишь изменяет значения других опций (на самом деле, это не совсем так — в исходном коде JDK довольно не мало мест, где AggressiveOpts изменяет поведение JVM, помимо упомянутых опций, один из примеров тут). Проверять измененные флаги будем с помощью двух команд:

java -server -XX:-AggressiveOpts -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal > no_aggr
java -server -XX:+AggressiveOpts -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal > aggr

После выполнения разница в результатах выполнения команд выглядела так:
-AggressiveOpts
+AggressiveOpts
AutoBoxCacheMax
128
20000
BiasedLockingStartupDelay
4000
500
EliminateAutoBox
false
true
OptimizeFill
false
true
OptimizeStringConcat
false
true

Иными словами, все что делает эта опция — изменяет 5 данных параметров виртуальной машины. Причём, для версий 1.6 update 35 и 1.7 update 7 никаких отличий замечено не было. Данная опция по умолчанию отключена и в клиентском моде ничего не изменяет.
Расcмотрим, что же java подразумевает под агрессивной оптимизацией:

-XX:AutoBoxCacheMax=size


Позволяет расширить диапазон кешируемых значений для целых типов при старте виртуальной машины. Эту опцию я уже упоминал тут (второй абзац).

-XX:BiasedLockingStartupDelay=delay


Как известно, synchronized блок в java может быть представлен одним из 3-х видов блокировок:

  • biased
  • thin
  • fat

Подробней про это можно прочитать тут, тут и тут.

Так как большинство объектов (синхронизированных) блокируются максимум 1 одним потоком, то такие объекты могут быть привязаны (biased) к этому потоку и операции синхронизации над этим объектом внутри потока сильно удешевляются. Если к biased объекту пытается получить доступ другой поток, то происходит переключение блокировки для этого объекта на thin блокировку.

Само переключение относительно дорого, поэтому на старте JVM существует задержка, которая по умолчанию создает все блокировки как thin и если никакой конкуренции не обнаружено и код используется одним и тем же потоком, то такие блокировки, после истечения задержки, становятся biased. То есть, JVM пытается на старте определить сценарии использования блокировок и соответственно использует меньше переключений между ними. Соответственно, выставляя BiasedLockingStartupDelay в ноль, мы рассчитаем на то, что основные куски кода синхронизации будут использоваться лишь одни и тем же потоком.

-XX:+OptimizeStringConcat


Тоже довольно интересная опция. Распознает паттерн на подобии

StringBuilder().append(...).toString()
//или рекурсивного вида
StringBuilder().append(new StringBuiler().append(...).toString()).toString()

и вместо постоянного выделения памяти под новую операцию конкатенации, идет попытка вычислить общее количество символов каждого объекта конкатенации для выделения памяти только 1 раз.
Иными словами, если мы вызовем 20 раз операцию append() для строки длинной 20 символов. То создание массива char произойдет один раз и длиной 400 символов.

XX:+OptimizeFill


Циклы заполнения/копирования массивов заменяются на прямые машинные инструкции для ускорения работы.
Например, следующий блок (взято из Arrays.fill()):

        for (int i=fromIndex; i<toIndex; i++)
            a[i] = val;

будет полностью замен на соответствующие процессорные инструкции на подобии сишных memset, memcpy только более низкоуровневых.

XX:+EliminateAutoBox


Исходя из названия, флаг должен как-то уменьшать количество операций автобоксинга. К сожалению, я до конца так и не смог выяснить, что же делает этот флаг. Единственное, что удалось прояснить, что применяется это только к Integer оболочкам.

-XX:+UseCompressedStrings


Довольно спорная опция по моему убеждению… Если в далеких 90-х разработчики java не пожалели 2 байта на символ, то сегодня такая оптимизация смотрится довольно нелепо. Если кто не догадался, то опция заменяет в строках символьные массивы на байтовые, где это возможно (ASCII). По сути:

char[] -> byte[]             

Таким образом возможна существенная экономия памяти. Но в виду того, что меняется тип, появляются накладные расходы на контроль типов при определенных операциях. То есть, с этой опцией возможна деградация производительности JVM. Собственно поэтому опция по умолчанию и отключена.

-XX:+UseStringCache


Довольно загадочная опция, судя по названию она должна каким-то образом кешировать строки. Как? Не понятно. Информации нету. Да и по коду похоже она ничего не выполняет. Буду рад, если кто-то сможет прояснить.

-XX:+UseCompressedOops


Для начала несколько фактов:

  • Размер указателя на объект в 32-х разрядной JVM составляет 32 бита. В 64-х разрядной — 64 бита. Следовательно, в первом случае Вы можете использовать адресное пространство размером 2^32 байт (4 ГБ), а во втором случае 2^64 байт.
  • Размер объектов в java кратен 8 байтам не зависимо от разрядности виртуальной машины (это не для всех виртуальных машин правда, но речь о Hotspot). То есть, при использовании 32-х разрядных указателей последние 3 бита будут всегда нулями, фактически, виртуальная машина реально использует лишь 29 бит.


Данная опция позволяет уменьшить размер указателя для 64-х разрядных JVM до 32-х бит, но в этом случае размер кучи ограничен 4 ГБ, поэтому, в дополнение к сокращенному указателю, используется свойство о кратности 8 байтам. В результате получаем возможность использовать адресное пространство размером 2^35 байт (32 ГБ) имея указатели в 32 бита.
Фактически, внутри виртуальной машины, мы имеем указатели на объекты, а не конкретные байты в памяти. Понятное дело, что из-за подобных допущений (о кратности) появляются дополнительные расходы на преобразование указателей. Но по сути это всего лишь одна операция сдвига и суммирования.

Помимо уменьшения размеров самих указателей, эта опция уменьшает также заголовки объектов и разного рода выравнивания и сдвиги внутри созданных объектов, что позволяет в среднем уменьшить потребление памяти на 20-60% в зависимости от модели приложения.

То есть, из недостатков имеем лишь:

  • Максимальный размер кучи ограничен 32 ГБ (64ГБ для JRockit при кратности объектов 16 байтам);
  • Появляются доп. расходы на преобразование JVM ссылок в нативные и обратно.


Так как для большинства приложений опция несет одни плюсы, то начиная с JDK 6 update 23 она включена по умолчанию, так же как и в JDK 7. Детальней тут и тут.

-XX:+EliminateLocks


Опция, которая устраняет лишние блокировки путем их объединения. Например следующие блоки:

synchronized (object) {
    //doSomething1
}

synchronized (object) {
    //doSomething2
}

synchronized (object) {
    //doSomething3
}

//doSomething4

synchronized (object) {
    //doSomething5
}

будут преобразованы соответственно в

synchronized (object) {
    //doSomething1
    //doSomething2
}


synchronized (object) {
    //doSomething3
    //doSomething4
    //doSomething5
}


Таким образом сокращается количество попыток захвата монитора.

Заключение

За бортом осталось довольно много интересных опций, так как поместить все ~700 флагов в одну статью довольно трудно. Я специально не затрагивал опции по тюнингу сборщика, так это довольно обширная и сложная тема и она заслуживает нескольких постов. Надеюсь статья была вам полезной.
Дмитрий Думанский @doom369
карма
85,5
рейтинг 0,0
Co-Founder в Blynk
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (17)

  • +3
    Появляются доп. расходы на преобразование JVM ссылок в нативные и обратно.

    Есть информация, что потери на преобразование ссылок, компенсируются тем, что за счёт уменьшения размеров объектов, больше данных помещается в кешах процессора, т.е. в некоторых случаях получаем не потерю, а прирост производительности.
    • –2
      Заодно выравнивание получается немного по-лучше. 64-битная JVM отвратительно работает с выравниванием (не экономно и многие стандартные библиотеки написаны с учётом выравнивания 32-бита)/
      • 0
        На Хабре были статьи про выравнивание в Java. Там очень показательно было, что на x64 стандартные объекты реpко растут в размере из-за изменения выравнивания указателей.
    • 0
      Да, все верно. На разного рода тестах при переходе на сжатые указатели прирост работы JVM составляет 2-10%.
  • 0
    -XX:+UseStringCache
    Все константные строки JVM сначала будет пытаться найти в динамическом кэше. В результате одинаковые по содержимому строки из разных классов (модулей) будут указывать на один и тот же объект String. Экономит время на выделении памяти под объект и его инициализации, за счёт времени поиска при инициализации строки. Было в одном бородатых Release Notes. В принципе имеет смысл.
    • 0
      По флагу UseStringCache я нашел только это, строка 3009
      • 0
        Дык,. там именно это и делается — если включен StriingCache, то строка ищется в системном словаре. Если строка найдена, то используется она. Если строка не найдена или кэш отключен, то происходит обычное выделение памяти и инициализация.
  • +6
    Как все запущено. :(
    Escape Analysis НЕ размещает объекты на «стеке вместо кучи». Escape Analysis — это анализ, а не оптимизация. Он вообще ничего оптимизирует, а только проводит анализ кода и предоставляет полученную информацию другим частям HotSpot'а которые и делают оптимизацию. Это первое.
    Второе — размещения объектов на стеке нет. Вообще, такая оптимизация возможна и даже есть альтернативные JVM в которых это реализовано. Вместо этого HotSpot содержит другую оптимизацию, которая называется Scalar replacement. То есть если объект локален (не убегает), то его можно заменить на россыпь локалов (которые, при сотворении кода девелопером, были полями этого объекта). То есть вместо объекта (композитной сущности ) — получаем кучку скаляров. Это и есть Scalar Replacement.
    Ключика включать Scalar Replacement нет. Он всегда включен, просто если выключить Escape анализ — то для него не будет информации и он ничего не сделает.
    • 0
      В конце-концов можно же считать, что эти локалы размещаются на стеке?
      • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      По первому пункту — абсолютно согласен, я и нигде не говорил, что оптимизацию делает EscapeAnalysis.

      По второму — спасибо за замечание, подправил. Я не правильно понял термин Scalar replacement.
  • +1
    Интересная статься. Некоторые опции достаточно экзотичны, не слышал о них, с трудом представляю зачем может, например, понадобится сжатие строк. По производительности в Jave, есть хорошая книга, дает представление со всех сторон:
    www.amazon.com/Java-Performance-Charlie-Hunt/dp/0137142528
  • 0
    Оставлю на всякий случай список опций, который часто может поднять производительность в разы:

    Xmx2048M -Xms2048M -XX:ParallelGCThreads=8 -Xincgc -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSIncrementalPacing -XX:+AggressiveOpts -XX:+CMSParallelRemarkEnabled -XX:+DisableExplicitGC -XX:MaxGCPauseMillis=500 -XX:SurvivorRatio=16 -XX:TargetSurvivorRatio=90 -XX:+UseAdaptiveGCBoundary -XX:-UseGCOverheadLimit -Xnoclassgc -XX:UseSSE=3 -XX:PermSize=128m -XX:LargePageSizeInBytes=4m

    В своё время помогли поднять производительность сервера майнкрафт, а затем и самописной КИС в tomcat на 200-800% (в некоторых местах и больше). Само собой, xms/xmx/gcghreads надо тюнить, но там всё легко. А вот как работают некоторые остальные опции — для меня загадка :)
    • +1
      -XX:+UseConcMarkSweepGC и -XX:+UseAdaptiveGCBoundary вместе не работают. последний только в Parallel GC используется.
    • 0
      Список хорош для того, чтобы пойти гуглить опции. И выйти на статьи, описывающие регионы памяти hotspot, варианты и особенности сборки мусора, способы мониторинга.

      А так похоже на теорию заговора: существуют секретные опции HotSpot, ускоряющие её в разы. Oracle скрывает.
  • 0
    А что за invokeDynamic? Первый раз слышу об этом. Объясните, пожалуйста, на пальцах, что это и зачем.
    • 0
      В статье есть ссылка. Самая первая. Я думаю лучше будет по ней прочитать, чем читать из 3-х рук.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.