Статьи

Внутри Java 9 — производительность, компилятор и многое другое

Java 9 может многое предложить помимо модульности: новые языковые функции и множество новых или улучшенных API , опции команд в стиле GNU, многократные JAR-файлы, улучшенное ведение журнала и многое другое. Давайте рассмотрим это «больше» и посмотрим на улучшения производительности, во многом благодаря хитрости строк, компилятору, сборке мусора и JavaDoc.

Улучшения производительности

Java становится более производительной от выпуска к выпуску, и 9 не является исключением. Есть несколько интересных изменений, направленных на сокращение циклов ЦП или сохранение памяти.

Компактные струны

Когда вы смотрите на кучу Java-приложения и извлекаете все заголовки объектов и указатели, которые мы используем для организации состояния, остаются только необработанные данные. Из чего он состоит? Конечно, примитивы — многие, многие, многие из которых являются char , объединенными в массивы char которые поддерживают экземпляры String . Как оказалось, эти массивы занимают где-то между 20% и 30% средних данных реального приложения (включая заголовки и указатели). Любое улучшение в этой области будет большой победой для огромной части Java-программ! И действительно, есть куда улучшаться.

char занимает два байта, потому что он представляет собой полную кодовую единицу UTF-16, но, как оказалось, подавляющему большинству строк требуется только ISO-8859-1, то есть один байт. Это огромно! Благодаря новому представлению, в котором по возможности используется только один байт, объем памяти, вызванный строками, можно сократить почти вдвое. Это уменьшит потребление памяти для средних приложений на 10-15%, а также сократит время выполнения, затрачивая меньше времени на сбор мусора.

Конечно, это верно только в том случае, если это произошло без накладных расходов. Свободный обед кто-нибудь? JEP 254 дал ему попробовать …

Реализация

В Java 8 String имеет значение поля char[] value — это массив, который мы только что обсудили и который содержит символы строки. Идея состоит в том, чтобы использовать вместо этого byte массив и тратить один или два байта на символ, в зависимости от требуемой кодировки.

Это может звучать как случай для записей переменного размера, таких как UTF-8, где проводится различие между одним и двумя байтами на символ. Но тогда не было бы никакого способа предсказать для одного символа, какие слоты массива он будет занимать, таким образом, требующий произвольного доступа (например, charAt(int) ) для выполнения линейного сканирования. Снижение производительности произвольного доступа с постоянного до линейного времени было неприемлемым регрессом.

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

Когда в Java 8 создается новая строка, массив char обычно создается заново, а затем заполняется из параметров конструктора. Например, когда new String(myChars) , Arrays.copyOf используется для назначения копии myChars для value . Это сделано для предотвращения совместного использования массива с кодом пользователя, и только в нескольких случаях массив не копируется, например, когда строка создается из другого. Поэтому, поскольку массив value никогда не используется совместно с кодом вне String рефакторинг в byte массив безопасен (yay для инкапсуляции). И поскольку аргументы конструктора в любом случае копируются, его преобразование не добавляет чрезмерных накладных расходов.

Вот как это выглядит:

 // this is a simplified version of a String constructor, // where `char[] value` is the argument if (COMPACT_STRINGS) { byte[] val = StringUTF16.compress(value); if (val != null) { this.value = val; this.coder = LATIN1; return; } } this.coder = UTF16; this.value = StringUTF16.toBytes(value); 

Здесь следует отметить пару вещей:

  • COMPACT_STRINGS флаг COMPACT_STRINGS , который является реализацией флага командной строки XX:-CompactStrings и с помощью которого можно отключить всю функцию.
  • StringUTF16 класс StringUTF16 сначала используется для того, чтобы попытаться сжать массив value до StringUTF16 value а в случае сбоя и возврата null вместо этого преобразовать его в двойные байты.
  • Поле coder присваивается соответствующая константа, которая отмечает, какой случай применяется.

Если вы находите эту тему настолько интересной, что до сих пор не спали, я настоятельно рекомендую посмотреть поучительный и увлекательный разговор Алексея Шипилева о компактных струнах и объединении незаметных строк с великолепным подзаголовком:

