Статьи

Демонтаж invokedynamic

Многие Java-разработчики расценили версию JDK 7 как разочарование. На первый взгляд, только несколько языковых и библиотечных расширений вошли в релиз, а именно Project Coin и NIO2 . Но под прикрытием седьмая версия платформы поставила самое большое расширение системы типов JVM, когда-либо представленное после ее первоначального выпуска. Добавление инструкции invokedynamic не только заложило основу для реализации лямбда-выражений в Java 8, но также изменило правила игры для перевода динамических языков в формат байт-кода Java.

В то время как вызыванная динамическая инструкция является подробностью реализации для выполнения языка на виртуальной машине Java, понимание функционирования этой инструкции дает истинное понимание внутренней работы выполнения Java-программы. Эта статья дает начинающему взгляду на то, какую проблему решает invokedynamic инструкция и как она ее решает.

Методы

Описатели метода часто описываются как модифицированная версия API отражения Java, но это не то, что они должны представлять. Хотя дескрипторы метода действительно представляют метод, конструктор или поле, они не предназначены для описания свойств этих членов класса. Например, невозможно напрямую извлечь метаданные из дескриптора метода, такого как модификаторы или значения аннотации представленного метода. И хотя дескрипторы метода допускают вызов ссылочного метода, их основное назначение – использовать его вместе с сайтом вызываемого динамического вызова. Тем не менее, для лучшего понимания дескрипторов методов, рассмотрим их как несовершенную замену API отражений, что является разумной отправной точкой.

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

1
2
3
4
5
class Example {
  void doSomething() {
    MethodHandles.Lookup lookup = MethodHandles.lookup();
  }
}

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

Кроме того, дескриптор метода более специфичен, чем API отражения, описывая конкретный тип метода, а не просто представляя какой-либо метод. В Java-программе тип метода представляет собой совокупность как возвращаемого типа метода, так и типов его параметров. Например, единственный метод следующего класса Counter возвращает int, представляющий количество символов единственного аргумента типа String :

1
2
3
4
5
class Counter {
  static int count(String name) {
    return name.length();
  }
}

Представление типа этого метода может быть создано с использованием другой фабрики. Эта фабрика находится в классе MethodType который также представляет экземпляры созданных типов методов. Используя эту фабрику, тип метода для Counter::count может быть создан путем передачи типа возврата метода и его типов параметров, связанных в виде массива:

1
MethodType methodType = MethodType.methodType(int.class, new Class<?>[] {String.class});

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

1
MethodType.methodType(int.class, Example.class, new Class<?>[] {String.class});

Используя объект поиска, созданный ранее, и вышеуказанный тип метода, теперь можно найти дескриптор метода, который представляет метод Counter::count как показано в следующем коде:

1
2
3
4
5
MethodType methodType = MethodType.methodType(int.class, new Class<?>[] {String.class});
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle methodHandle = lookup.findStatic(Counter.class, "count", methodType);
int count = methodHandle.invokeExact("foo");
assertThat(count, is(3));

На первый взгляд использование дескриптора метода может показаться слишком сложной версией использования API отражения. Однако имейте в виду, что прямой вызов метода, использующего дескриптор, не является основной целью его использования.

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

Следуя этому принципу, рефлексивный вызов метода выполняется как общий вызов метода Method :: invoke. Этот метод идентифицируется его двумя параметрами, которые имеют типы Object и Object []. В дополнение к этому, метод идентифицируется по типу возвращаемого объекта. Из-за этой подписи все аргументы этого метода должны быть всегда упакованы и заключены в массив. Точно так же возвращаемое значение должно быть упаковано, если оно было примитивным, или нуль возвращается, если метод был пустым.

Дескрипторы метода являются исключением из этого правила. Вместо вызова дескриптора метода путем обращения к сигнатуре сигнатуры MethodHandle::invokeExact которая принимает Object[] качестве единственного аргумента и возвращает Object , дескрипторы метода вызываются с помощью так называемой полиморфной сигнатуры. Полиморфная подпись создается компилятором Java в зависимости от типов фактических аргументов и ожидаемого типа возврата на сайте вызова. Например, при вызове дескриптора метода, как указано выше,

1
int count = methodHandle.invokeExact("foo");

