Статьи

Нам действительно все еще нужна 32-битная JVM?

Даже сегодня (и в 2015 году) у нас есть две версии или Oracle HotSpot JDK — настроенные на 32- или 64-битную архитектуру. Вопрос в том, хотим ли мы использовать 32-битную JVM на наших серверах или даже на ноутбуках? Существует довольно популярное мнение, что мы должны! Если вам нужна только небольшая куча, используйте 32 бита — она ​​занимает меньше памяти, поэтому ваше приложение будет использовать меньше памяти и будет вызывать более короткие GC-паузы. Но так ли это? Я исследую три разные области:

  1. След памяти
  2. Производительность GC
  3. Общая производительность

Начнем с потребления памяти.

След памяти

Известно, что основная разница между 32- и 64-битной JVM связана с адресацией памяти. Это означает, что все ссылки в 64-битной версии занимают 8 байтов вместо 4. К счастью, JVM поставляется с указателями сжатых объектов, которые по умолчанию включены для всех куч, меньших 26 ГБ. Этот предел для нас более чем приемлем, если 32-битная JVM может адресовать около 2 ГБ (в зависимости от целевой ОС она все еще примерно в 13 раз меньше). Так что не беспокойтесь о ссылках на объекты. Единственное, что отличает расположение объектов — это заголовки меток, которые на 4 бита больше на 64 бита. Мы также знаем, что все объекты в Java выровнены на 8 байт, поэтому возможны два варианта:

  • худшее — на 64-битном объекте объект на 8 байт больше, чем на 32-битном. Это потому, что добавление 4 байтов к заголовку приводит к тому, что объект помещается в другой слот памяти, поэтому нам нужно добавить еще 4 байта, чтобы заполнить пробел выравнивания.
  • лучше всего — объекты на обеих архитектурах имеют одинаковый размер. Это происходит, когда на 32 битах у нас есть 4-байтовый интервал выравнивания, который можно просто заполнить дополнительными байтами заголовка метки.

Давайте теперь вычислим оба случая, предполагая два разных размера приложения. IntelliJ IDEA с загруженным довольно большим проектом содержит около 7 миллионов объектов — это будет наш меньший проект. Для второго варианта предположим, что у нас есть большой проект (я назову его Огромным), содержащий 50 миллионов объектов в живом наборе. Давайте теперь посчитаем наихудший случай:

  • IDEA -> 7 millions * 8 bytes = 53 MB
  • Huge -> 50 millions * 8 bytes = 381 MB

Вышеприведенные расчеты показывают, что в худшем случае реальная площадь приложения увеличивается примерно на 50 МБ для IntelliJ и около 400 МБ для какого-то огромного, гранулированного проекта с действительно небольшими объектами. Во втором случае это может составлять около 25% от общей кучи, но для подавляющего большинства проектов это около 2%, что почти ничего.

GC Performance

Идея состоит в том, чтобы поместить 8 миллионов объектов String в кэш с помощью ключа Long. Один тест состоит из 4 вызовов, а это означает, что 24 миллиона вложений в карту кэша. Я использовал Parallel GC с общим размером кучи, установленным на 2 ГБ. Результаты оказались довольно удивительными, потому что весь тест закончился раньше на 32-битной JDK. 3 минуты 40 секунд по сравнению с 4 минутами 30 секундами на 64-битной виртуальной машине. После сравнения логов GC мы видим, что разница в основном происходит от пауз GC: от 114 до 157 секунд. Это означает, что на практике 32-битная JVM значительно снижает накладные расходы GC — 554 паузы до 618 для 64-битных. Ниже вы можете увидеть скриншоты из GC Viewer (оба с одинаковым масштабом на обеих осях)

32-битная JVM Parallel GC

32-битная JVM Parallel GC

64-битная JVM Parallel GC

64-битная JVM Parallel GC

Я ожидал меньших накладных расходов на 64-битную JVM, но тесты показывают, что даже общее использование кучи схоже на 32-битных, мы освобождаем больше памяти на Full GC. Паузы молодого поколения также схожи — около 0,55 секунд для обеих архитектур. Но средняя большая пауза выше на 64 бита — 3,2 по сравнению с 2,7 на 32 бита. Это доказывает, что производительность GC для небольшой кучи намного лучше на 32-битной JDK. Вопрос в том, насколько ваши приложения так требовательны к GC — в тесте средняя пропускная способность составляла около 42-48%.

Второй тест был выполнен по более «корпоративному» сценарию. Мы загружаем сущности из базы данных и вызываем метод size () в загруженном списке. Для общего времени тестирования около 6 минут у нас есть общее время паузы 133,7 с для 64-битных и 130,0 с для 32-битных. Использование кучи также очень похоже — 730 МБ для 64-битной и 688 МБ для 32-битной JVM. Это показывает нам, что для обычного «корпоративного» использования нет большой разницы между производительностью GC на различных архитектурах JVM.

32-битная JVM Parallel GC выбирает из БД

32-битная JVM Parallel GC выбирает из БД

64-битная JVM Parallel GC выбирает из БД

64-битная JVM Parallel GC выбирает из БД

Даже с аналогичной производительностью GC 32-битная JVM завершила работу на 20 секунд раньше (что составляет около 5%).

Общая производительность

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

01
02
03
04
05
06
07
08
09
10
Benchmark                    32bits [ns]   64bits [ns]   ratio
 
System.currentTimeMillis()       113.662        22.449    5.08
System.nanoTime()                128.986        20.161    6.40
 
findMaxIntegerInArray           2780.503      2790.969    1.00
findMaxLongInArray              8289.475      3227.029    2.57
countSinForArray                4966.194      3465.188    1.43
 
UUID.randomUUID()               3084.681      2867.699    1.08

Как мы видим, самое большое и определенно существенное различие заключается во всех операциях, связанных с длинными переменными. Эти операции выполняются в 2,6–6,3 раза быстрее на 64-битной JVM. Работа с целыми числами очень похожа, а генерация случайного UUID быстрее всего на 7%. Стоит отметить, что интерпретируемый код (-Xint) имеет аналогичную скорость — просто JIT для 64-битной версии намного эффективнее. Так есть ли какие-то особые различия? Да! 64-битная архитектура поставляется с дополнительными регистрами процессора, которые используются JVM. После проверки сгенерированной сборки выясняется, что повышение производительности в основном обусловлено возможностью использования 64-битных регистров, что может упростить длительные операции. Любые другие изменения можно найти, например, на странице вики . Если вы хотите запустить это на своем компьютере, вы можете найти все тесты на моем GitHub — https://github.com/jkubrynski/benchmarks_arch

Выводы

Как и во всем мире ИТ, мы не можем просто ответить — «да, вы всегда должны использовать ** бит JVM». Это сильно зависит от характеристик вашего приложения. Как мы видели, существует много различий между 32-х и 64-х битной архитектурой. Даже если производительность JIT для длинных связанных операций на несколько сотен процентов выше, мы видим, что протестированные пакетные процессы завершались раньше на 32-битной JVM. В заключение — простого ответа нет. Вы всегда должны проверять, какая архитектура лучше соответствует вашим требованиям.

Большое спасибо Войтеку Кудле за рецензирование этой статьи и проведение дополнительных тестов 🙂

Ссылка: Нам действительно все еще нужна 32-битная JVM? от нашего партнера JCG Якуба Кубринского в блоге Java (B) Log .