Почему эти [ругательные] [ругательные] [ругательные] не могут сделать функцию за месяц, а вместо этого потратить год ?!

Производительность

Прежде чем мы действительно посмотрим на производительность, есть небольшая изящная деталь для наблюдения. JVM 8-байтовое выравнивание объектов в памяти, что означает, что, когда объект занимает менее чем 8 байтов, остальное тратится впустую. В наиболее распространенной конфигурации JVM, 64-битной виртуальной машине со сжатыми ссылками, строка требует 20 байтов (12 для заголовка объекта, 4 для массива value и последние 4 для кэшированного хэша), что оставляет еще 4 байта для сожмите в поле coder не добавляя к следу. Ницца.

Компактные строки в значительной степени оптимизируют память, поэтому имеет смысл наблюдать за сборщиком мусора. Попытка разобраться в журналах G1 вышла за рамки этого поста, поэтому я сосредоточился на производительности во время выполнения. Это имеет смысл, потому что, если строки требуют меньше памяти, их создание также должно быть быстрее.

Чтобы измерить производительность во время выполнения, я запустил этот код :

 long launchTime = System.currentTimeMillis(); List<String> strings = IntStream.rangeClosed(1, 10_000_000) .mapToObj(Integer::toString) .collect(toList()); long runTime = System.currentTimeMillis() - launchTime; System.out.println("Generated " + strings.size() + " strings in " + runTime + " ms."); launchTime = System.currentTimeMillis(); String appended = strings.stream() .limit(100_000) .reduce("", (left, right) -> left + right); runTime = System.currentTimeMillis() - launchTime; System.out.println("Created string of length " + appended.length() + " in " + runTime + " ms."); 

Сначала он создает список из десяти миллионов строк, затем он объединяет первые 100 000 из них невероятно наивным способом. И действительно, при выполнении кода либо с компактными строками (по умолчанию в Java 9), либо без (с -XX:-CompactStrings ) я заметил существенную разницу:

 # with compact strings Generated 10000000 strings in 1044 ms. Created string of length 488895 in 3244 ms. # without compact strings Generated 10000000 strings in 1075 ms. Created string of length 488895 in 7005 ms. 

Теперь, когда кто-то говорит о таких микробенчмарках, вы должны немедленно им не доверять, если они не используют JMH . Но в этом случае я не хотел проходить через потенциальную проблему запуска JMH с Java 9, поэтому я выбрал легкий путь. Это означает, что результаты могут быть полным мусором, потому что какая-то оптимизация или другой обман меня Поэтому возьмите результаты с грузом соли и посмотрите на них как на первое указание, а не на доказательство улучшения производительности.

Но ты не должен доверять мне. В приведенном выше выступлении Алексей показывает свои измерения, начиная с 36:30 , ссылаясь на лучшую пропускную способность в 1,36 раза и на 45% меньше мусора.

Указанная конкатенация строк

Быстрое повторение работы конкатенации строк … Допустим, вы пишете следующее:

 String s = greeting + ", " + place + "!" 

Затем компилятор создаст байт-код, который использует StringBuilder для создания s , сначала добавляя отдельные части, а затем вызывая toString чтобы получить результат. Во время выполнения JIT-компилятор может распознавать эти цепочки добавления и, если это так, может значительно повысить производительность. Он сгенерирует код, который проверяет длину аргументов, создает массив правильного размера, копирует символы прямо в этот массив и, вуаля, упаковывает его в строку.

Это не становится лучше, но распознавать эти цепочки дополнений и доказывать, что они могут быть заменены оптимизированным кодом, нетривиально и быстро ломается. По-видимому, все, что вам нужно, это long или double в этой конкатенации, и JIT не сможет оптимизировать.

Но зачем столько усилий? Почему бы просто не иметь метод String.concat(String... args) который вызывает байт-код? Потому что создание массива varargs на пути, критичном к производительности, не лучшая идея. Кроме того, примитивы не очень хорошо с этим справляются, если только вы заранее не toString все из них, что, в свою очередь, не позволит их преобразовать прямо в целевой массив. И даже не думайте о String.concat(Object... args) , который бы String.concat(Object... args) каждый примитив.