компилятор Java транслирует этот вызов так, как если invokeExact был определен метод invokeExact который принимает один единственный аргумент типа String и возвращает тип int . Очевидно, что такой метод не существует, и для (почти) любого другого метода это приведет к ошибке компоновки во время выполнения. Для дескрипторов метода виртуальная машина Java, однако, распознает эту сигнатуру как полиморфную и обрабатывает вызов дескриптора метода так, как если бы метод Counter::count , на который ссылается дескриптор, был вставлен непосредственно в сайт вызова. Таким образом, метод может быть вызван без накладных расходов на примитивы бокса или тип возвращаемого значения и без помещения значений аргумента в массив.

В то же время при использовании вызова invokeExact для виртуальной машины Java гарантируется, что дескриптор метода всегда ссылается на метод во время выполнения, совместимый с полиморфной сигнатурой. В этом примере JVM ожидала, что ссылочный метод фактически принимает String качестве единственного аргумента и возвращает примитив int . Если это ограничение не будет выполнено, выполнение приведет к ошибке во время выполнения. Однако любой другой метод, который принимает одну String и возвращает примитив int может быть успешно заполнен на сайте вызова дескриптора метода, чтобы заменить Counter::count .

Напротив, использование дескриптора метода Counter::count при следующих трех вызовах приведет к ошибкам во время выполнения, даже если код успешно компилируется:

1
2
3
int count1 = methodHandle.invokeExact((Object) "foo");
int count2 = (Integer) methodHandle.invokeExact("foo");
methodHandle.invokeExact("foo");

Первое утверждение приводит к ошибке, потому что аргумент, который передается дескриптору, является слишком общим. Хотя JVM ожидала String в качестве аргумента метода, Java-компилятор предположил, что аргумент будет типом Object . Важно понимать, что компилятор Java воспринял приведение как подсказку для создания другой полиморфной сигнатуры с типом Object в качестве одного типа параметра, в то время как JVM ожидала String во время выполнения. Обратите внимание, что это ограничение также применимо для передачи слишком определенных аргументов, например, при приведении аргумента к Integer где дескриптор метода требует тип Number качестве аргумента. Во втором утверждении компилятор Java предложил среде выполнения, что метод дескриптора будет возвращать тип-обертку Integer вместо примитива int . И даже не предлагая тип возврата вообще в третьем утверждении, компилятор Java неявно преобразовал вызов в вызов метода void. Следовательно, invokeExact действительно означает точное.

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

Поля, методы и конструкторы: дескрипторы как единый интерфейс

Кроме экземпляров метода API отражения, дескрипторы метода могут в равной степени ссылаться на поля или конструкторы. Таким образом, имя типа MethodHandle может рассматриваться как слишком узкое. Фактически, не имеет значения, на какой член класса ссылается дескриптор метода во время выполнения, если его MethodType , другой тип с вводящим в заблуждение именем, соответствует аргументам, которые передаются на связанном сайте вызова.

Используя соответствующие фабрики объекта MethodHandles.Lookup , можно искать поле для представления метода получения или установки. Использование методов получения или установки в этом контексте не относится к вызову фактического метода, который следует спецификации Java-бина. Вместо этого дескриптор метода на основе поля непосредственно считывает или записывает данные в поле, но в форме вызова метода через вызов дескриптора метода. Представляя такой доступ к полю через дескрипторы метода, доступ к полю или вызовы метода могут использоваться взаимозаменяемо.

В качестве примера такого обмена возьмем следующий класс:

1
2
3
4
5
6
class Bean {
  String value;
  void print(String x) {
    System.out.println(x);
  }
}

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

1
2
3
MethodHandle fieldHandle = lookup.findSetter(Bean.class, "value", String.class);
MethodType methodType = MethodType.methodType(void.class, new Class<?>[] {String.class});
MethodHandle methodHandle = lookup.findVirtual(Bean.class, "print", methodType);

Пока сайт вызова дескриптора метода получает экземпляр Bean вместе со String при возврате void , оба дескриптора метода могут использоваться взаимозаменяемо, как показано здесь:

1
anyHandle.invokeExact((Bean) mybean, (String) myString);

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

Показатели эффективности

