Статьи

Руки на постоянной динамике Java 11

С целью сделать JVM более привлекательной для динамических языков, седьмая версия платформы представила invokedynamic в своем наборе команд. Разработчики Java обычно не принимают к сведению эту функцию, поскольку она скрыта в байт-коде Java. Короче говоря, благодаря использованию invokedynamic стало возможным отложить привязку вызова метода до его первого вызова. Этот метод, например, используется языком Java для реализации лямбда-выражений, которые проявляются только по требованию при их первом использовании. При этом invokedynamic превратился в существенную языковую функцию, которую я подробно описал в предыдущей публикации блога . С помощью constantdynamic аналогичный механизм был введен в Java 11 только потому, что он задерживает создание постоянного значения. Эта публикация описывает цель и внутреннюю работу этой функции и показывает, как генерировать код, который использует эту новую инструкцию, используя библиотеку Byte Buddy.

Каковы постоянные значения в Java?

До Java 5 постоянные значения в программах Java могли быть только строками или примитивного типа. Эти константы были встроены в язык как литералы и даже предполагаются компилятором javac для уменьшения размера файла класса. Например, в следующем фрагменте кода значение единственного поля фактически никогда не читается, а вместо этого копируется на его сайт использования во время компиляции:

1
2
3
4
5
6
class ConstantSample {
  final String field = “foo”;
  void hello() {
    System.out.print(field);
  }
}

Вместо чтения поля в методе hello сгенерированный байт-код будет содержать прямую ссылку на постоянное значение foo . Фактически, вышеупомянутый класс никогда не будет пытаться прочитать значение поля, которое можно проверить, изменив его с помощью отражения Java, после чего вызов hello все равно выведет foo .

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

Поэтому, когда поле читается в приведенном выше исходном коде, javac испускает байтовый код, который ссылается на смещение значения foo в пуле констант, вместо того, чтобы выдавать инструкцию чтения для поля. Это можно сделать, так как поле объявлено как final, где javac игнорирует крайний случай изменения отражающего значения. Отправляя инструкцию для чтения константы, javac также экономит некоторые байты по сравнению с инструкцией для чтения поля. Это то, что делает эту оптимизацию прибыльной, тем более что строковые и числовые значения довольно часто встречаются в любом классе Java. Меньшие файлы классов помогают среде выполнения Java быстрее загружать классы, а явное представление о постоянстве помогает компиляторам JIT и AOT JVM применять дальнейшую оптимизацию.

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

1
assert “foo” == “foo”;

Под капотом оба значения foo указывают на одно и то же постоянное смещение пула в постоянном пуле определяющего класса. Кроме того, JVM даже дедуплицирует строки констант между классами, интернируя строки, которые находятся в пулах констант.

Ограничения постоянного пула хранилища

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

1
2
3
4
5
6
class NoConstantSample {
  final String field = “foo”.toString();
  void hello() {
    System.out.print(field);
  }
}

Хотя метод toString является тривиальным для строк, это обстоятельство остается неизвестным для javac, который не оценивает методы Java. Следовательно, компилятор больше не может выдавать постоянное значение пула в качестве входных данных для оператора печати. Вместо этого он должен выдавать инструкцию чтения поля для поля, которая требует дополнительных байтов, как было упомянуто ранее. На этот раз, если значение поля было изменено с помощью отражения, вызов hello выведет обновленное значение.

Конечно, этот пример надуманный. Но нетрудно представить, как ограничение классического подхода к константам в Java действует на практике. Например, представьте целочисленное значение, которое определено как Math.max(CONST_A, CONST_B) . Конечно, максимум двух констант времени компиляции сам по себе будет постоянным. Тем не менее, из-за неспособности javac оценивать методы Java, производное значение не обнаруживается как константа, а вычисляется только во время выполнения.