Поэтому для повышения производительности необходимо другое решение. Следующая лучшая вещь — позволить javac генерировать лучший байт-код, но у этого также есть недостатки:

  • Каждый раз, когда реализуется новая оптимизация, байт-код снова меняется.
  • Чтобы пользователи могли извлечь выгоду из этих оптимизаций, они должны перекомпилировать свой код — чего обычно избегает Java, если это возможно.
  • Поскольку все JVM должны иметь возможность JIT компилировать все варианты, матрица тестирования взрывается.

Так что еще можно сделать? Может быть, здесь отсутствует абстракция? Разве байт-код не может просто объявить намерение «объединить эти вещи» и позволить JVM обработать все остальное?

Да, это в значительной степени решение, используемое JEP 280 — по крайней мере, для первой части. Благодаря магии invokedynamic , байт-код может выражать намерение и аргументы (без упаковки), но JVM не должна предоставлять эту функциональность и может вместо этого направить обратно в JDK для реализации. Это замечательно, потому что в JDK все виды частных API могут использоваться для различных приемов ( javac может использовать только публичные API).

Позвольте мне еще раз отослать вас к выступлению Алексея — вторая половина, начиная с 37:58 , охватывает эту часть. Он также содержит некоторые числа, которые показывают ускорение до 2,6х и до 70% меньше мусора — и это без компактных строк!

Еще одна смешанная сумка

Есть еще одно улучшение, связанное со строками, но этого я не совсем понял. Насколько я понимаю, различные процессы JVM могут совместно использовать загруженные классы через архивы совместного использования классов (CDS). В этих архивах строки данных класса (точнее, пул констант) представлены в виде строк UTF-8 и по требованию превращаются в экземпляры String . Объем памяти можно уменьшить, создавая не всегда новые экземпляры, а разделяя их между различными JVM. Чтобы сборщик мусора взаимодействовал с этим механизмом, ему необходимо предоставить функцию, называемую закрепленными областями , которой обладает только G1. Это понимание, похоже, противоречит названию JEP Store Interned Strings в CDS Archives , поэтому, если вас это интересует, вы должны посмотреть сами. ( JEP 250 )

Основным строительным блоком параллелизма Java являются мониторы — у каждого объекта есть один, и каждый монитор может принадлежать максимум одному потоку за раз. Чтобы поток стал владельцем монитора, он должен вызвать synchronized метод, объявленный этим объектом, или ввести synchronized блок, который синхронизируется с объектом. Если несколько потоков пытаются сделать это в, все, кроме одного, помещаются в набор ожидания, и монитор считается состязательным , что создает узкое место в производительности. Во-первых, само приложение тратит время на ожидание, но в дополнение к этому JVM должна выполнить некоторую работу, организовав конфликт блокировки и выбрав новый поток, как только монитор снова станет доступным. Это согласование с JVM улучшено, что должно улучшить производительность в сильно оспариваемом коде. ( JEP 143 )

В Java 2D все сглаживание (кроме шрифтов) выполняется так называемым растеризатором. Это внутренняя подсистема без API для разработчиков Java. Но он лежит на горячем пути, и его производительность имеет решающее значение для многих приложений с интенсивным использованием графики. OpenJDK использует Pisces, Oracle JDK использует Ductus, где первый демонстрирует гораздо меньшую производительность, чем второй. Рыбы теперь должны быть заменены графическим рендерером Marlin , который обещает превосходную производительность при том же качестве и точности. Вероятно, что Marlin будет соответствовать Dustus с точки зрения качества, точности и производительности одного потока и даже превзойдет его в многопоточных сценариях. ( JEP 265 , немного истории и контекста )

Неподтвержденные данные свидетельствуют о том, что запуск приложения с активным менеджером безопасности снижает производительность на 10-15%. Была предпринята попытка уменьшить этот разрыв с помощью различных небольших оптимизаций. ( JEP 232 )

Процессоры SPARC и Intel недавно представили инструкции, которые хорошо подходят для криптографических операций. Они были использованы для повышения производительности вычислений GHASH и RSA . ( JEP 246 )