Описатели метода часто описываются как более производительные, как API отражения Java. По крайней мере, для последних выпусков виртуальной машины HotSpot это не так. Самый простой способ доказать это – написать соответствующий тест . Опять же, не так уж просто написать эталонный тест для Java-программы, которая оптимизируется во время ее выполнения. Де-факто стандартом для написания теста стало использование JMH, жгута, который поставляется под зонтиком OpenJDK. Полный тест можно найти в моем профиле GitHub. В этой статье рассматриваются только самые важные аспекты этого теста.

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

Создание сайта invokedynamic call

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

Для лучшего понимания вызываемых динамических сайтов вызовов, это помогает явно создавать такие сайты вызовов, чтобы взглянуть на механизм отдельно. Для этого в следующем примере используется моя структура генерации кода Byte Buddy, которая обеспечивает явную генерацию байтового кода сайтов вызываемых динамических вызовов, не требуя каких-либо знаний о формате байтового кода.

Любой вызванный сайт динамического вызова в конечном итоге приводит к MethodHandle, который ссылается на метод, который будет вызван. Вместо того, чтобы вызывать этот дескриптор метода вручную, это зависит от времени выполнения Java. Поскольку дескрипторы методов стали известной концепцией для виртуальной машины Java, эти вызовы затем оптимизируются аналогично общему вызову метода. Любой такой дескриптор метода получен из так называемого метода начальной загрузки, который является не чем иным как простым методом Java, который выполняет определенную сигнатуру. Для тривиального примера метода начальной загрузки посмотрите на следующий код:

1
2
3
4
5
6
7
8
class Bootstrapper {
  public static CallSite bootstrap(Object... args) throws Throwable {
    MethodType methodType = MethodType.methodType(int.class, new Class<?>[] {String.class})
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle methodHandle = lookup.findStatic(Counter.class, "count", methodType);
    return new ConstantCallSite(methodHandle);
  }
}

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

Как видно из приведенного выше примера, MethodHandle не возвращается непосредственно из метода начальной загрузки. Вместо этого дескриптор оборачивается внутри объекта CallSite . Всякий раз, когда вызывается метод начальной загрузки, сайт вызываемого динамического вызова впоследствии постоянно связывается с объектом CallSite который возвращается из этого метода. Следовательно, метод начальной загрузки вызывается только один раз для любого сайта вызовов. Благодаря этому промежуточному объекту CallSite , однако, возможно обменяться ссылочным MethodHandle на более позднем этапе. Для этой цели библиотека классов Java уже предлагает различные реализации CallSite . Мы уже видели ConstantCallSite в приведенном выше примере кода. Как следует из названия, ConstantCallSite всегда ссылается на один и тот же дескриптор метода без возможности последующего обмена. В качестве альтернативы, однако, также возможно, например, использовать MutableCallSite который позволяет изменить ссылочный MethodHandle в более поздний момент времени или даже можно реализовать собственный класс CallSite .

С помощью вышеуказанного метода начальной загрузки и Byte Buddy мы можем теперь реализовать пользовательскую команду invokedynamic. Для этого Byte Buddy предлагает инструментарий InvokeDynamic который принимает метод начальной загрузки в качестве единственного обязательного аргумента. Такие контрольно-измерительные приборы затем передаются в Byte Buddy. Предполагая следующий класс:

1
2
3
abstract class Example {
  abstract int method();
}

мы можем использовать Byte Buddy для подкласса Example , чтобы переопределить method . Затем мы собираемся реализовать этот метод, чтобы он содержал единый сайт вызова динамического вызова. Без дальнейшей настройки Byte Buddy создает полиморфную сигнатуру, которая напоминает тип метода переопределенного метода. Помните, что для нестатических методов ссылка this передается как первый неявный аргумент. Предполагая, что мы хотим связать метод Counter::count который ожидает String в качестве единственного аргумента, мы не можем связать этот дескриптор с Example::method который не соответствует типу метода. Поэтому нам нужно создать другой сайт вызовов без неявного аргумента, но с String на его месте. Это может быть достигнуто с помощью языка, специфичного для домена Byte Buddy:

1
2
3
4
Instrumentation invokeDynamic = InvokeDynamic
 .bootstrap(Bootstrapper.class.getDeclaredMethod(“bootstrap”, Object[].class))
 .withoutImplicitArguments()
 .withValue("foo");

С помощью этого инструментария мы наконец можем расширить класс Example и метод переопределения для реализации сайта вызова invokedynamic, как в следующем фрагменте кода:

01
02
03
04
05
06
07
08
09
10
Example example = new ByteBuddy()
  .subclass(Example.class)
   .method(named(“method”)).intercept(invokeDynamic)
   .make()
   .load(Example.class.getClassLoader(),
         ClassLoadingStrategy.Default.INJECTION)
   .getLoaded()
   .newInstance();
int result = example.method();
assertThat(result, is(3));

Как видно из приведенного выше утверждения, символы строки "foo" были подсчитаны правильно. Установив соответствующие точки останова в коде, можно далее проверить, что метод начальной загрузки вызывается и этот поток управления далее достигает метода Counter::count .

До сих пор мы не получили много пользы от использования сайта invokedynamic call. Приведенный выше метод начальной загрузки всегда связывает Counter::count и, следовательно, может привести к действительному результату только в том случае, если сайт вызываемого динамического вызова действительно хочет преобразовать String в int . Очевидно, что методы начальной загрузки могут быть более гибкими благодаря аргументам, которые они получают с сайта вызываемого динамического вызова. Любой метод начальной загрузки получает как минимум три аргумента:

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

Второй аргумент – это String представляющая имя метода. Эта строка служит подсказкой для указания с сайта вызова, какой метод должен быть привязан к нему. Строго говоря, этот аргумент не является обязательным, поскольку совершенно законно связывать метод с другим именем. Byte Buddy просто использует имя переопределенного метода в качестве этого аргумента, если не указано иначе.

Наконец, MethodType дескриптора метода, который, как ожидают, будет возвращен, служит третьим аргументом. В приведенном выше примере мы явно указали, что ожидаем String как один параметр. В то же время Byte Buddy выяснил, что нам требуется int в качестве возвращаемого значения при просмотре переопределенного метода, поскольку мы снова не указали какой-либо явный тип возвращаемого значения.

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

Кроме того, метод начальной загрузки может получить несколько аргументов с сайта вызываемого динамического вызова, если эти аргументы могут храниться в пуле констант класса. Для любого класса Java в пуле констант хранятся значения, которые используются внутри класса, в основном числа или строковые значения. На сегодняшний день такими константами могут быть примитивные значения размером не менее 32 бит, String s, Class es, MethodHandl es и MethodType s. Это позволяет использовать методы начальной загрузки более гибко, если для поиска подходящего дескриптора метода требуется дополнительная информация в форме таких аргументов.

Лямбда-выражения

Всякий раз, когда компилятор Java переводит лямбда-выражение в байт-код, он копирует тело лямбды в закрытый метод внутри класса, в котором определено выражение. Эти методы называются lambda$X$Y где X – это имя метода, содержащего лямбда-выражение, а Y – порядковый номер, Y с нуля. Параметры такого метода – это параметры функционального интерфейса, которые реализует лямбда-выражение. Учитывая, что лямбда-выражение не использует нестатические поля или методы включающего класса, метод также определен как статический.

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

Для начальной загрузки сайта вызова любая вызываемая динамическая инструкция в настоящее время делегируется классу LambdaMetafactory который включен в библиотеку классов Java. Затем эта фабрика отвечает за создание класса, который реализует функциональный интерфейс и вызывает соответствующий метод, содержащий тело лямбда-выражения, которое, как описано выше, хранится в исходном классе. Однако в будущем этот процесс начальной загрузки может измениться, что является одним из основных преимуществ использования invokedynamic для реализации лямбда-выражений. Если в один прекрасный день для реализации лямбда-выражений будет доступна более подходящая языковая функция, текущая реализация может быть просто заменена.

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

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

В качестве примера рассмотрим следующее лямбда-выражение:

1
2
3
4
5
6
class Foo {
  int i;
  void bar(int j) {
    Consumer consumer = k -> System.out.println(i + j + k);
  }
}

Для выполнения лямбда-выражения требуется доступ как к включающему экземпляру Foo и к значению j его включающего метода. Следовательно, версия описанного выше класса с десугардом выглядит примерно так, где вызыванная динамическая инструкция представлена ​​некоторым псевдокодом:

1
2
3
4
5
6
7
8
9
class Foo {
  int i;
  void bar(int j) {
    Consumer consumer = <invokedynamic(this, j)>;
  }
  private /* non-static */ void lambda$foo$0(int j, int k) {
    System.out.println(this.i + j + k);
  }
}