Другой проблемой объявления постоянных значений в константном пуле файла класса является его ограничение простыми значениями. Строки и числовые значения, конечно, тривиально представить, но более сложные объекты Java требуют большей гибкости, чем классический подход. Для поддержки дополнительных констант в формате файлов классов Java уже добавлены константы литералов классов в Java 5, где такие значения, как String.class , больше не будут компилироваться для вызова Class.forName("java.lang.String") а константы. запись пула, содержащая ссылку на класс. Кроме того, в выпуске Java 7 добавлены новые типы пулов констант в спецификацию файлов классов, чтобы обеспечить постоянное представление MethodType и MethodHandle .

В отличие от строк, классов и примитивных значений, язык программирования Java, тем не менее, не предлагает литерала для создания этих последних констант. Скорее, возможность для таких констант была добавлена ​​для лучшей поддержки invokedynamic инструкций, где javac требовал эффективного способа представления. По сути, лямбда-выражение описывается сигнатурой типа лямбда-выражений — MethodType — и ссылкой на его реализацию — MethodHandle . Если бы оба значения должны были быть созданы как явные, непостоянные аргументы для каждого вызова лямбда-выражения, издержки производительности при использовании таких выражений, несомненно, перевесили бы их преимущество.

Хотя это решение облегчило некоторую промежуточную боль, оно подразумевало неудовлетворительную перспективу будущего Java в отношении добавления дополнительных константных типов. Тип записи константного пула кодируется одним байтом, что серьезно ограничивает общее количество возможных типов констант в файле класса. В качестве дополнительной проблемы, изменения в формате файла класса требуют каскадной настройки любого инструмента, который обрабатывает файлы класса, что делает желательным более общий подход для выражения постоянных значений. Благодаря введению constantdynamic , такой механизм наконец поддерживается виртуальной машиной Java с выходом Java 11.

Введение в динамические константы

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

По сути, методы начальной загрузки — это не что иное, как методы Java с некоторыми требованиями к их сигнатуре. В качестве первого аргумента любой метод начальной загрузки получает экземпляр MethodHandles.Lookup который автоматически предоставляется JVM. Такие поиски предоставляют доступ с привилегиями класса, который представляет конкретный экземпляр класса. Например, когда MethodHandles.lookup() вызывается из любого класса, чувствительный к вызывающему методу метод возвращает экземпляр, который, например, позволяет читать закрытые поля вызывающего класса, что было бы невозможно для экземпляра поиска, созданного из другого класс. В случае метода начальной загрузки поиск представляет класс, который определяет создаваемую динамическую константу, а не класс, который объявляет метод boostrap. При этом методы начальной загрузки могут получить доступ к той же информации, как если бы константа была создана из самого определяющего константу класса. В качестве второго аргумента метод начальной загрузки получает имя константы, а в качестве третьего аргумента получает ожидаемый тип констант. Метод начальной загрузки должен быть статическим или конструктором, где созданное значение представляет константу.

Во многих случаях ни один из этих трех аргументов не требуется для реализации метода начальной загрузки, но их существование позволяет реализовать более общие механизмы начальной загрузки, чтобы упростить повторное использование методов начальной загрузки для создания нескольких констант. При желании последние два аргумента также могут быть опущены при объявлении метода начальной загрузки. MethodHandles.Lookup типа MethodHandles.Lookup в качестве первого параметра является обязательным. Это сделано для того, чтобы в будущем можно было разрешить дальнейшие режимы вызова, где первый параметр служит типом маркера. Это еще одно отличие от invokedynamic, которое допускает пропуск первого параметра.

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

1
2
3
4
5
public class Bootstrapper {
  public static int bootstrap(MethodHandles.Lookup lookup, String name, Class type) {
    return Math.max(CONST_A, CONST_B);
  }
}

Поскольку экземпляр поиска, который является первым аргументом, имеет привилегии класса, который определяет константу, также можно было бы получить значения CONST_A и CONST_B с помощью этого поиска, даже если они обычно не были видны методу начальной загрузки Например, потому что они были частными. Javadoc класса подробно объясняет, какой API нужно использовать для поиска поля и считывания их значений.