вагоноремонтный-внутри-ява-9-производительность-компилятор

Вывоз мусора

Одно из наиболее оспариваемых изменений Java 9, уступающее только Project Jigsaw, заключается в том, что Garbage First (G1) станет новым сборщиком мусора по умолчанию ( JEP 248 ). К счастью для меня, он готов к производству со времен Java 8, поэтому мне не нужно сейчас это обсуждать.

Краткое резюме: G1 ограничивает время паузы и теряет некоторую пропускную способность для достижения этой цели. При реализации он не разделяет кучу на непрерывные пространства, такие как Eden , молодые и старые, но на регионы фиксированного размера, где G1 назначает роль региону, когда он начинает его использовать, и сбрасывает роль, как только он собрал весь контент региона. Говоря о коллекциях, они сосредоточены на регионах с наибольшим количеством мусора, отсюда и название, потому что это обещает наименьшую работу.

G1 — интересный зверь, и я рекомендую уделить ему время. Если вы не хотите делать это самостоятельно, оставайтесь на связи, потому что этот канал скоро это обсудит. Одна хорошая деталь, которую я нашел, — это дедупликация строк (введена в 8u20 JEP 192 ), где G1 идентифицирует экземпляры String которые имеют одинаковые массивы value а затем заставляет их использовать один и тот же экземпляр массива. По-видимому, встречаются повторяющиеся строки, и эта оптимизация экономит около 10% пространства кучи — хотя это было до появления компактных строк, поэтому, возможно, сейчас оно ближе к 5%.

Наконец, JEP 214 удалил некоторые опции GC, которые JEP 173 устарел.

составитель

Компилировать для более старых версий Java

Вы когда-нибудь использовали параметры -source и -target , чтобы скомпилировать код для запуска на более старой JRE, чтобы увидеть, как он -source с -target во время выполнения, потому что вызов некоторого метода завершился неудачно с кажущейся необъяснимой ошибкой? Возможно, вы забыли указать -bootclasspath . Потому что без этого компилятор ссылается на API-интерфейс библиотеки ядра текущей версии, что может сделать байт-код несовместимым со старыми версиями. Для исправления этой распространенной рабочей ошибки компилятор Java 9 поставляется с флагом --release который устанавливает для всех остальных трех параметров правильное значение.

Интерфейс компилятора JVM

Я нашел это чрезвычайно интересным! JEP 243 разработал интерфейс компилятора виртуальной машины Java (JVMCI) — набор интерфейсов Java, реализации которых JVM может использовать для выполнения своевременной компиляции, тем самым заменив компилятор C1 / C2. Это все еще экспериментальная функция, и ее нужно явно активировать в командной строке, но траектория ясна: иметь JIT-компилятор, который реализован на Java. Вероятным кандидатом является тот, который разработан проектом Graal , который уже реализует JVMCI.

Если вы спрашиваете себя: «Но почему?», Вот что должен сказать по этому поводу JEP:

Оптимизирующий компилятор — это сложная часть программного обеспечения, которая значительно выигрывает от функций, предоставляемых Java, таких как автоматическое управление памятью, обработка исключений, синхронизация, отличные (и бесплатные) интегрированные среды разработки, отличная поддержка модульного тестирования и расширяемость во время выполнения с помощью загрузчиков служб, просто чтобы назвать несколько. Кроме того, компилятору не требуются функции языка низкого уровня, требуемые многими другими подсистемами JVM, такими как интерпретатор байт-кода и сборщик мусора. Эти наблюдения убедительно свидетельствуют о том, что написание JVM-компилятора на Java должно позволить создать высококачественный компилятор, который будет легче поддерживать и улучшать, чем существующие компиляторы, разработанные на C или C ++.

Имеет смысл, верно?

Опережающая сборка

Java — это «напиши один раз, беги куда угодно», и это здорово, но что, если ты не хочешь платить за это? Если вы хотите раскрутить JVM для одного вызова метода (кто-нибудь сказал, что он без сервера ?), То JIT не принесет вам большой пользы — для максимальной производительности вам понадобится машинный код еще до запуска.

