С каждым днем слово java все больше и больше воспринимается уже не как язык, а как платформа благодаря небезызвестному invokeDynamic. Именно поэтому сегодня я бы хотел поговорить про виртуальную java машину, а именно — об так называемых Performance опциях в Oracle HotSpot JVM версии 1.6 и выше (server). Потому что сегодня почти не встретить людей, которые знают что-то больше чем -Xmx, -Xms и -Xss. В свое время, когда я начал углубляться в тему, то обнаружил огромное количество интересной информации, которой и хочу поделится. Отправной точкой, понятное дело, послужила официальная документация от Oracle. А дальше — гугл, эксперименты и общение:
Начну, пожалуй, с самой интересной опции — DoEscapeAnalysis. Как многие из Вас знают, примитивы и ссылки на объекты создаются не в куче, а выделяются на стеке потока (256КБ по умолчанию для Hotspot). Вполне очевидно, что язык java не позволяет создавать объекты на стеке на прямую. Но это вполне себе может проделывать Ваша JVM 1.6 начиная с 14 апдейта.
Про то, как работает сам алгоритм можно прочитать тут (PDF). Если коротко, то:
Для реализации данного алгоритма строится и используется так называемый — граф связей (connection graph), по которому на этапе анализа (алгоритмов анализа — несколько) осуществляется проход для нахождения пересечений с другими потоками и методами.
Таким образом после прохода графа связей для любого объекта возможно одно из следующих следующих состояний:
После этапа анализа, уже сама JVM проводит возможную оптимизацию: в случае если объект NoEscape, то он может быть создан на стеке; если объект NoEscape или ArgEscape, то операции синхронизации над ним могут быть удалены.
Следует уточнить, что на стеке создается не сам объект а его поля. Так как JVM заменяет цельный объект на совокупность его полей (спасибо Walrus за уточнение).
Вполне очевидно, что благодаря такого рода анализу, производительность отдельных частей программы может возрасти в разы. В синтетических тестах, на подобии этого:
скорость выполнения может увеличится в 8-15 раз. Хотя, на казалось бы, очевидных случаях из практики о которых недавно писалось (тут и тут) EscapeAnalys не работает. Подозреваю, что это связано с размером стека.
Кстати, EscapeAnalysis как раз частично ответственен за известный спор про StringBuilder и StringBuffer. То есть, если Вы вдруг в методе использовали StringBuffer вместо StringBuilder, то EscapeAnalysis (в случае срабатывания) устранит блокировки для StringBuffer'а, после чего StringBuffer вполне превращается в StringBuilder.
Опция AggressiveOpts является супер опцией. Не в том плане, что она резко увеличивает производительность Вашего приложения, а в том смысле, что она всего лишь изменяет значения других опций (на самом деле, это не совсем так — в исходном коде JDK довольно не мало мест, где AggressiveOpts изменяет поведение JVM, помимо упомянутых опций, один из примеров тут). Проверять измененные флаги будем с помощью двух команд:
После выполнения разница в результатах выполнения команд выглядела так:
Иными словами, все что делает эта опция — изменяет 5 данных параметров виртуальной машины. Причём, для версий 1.6 update 35 и 1.7 update 7 никаких отличий замечено не было. Данная опция по умолчанию отключена и в клиентском моде ничего не изменяет.
Расcмотрим, что же java подразумевает под агрессивной оптимизацией:
Позволяет расширить диапазон кешируемых значений для целых типов при старте виртуальной машины. Эту опцию я уже упоминал тут (второй абзац).
Как известно, synchronized блок в java может быть представлен одним из 3-х видов блокировок:
Подробней про это можно прочитать тут, тут и тут.
Так как большинство объектов (синхронизированных) блокируются максимум 1 одним потоком, то такие объекты могут быть привязаны (biased) к этому потоку и операции синхронизации над этим объектом внутри потока сильно удешевляются. Если к biased объекту пытается получить доступ другой поток, то происходит переключение блокировки для этого объекта на thin блокировку.
Само переключение относительно дорого, поэтому на старте JVM существует задержка, которая по умолчанию создает все блокировки как thin и если никакой конкуренции не обнаружено и код используется одним и тем же потоком, то такие блокировки, после истечения задержки, становятся biased. То есть, JVM пытается на старте определить сценарии использования блокировок и соответственно использует меньше переключений между ними. Соответственно, выставляя BiasedLockingStartupDelay в ноль, мы рассчитаем на то, что основные куски кода синхронизации будут использоваться лишь одни и тем же потоком.
Тоже довольно интересная опция. Распознает паттерн на подобии
и вместо постоянного выделения памяти под новую операцию конкатенации, идет попытка вычислить общее количество символов каждого объекта конкатенации для выделения памяти только 1 раз.
Иными словами, если мы вызовем 20 раз операцию append() для строки длинной 20 символов. То создание массива char произойдет один раз и длиной 400 символов.
Циклы заполнения/копирования массивов заменяются на прямые машинные инструкции для ускорения работы.
Например, следующий блок (взято из Arrays.fill()):
будет полностью замен на соответствующие процессорные инструкции на подобии сишных memset, memcpy только более низкоуровневых.
Исходя из названия, флаг должен как-то уменьшать количество операций автобоксинга. К сожалению, я до конца так и не смог выяснить, что же делает этот флаг. Единственное, что удалось прояснить, что применяется это только к Integer оболочкам.
Довольно спорная опция по моему убеждению… Если в далеких 90-х разработчики java не пожалели 2 байта на символ, то сегодня такая оптимизация смотрится довольно нелепо. Если кто не догадался, то опция заменяет в строках символьные массивы на байтовые, где это возможно (ASCII). По сути:
Таким образом возможна существенная экономия памяти. Но в виду того, что меняется тип, появляются накладные расходы на контроль типов при определенных операциях. То есть, с этой опцией возможна деградация производительности JVM. Собственно поэтому опция по умолчанию и отключена.
Довольно загадочная опция, судя по названию она должна каким-то образом кешировать строки. Как? Не понятно. Информации нету. Да и по коду похоже она ничего не выполняет. Буду рад, если кто-то сможет прояснить.
Для начала несколько фактов:
Данная опция позволяет уменьшить размер указателя для 64-х разрядных JVM до 32-х бит, но в этом случае размер кучи ограничен 4 ГБ, поэтому, в дополнение к сокращенному указателю, используется свойство о кратности 8 байтам. В результате получаем возможность использовать адресное пространство размером 2^35 байт (32 ГБ) имея указатели в 32 бита.
Фактически, внутри виртуальной машины, мы имеем указатели на объекты, а не конкретные байты в памяти. Понятное дело, что из-за подобных допущений (о кратности) появляются дополнительные расходы на преобразование указателей. Но по сути это всего лишь одна операция сдвига и суммирования.
Помимо уменьшения размеров самих указателей, эта опция уменьшает также заголовки объектов и разного рода выравнивания и сдвиги внутри созданных объектов, что позволяет в среднем уменьшить потребление памяти на 20-60% в зависимости от модели приложения.
То есть, из недостатков имеем лишь:
Так как для большинства приложений опция несет одни плюсы, то начиная с JDK 6 update 23 она включена по умолчанию, так же как и в JDK 7. Детальней тут и тут.
Опция, которая устраняет лишние блокировки путем их объединения. Например следующие блоки:
будут преобразованы соответственно в
Таким образом сокращается количество попыток захвата монитора.
За бортом осталось довольно много интересных опций, так как поместить все ~700 флагов в одну статью довольно трудно. Я специально не затрагивал опции по тюнингу сборщика, так это довольно обширная и сложная тема и она заслуживает нескольких постов. Надеюсь статья была вам полезной.
-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 флагов в одну статью довольно трудно. Я специально не затрагивал опции по тюнингу сборщика, так это довольно обширная и сложная тема и она заслуживает нескольких постов. Надеюсь статья была вам полезной.