Чтобы создать динамическую константу, метод начальной загрузки должен быть указан в пуле констант класса как запись динамической константы типа. На сегодняшний день язык Java не имеет возможности создать такую ​​запись, и, насколько мне известно, ни один другой язык в настоящее время не использует этот механизм. По этой причине мы рассмотрим создание таких классов с использованием библиотеки генерации кода Byte Buddy позже в этой статье. В псевдокоде Java, который намекает на постоянные значения пула в комментариях, динамическая константа и ее метод начальной загрузки будут, однако, называться следующим образом:

1
2
3
4
5
6
7
8
9
class DynamicConstant {
  // constant pool #1 = 10
  // constant pool #2 = 20
  // constant pool #3 = constantdyamic:Bootstrapper.bootstrap/maximum/int.class
  final int CONST_A = [constant #1], CONST_B = [constant #2];
  void hello() {
    System.out.print([constant #3]);
  }
}

Как только метод hello будет выполнен в первый раз, JVM разрешит указанную константу, вызвав метод Bootstrapper.bootstrap с максимумом в качестве имени константы и int.class в качестве запрошенного типа для созданной константы. После получения результата от метода начальной загрузки JVM затем заменит любую ссылку на константу этим результатом и никогда больше не вызовет метод начальной загрузки. Это также было бы верно, если бы на динамическую константу ссылались несколько сайтов.

Избегайте пользовательских методов начальной загрузки

В большинстве случаев создание динамической константы не требует реализации отдельного метода начальной загрузки. Для охвата большинства случаев использования JVM-связанный класс java.lang.invoke.ConstantBootstraps уже реализует несколько универсальных методов начальной загрузки, которые можно использовать для создания большинства констант. Как центральный элемент, метод invoke класса позволяет определять константу, предоставляя ссылку на метод в качестве фабрики для постоянного значения. Чтобы заставить такой общий подход работать, методы начальной загрузки способны получать любое количество дополнительных аргументов, которые сами должны быть постоянными значениями. Эти аргументы затем включаются в качестве ссылок на другие записи пула констант при описании записи динамической константы.

При этом вышеупомянутый максимум можно скорее вычислить, предоставив дескриптор метода Math.max и два постоянных значения CONST_A и CONST_B качестве дополнительных аргументов. Реализация метода invoke в ConstantBootstraps затем вызовет Math.max с использованием двух значений и вернет результат, где метод начальной загрузки примерно реализован следующим образом:

1
2
3
4
5
6
class ConstantBootstraps {
  static Object invoke(MethodHandles.Lookup lookup, String name, Class type,
          MethodHandle handle, Object[] arguments) throws Throwable {
    return handle.invokeWithArguments(arguments);
  }
}

Когда для метода начальной загрузки предоставляются дополнительные аргументы, они присваиваются в своем порядке каждому дополнительному параметру метода. Чтобы обеспечить более гибкие методы начальной загрузки, такие как приведенный выше метод invoke, последний параметр также может иметь тип массива Object для получения лишних аргументов, в данном случае двух целочисленных значений. Если метод начальной загрузки не принимает предоставленный аргумент, JVM не будет вызывать метод начальной загрузки, но выдаст ошибку BootstrapMethodError во время неудачного разрешения константы.

При таком подходе псевдокод для использования ConstantBootstraps.invoke больше не требует отдельного метода начальной загрузки, а выглядит так, как в следующем псевдокоде:

01
02
03
04
05
06
07
08
09
10
class AlternativeDynamicConstant {
  // constant pool #1 = 10
  // constant pool #2 = 20
  // constant pool #3 = MethodHandle:Math.max(int,int)
  // constant pool #4 = constantdyamic:ConstantBootstraps.invoke/maximum/int.class/#3,#1,#2
  final int CONST_A = [constant #1], CONST_B = [constant #2];
  void hello() {
    System.out.print([constant #4]);
  }
}

Вложенные динамические константы

Как уже упоминалось, аргументы метода начальной загрузки должны быть другими записями константного пула. Благодаря тому, что динамические константы хранятся в пуле констант, это позволяет вкладывать динамические константы, что делает эту функцию еще более гибкой. Это связано с интуитивным ограничением: инициализация динамических констант не должна содержать кружков. Например, следующие методы начальной загрузки будут вызваны сверху вниз, если будет Qux значение Qux :

01
02
03
04
05
06
07
08
09
10
11
static Foo boostrapFoo(MethodHandles.Lookup lookup, String name, Class type) {
  return new Foo();
}
 
static Bar boostrapBar(MethodHandles.Lookup lookup, String name, Class type, Foo foo) {
  return new Bar(foo);
}
 
static Qux boostrapQux(MethodHandles.Lookup lookup, String name, Class type, Bar bar) {
  return new Qux(bar);
}

Когда JVM требуется для разрешения динамической константы для Qux , она сначала разрешает Bar что снова вызывает предыдущую инициализацию Foo поскольку каждое значение зависит от предыдущего.

Вложенные динамические константы могут также потребоваться при выражении значений, которые не поддерживаются типами записей статического пула констант, такими как нулевая ссылка. До Java 11 нулевое значение могло быть выражено только как инструкция байтового кода, но не как постоянное значение пула, где байтовый код не подразумевал тип для null . Чтобы преодолеть это ограничение, java.lang.invoke.ConstantBootstraps предлагает несколько удобных методов, таких как nullValue который позволяет nullValue этого загружать типизированное null значение как динамическую константу. Это null значение затем может быть передано в качестве аргумента другому методу начальной загрузки, этот метод ожидал null в качестве аргумента. Аналогично, невозможно выразить литерал примитивного типа, такой как int.class в пуле констант, который может представлять только ссылочные типы. Вместо этого javac переводит, например, int.class в чтение статического поля Integer.TYPE которое разрешает его значение int.class при запуске собственным вызовом в JVM. Опять же, ConstantBootstraps предлагает метод начальной загрузки primitiveType для простого представления таких значений в виде динамических констант.

Почему нужно заботиться о постоянных значениях?

Все вышеперечисленное может звучать как техническая утонченность, которая мало что добавляет к платформе Java, помимо того, что уже предоставляют статические поля. Тем не менее, потенциал динамических констант велик, но все еще не изучен. Как наиболее очевидный вариант использования, динамические константы могут использоваться для правильной реализации отложенных значений. Ленивые значения обычно используются для представления дорогих объектов только по запросу, когда они используются. На сегодняшний день ленивые значения часто реализуются с помощью так называемой блокировки с двойной проверкой , которая, например, реализуется компилятором скалака для его lazy ключевого слова:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
class LazyValue {
  volatile ExpensiveValue value;
  void get() {
    T value = this.value;
    if (value == null) {
      synchronized (this) {
        value = this.value;
          if (value == null) {
            value = new ExpensiveValue();
          }
       }
     }
     return value;
  }
}

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

Другим важным вариантом использования является наличие постоянных выражений для оптимизирующих компиляторов. Легко представить, почему компиляторы предпочитают обрабатывать значения констант перед изменяемыми значениями. Например, если компилятор может объединять две константы, результат этой комбинации может навсегда заменить предыдущие значения. Это, конечно, было бы невозможно, если бы исходные значения могли изменяться со временем. И хотя компилятор точно в срок все еще может предполагать, что изменяемые значения фактически постоянны во время выполнения, опережающий компилятор зависит от некоторого явного понятия постоянства. Убедившись, что методы начальной загрузки не имеют побочных эффектов, будущая версия Java может, например, учесть их оценку во время компиляции, где constantdynamic может служить легким макро-механизмом для расширения области собственных изображений, написанных на Java с использованием Graal .

Буду ли я когда-нибудь работать с этой функцией?

Когда invokedynamic был представлен в Java 7, эта новая функция байт-кода не использовалась с точки зрения языка Java. Однако, начиная с Java 8, вызываемые динамические инструкции можно найти в большинстве файлов классов как реализацию лямбда-выражений. Точно так же Java 11 еще не использует постоянную динамическую функцию, но можно ожидать, что это изменится в будущем.

Во время последней JVMLS уже обсуждалось несколько потенциальных API для представления постоянной динамики (что также сделало бы доступной invokedynamic через API). Это было бы особенно полезно для авторов библиотек, чтобы они могли лучше разрешать критические пути выполнения, но также могло бы раскрыть некоторый потенциал для улучшения обнаружения констант javac , например, для расширения области захвата лямбда-выражений, где доступ к полям или переменным мог бы быть заменяется чтением постоянного значения, если постоянное значение было обнаружено во время компиляции. Наконец, этот новый механизм предлагает потенциал для будущих улучшений языка, таких как ленивое ключевое слово, которое позволяет избежать накладных расходов по текущим эквивалентам в альтернативных языках JVM.

Функция constantdynamic также может быть полезна агентам Java, которым часто требуется расширить существующие классы дополнительной информацией. Агенты Java обычно не могут изменять классы, например, добавляя статические поля, так как это может как мешать основанным на отражении средам, так и изменения формата класса запрещены в большинстве JVM при переопределении уже загруженного класса. Однако ни одно из ограничений не применяется к динамическим константам, которые добавляются во время выполнения, когда агент Java теперь может легко помечать классы дополнительной информацией.

Создание динамических констант с помощью Byte Buddy

Несмотря на отсутствие языковой поддержки constantdynamic , JVM версии 11 уже полностью способны обрабатывать файлы классов, которые содержат динамические константы. Используя библиотеку генерации байтового кода Byte Buddy, мы можем создавать такие файлы классов и загружать их в сборку JVM для раннего доступа .

В Byte Buddy динамические константы представлены экземплярами JavaConstant.Dynamic . Для удобства Byte Buddy предлагает фабрики для любого метода начальной загрузки, объявленного классом java.lang.invoke.ConstantBoostraps такого как метод invoke который обсуждался ранее.

Для простого примера следующий код создает подкласс Callable и определяет возвращаемое значение метода вызова как динамическую константу образца класса. Для начальной загрузки константы мы предоставляем конструктор Sample упомянутому методу invoke :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class Sample {
  public static void main(String[] args) throws Throwable {
    Constructor<? extends Callable<?>> loaded = new ByteBuddy()
      .subclass(Callable.class)
      .method(ElementMatchers.named("call"))
      .intercept(FixedValue.value(JavaConstant.Dynamic.ofInvocation(Sample.class.getConstructor())))
    .make()
    .load(Sample.class.getClassLoader())
    .getLoaded()
    .getConstructor();
 
    Callable<?> first = loaded.newInstance(), second = loaded.newInstance();
    System.out.println("Callable instances created");
    System.out.println(first.call() == second.call());
  }
   
  public Sample() {
    System.out.println("Sample instance created");
  }
}

Если вы запустите код, обратите внимание, как создается только один экземпляр Sample как было объяснено в этой статье. Также обратите внимание, как экземпляр создается лениво только при первом вызове метода вызова и после создания экземпляров Callable .

Чтобы запустить приведенный выше код, вам необходимо запустить Byte Buddy с параметром -Dnet.bytebuddy.experimental=true чтобы разблокировать поддержку этой функции. Это изменится, как только Java 11 будет завершена и готова к выпуску, где Byte Buddy 1.9.0 будет первой версией, поддерживающей Java 11 из коробки. Кроме того, в последнем выпуске Byte Buddy есть некоторые грубые грани при работе с динамическими константами. Поэтому лучше всего собрать Byte Buddy из главной ветки или использовать JitPack . Чтобы узнать больше о Byte Buddy, посетите bytebuddy.net .

Опубликовано на Java Code Geeks с разрешения Рафаэля Винтерхальтера, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Руки на постоянной динамике Java 11

Мнения, высказанные участниками Java Code Geeks, являются их собственными.