Введите заблаговременный сборник ( JEP 295 )! С его помощью вы можете использовать компилятор Graal, поставляемый с вашим локальным JDK, чтобы скомпилировать код, который вы собираетесь использовать, и затем сказать java использовать эти артефакты вместо байт-кода, который он содержит. Вот небольшой фрагмент из JEP, который компилирует код пользователя и требуемый модуль JDK и запускает с ними Java:

 jaotc --output libHelloWorld.so HelloWorld.class jaotc --output libjava.base.so --module java.base java9 -XX:AOTLibrary=./libHelloWorld.so,./libjava.base.so HelloWorld 

Это поднимает ряд вопросов:

  • Что если версии байт-кода и машинного кода модуля не совпадают?
  • Что если среда выполнения запускается с параметрами VM, отличными от того, с которым был скомпилирован код? (Сжатые указатели объектов, например.)
  • Должен ли скомпилированный код собирать информацию о профилировании для дальнейшей оптимизации?

Эти и другие вопросы, конечно же, решаются, и JEP является хорошим источником ответов.

Как и в случае с JVMCI, это явно экспериментальная функция (я даже не обнаружил ее в самой последней сборке), и она все еще имеет серьезные ограничения — в частности, она работает только на 64-битных системах Linux и может компилировать только java.base модуль. Но это интересное направление, в котором движется Java.

Внутренности

Компилятор также получил небольшое повышение производительности. В некоторых сценариях (например, для вложенных лямбда-выражений) вывод типа будет иметь экспоненциальное время выполнения — не очень хорошо. Многоуровневая атрибуция исправляет это. ( JEP 215 , 2-минутное видео резюме )

Поскольку конвейер аннотаций был создан для Java 5, его пришлось расширять несколько раз, чтобы включить новые функции, такие как повторяющиеся аннотации, аннотации типов и новые синтаксически допустимые позиции из-за лямбда-выражений. «[Он] не мог справиться с такими случаями из коробки; в результате первоначальный дизайн был растянут для того, чтобы учесть новые варианты использования, что привело к хрупкой и сложной в обслуживании реализации ». Для Java 9 был создан и реализован полный редизайн, конвейер аннотаций 2.0 . Он не добавляет никаких функций, но должен обеспечить лучшую основу для будущих расширений. ( JEP 217 , Проект конвейера аннотаций 2.0 )

Затем есть кое-что о включении управляемых, зависимых от метода флагов компилятора , чего я не получаю вообще. ( JEP 165 )

Наконец, JEP 237 интегрировал порт JDK 9 для Linux / AArch64 в OpenJDK.

JavaDoc

Вы взглянули на предварительный Javadoc в Java 9 ? Вы заметили что-нибудь новое? Если нет, перейдите сейчас, найдите текстовое поле в верхнем правом углу и начните вводить имя класса JDK. Аккуратно, верно? ( JEP 225 )

Javadoc теперь может генерировать HTML 5 страниц с «структурными элементами, такими как верхний и нижний колонтитулы , навигация и т. Д.» И улучшенным доступом благодаря ARIA . ( JEP 224 )

Сохраняя лучшее для последнего, по крайней мере, в отношении JavaDoc, я хочу закончить с новым API Doclet. Доклеты — это плагины JavaDoc, которые вы можете создавать самостоятельно для обработки комментариев Javadoc (вы комментируете свой код , верно?). Старый API был чрезвычайно эзотерическим с самой необычной, ошибочной особенностью, заключающейся в том, что вам приходилось создавать статические методы только с правильными именами, чтобы инструмент мог вызывать ваш плагин. (Это было до интерфейсов или как?) Новый API избавляет от такого безумия. Он также предоставляет доступ к API модели языка и API DocTree, что позволяет вам перемещаться по исходному коду и создавать выходные данные. ( JEP 224 )

Больше не надо!

На этом завершается тур по Java 9. Игнорируя несколько мелких вещей, эти три статьи представляют все, что может предложить Java 9:

Но пока что мы только немного поцарапали поверхность — в течение следующих месяцев мы опубликуем больше информации о Java 9, подробно остановившись на любом количестве тем. Так что смотрите это место, например, через RSS , или подпишитесь на нашу рассылку.