Чтобы иметь возможность вызывать lambda$foo$0 , включающий экземпляр Foo и переменную j передаются фабрике, которая связана с командой invokedyanmic. Затем эта фабрика получает переменные, необходимые для создания экземпляра сгенерированного класса. Этот сгенерированный класс будет выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class Foo$$Lambda$0 implements Consumer {
  private final Foo _this;
  private final int j;
  private Foo$$Lambda$0(Foo _this, int j) {
    this._this = _this;
    this.j = j;
  }
  private static Consumer get$Lambda(Foo _this, int j) {
    return new Foo$$Lambda$0(_this, j);
  }
  public void accept(Object value) { // type erasure
    _this.lambda$foo$0(_this, j, (Integer) value);
  }
}

В конце концов, фабричный метод сгенерированного класса привязывается к сайту вызываемого динамического вызова через дескриптор метода, который содержится в ConstantCallSite . Однако, если лямбда-выражение полностью не имеет состояния, то есть оно не требует доступа к экземпляру или методу, в который оно LambdaMetafactory , LambdaMetafactory возвращает так называемый дескриптор постоянного метода, который ссылается на нетерпеливо созданный экземпляр сгенерированного класса. Следовательно, этот экземпляр служит в качестве синглтона, который будет использоваться при каждом достижении сайта вызова лямбда-выражения. Очевидно, что это решение по оптимизации влияет на объем памяти вашего приложения и его следует учитывать при написании лямбда-выражений. Кроме того, ни один фабричный метод не добавляется в класс лямбда-выражения без сохранения состояния.

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

Под крышками: лямбда-формы

Лямбда-формы являются подробностями реализации того, как MethodHandles выполняются виртуальной машиной. Однако из-за своего названия лямбда-формы часто путают с лямбда-выражениями. Вместо этого лямбда-формы вдохновлены лямбда-исчислением и получили свое имя по этой причине, а не для фактического использования для реализации лямбда-выражений в OpenJDK.

В более ранних версиях OpenJDK 7 дескрипторы методов могли выполняться в одном из двух режимов. Дескрипторы методов либо напрямую отображались в виде байтового кода, либо отправлялись с использованием явного кода сборки, предоставленного средой выполнения Java. Визуализация байт-кода применялась к любому дескриптору метода, который считался полностью постоянным в течение всего времени жизни класса Java. Однако если JVM не смогла доказать это свойство, дескриптор метода вместо этого был выполнен путем отправки его в предоставленный код сборки. К сожалению, поскольку код сборки не может быть оптимизирован JIT-компилятором Java, это приводит к неконстантным вызовам дескрипторов методов, которые «падают с обрыва производительности». Поскольку это также повлияло на лямбда-выражения с ленивой привязкой, это явно не было удовлетворительным решением.

LambdaForm были введены для решения этой проблемы. Грубо говоря, лямбда-формы представляют инструкции байт-кода, которые, как указано выше, могут быть оптимизированы JIT-компилятором. В OpenJDK семантика вызова MethodHandle сегодня представлена LambdaForm на которую дескриптор несет ссылку. Благодаря этому оптимизируемому промежуточному представлению использование непостоянных MethodHandle стало значительно более производительным. На самом деле, можно даже увидеть скомпилированный LambdaForm байт-код в действии. Просто поместите точку MethodHandle внутри метода начальной загрузки или внутри метода, который вызывается через MethodHandle . Как только точка LambdaForm с байт- LambdaForm будет найден в стеке вызовов.

Почему это важно для динамических языков

Любой язык, который должен выполняться на виртуальной машине Java, должен быть переведен в байт-код Java. И как следует из названия, байт-код Java выравнивается довольно близко к языку программирования Java. Это включает в себя требование определить строгий тип для любого значения, а до введения invokedynamic – вызов метода, необходимого для указания явного целевого класса для отправки метода. Глядя на следующий код JavaScript, указание любой информации, однако, невозможно при переводе метода в байт-код:

1
2
3
function (foo) {
  foo.bar();
}

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

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

Ссылка: Демонтаж invokedynamic от нашего партнера по JCG Рафаэля Винтерхальтера в блоге My daily Java .