﻿# Method Handles быстрее рефлексии \(иногда\)

Как Java поддерживает динамические вызовы? От медленной рефлексии до оптимизированных MethodHandle и invokedynamic — изучаем эволюцию динамизма в JVM\. Разбираем внутреннее устройство MethodHandle и какие роли играют CallSite и invokedynamic\.

![1266_method_handles_faster_ru/image1.png](https://import.viva64.com/docx/blog/1266_method_handles_faster_ru/image1.png)

## Зачем в Java нужны динамические вызовы?

Вернёмся назад во времени: через год после Java 1\.0 выходит следующая версия, включающая первую итерацию механизма рефлексии\. Теперь Java\-код может получить информацию о классах \(их поля, методы, конструкторы\) и — самое важное — динамически манипулировать объектами во время выполнения, даже если их типы неизвестны на этапе компиляции\. Вскоре после этого выходит один из первых популярных динамически типизированных языков на базе JVM — JPython, активно использующий рефлексию\.

Но зачем это самой Java? Представьте:

* Вам требуется проанализировать аннотации в классе\.
* Вы разрабатываете фреймворк, которому необходимо работать с классами, о существовании которых он никак не мог знать на этапе компиляции\.
* Вам нужно вызывать метод, имя которого хранится в конфигурационном файле\.

Рефлексия предоставляет необходимую гибкость для решения таких задач\. Однако её использование имеет недостаток в производительности\.

## Накладные расходы и ограничения рефлексии

Использовав рефлексию для реализации динамического вызова методов в рамках JVM, разработчики столкнулись с основными её проблемами, актуальными по сей день\. 

Во\-первых, все операции рефлексии работают с типом `Object`, что делает невозможным проверку типов во время компиляции и откладывает все ошибки на этап выполнения\. 

Во\-вторых, каждый вызов через рефлексию сопровождается проверками доступа и типов, что существенно снижает производительность\. 

В\-третьих, JIT\-компилятор не может оптимизировать такие вызовы: они не инлайнятся, не компилируются в машинный код и не анализируются при профилировании\.

> Инлайнинг — замена вызова метода его телом, ускоряющая код за счёт устранения накладных расходов\. JIT после инлайнинга может применить дополнительные оптимизации \(например, удаление мёртвого кода\)\. Эффективен для небольших методов\.

Эти проблемы не решаемы в рамках рефлексии, потому что динамическая работа с типами во время выполнения — это её суть\. Именно поэтому её компромиссы \(отложенные ошибки, производительность, отсутствие оптимизаций\) — неотъемлемая плата за гибкость, а не исправимые недостатки\.

## Что и как решают MethodHandle

Решением проблем рефлексии \(но не полной заменой\) стал [JSR 292](https://www.jcp.org/en/jsr/detail%3Fid%3D292), который привнёс в Java `MethodHandle` API\. В отличие от рефлексии, `MethodHandle` API инкапсулирует вызов метода, а не отражает его метаданные\. Это позволяет JVM применять оптимизации, включая инлайнинг и спекулятивную компиляцию\.

> Спекулятивная компиляция — оптимизация JIT, где код компилируется оптимизированным для вероятных \(на основе статистики\) путей выполнения с использованием предположений \(о типах, значениях и так далее\), чтобы избежать задержек при вызове\. При ошибке прогноза результат оптимизации отменяется \(деоптимизация\)\.

Для создания `MethodHandle` используется `MethodHandles.Lookup` — фабрика с контролем доступа\.

Основными методами при работе с самим `MethodHandle` являются `#invokeExact ` и `#invoke`\. Взглянем на них:

```cpp
@IntrinsicCandidate
public final native @PolymorphicSignature Object invokeExact(Object... args)
    throws Throwable;

@IntrinsicCandidate
public final native @PolymorphicSignature Object invoke(Object... args)
    throws Throwable;
```

Хотя методы и объявлены как принимающие и возвращающие `Object`, аннотация `@PolyMorphicSignature` заставляет игнорировать это объявление\. Вместо этого реальные типы аргументов и возвращаемого значения определяются в момент компиляции на основе типов переданных аргументов и возвращаемого значения\.

Для разработчика это означает, что метод работает с конкретными типами в своём коде\.

То есть нельзя использовать `MethodHandle` вместе с `Object`\.

Например, создадим `MethodHandle`, сравнивающий числа через `Integer#compare`:

```cpp
MethodHandle Integer_compare = MethodHandles.lookup().findStatic(
        Integer.class, "compare",
        MethodType.methodType(int.class, int.class, int.class)
);
```

Не получится вызвать `#invokeExact`, не указав, что результатом будет `int` и произойдёт падение во время выполнения:

```cpp
Object result = Integer_compare.invokeExact(1, 2); // WrongMethodTypeException!
```

Код будет работать только с явным приведением к `int`:

```cpp
int result = (int) Integer_compare.invokeExact(1, 2);
```

Вызов `#invokeExact` или `#invoke` не является прямым\. Это интринсики, которые виртуальная машина заменяет на оптимизированный код\.

> Интринсик — это метод, который JVM распознаёт специальным образом и во время выполнения заменяет высокооптимизированным нативным кодом или машинными инструкциями\.

`#invokeExact` требует полного совпадения сигнатур, а `#invoke` допускает преобразования типов как при обычном вызове метода\. Однако, если сигнатура вызова неизвестна или требуется максимальная гибкость, существует ещё один способ взаимодействия с `MethodHandle`:

```cpp
public Object invokeWithArguments(Object... arguments) throws Throwable {
    ....
}
```

В отличие от остальных методов, `#invokeWithArguments` реально использует `Object` и динамически проверяет типы аргументов во время выполнения\. Это универсально, но бьёт по производительности\.

Пусть `#invokeExact`/`#invoke` и являются интринсиками, их нельзя просто взять и заменить на вызов метода\. JVM сначала преобразовывает вызов в `LambdaForm` — внутреннее промежуточное представление, раскладывающее вызов на более примитивные операции\.

## LambdaForm внутри MethodHandle

Перед разбором `LambdaForm` отметим, что это непубличная деталь реализации в OpenJDK\. В других JDK `LambdaForm` может сильно отличаться либо отсутствовать вовсе \(например, в [ART](https://source.android.com/docs/core/runtime)\)\.

Итак, `LambdaForm` представляет собой последовательность низкоуровневых операций\. И последовательность эту можно менять, используя методы в классе `MethodHandles`\.

Возьмём для примера следующий `MethodHandle`:

```cpp
MethodHandle Integer_compare = MethodHandles.lookup().findStatic(
        Integer.class, "compare",
        MethodType.methodType(int.class, int.class, int.class)
);
```

В упрощённом виде его `LambdaForm` выглядит следующим образом:

```cpp
(a0:int, a1:int) => {
    return Integer.compare(a0, a1);
}
```

Используем декоратор `MethodHandles#permuteArguments`, переставляющий аргументы местами:

```cpp
Integer_compare = MethodHandles.permuteArguments(
        Integer_compare,
        MethodType.methodType(int.class, int.class, int.class),
        1, 0
);
```

Теперь упрощённая версия `LambdaForm` выглядит следующим образом:

```cpp
(a0:int, a1:int) => {
    swap_a0 = a1
    swap_a1 = a0
    return Integer.compare(swap_a0, swap_a1);
}
```

Адаптеры `MethodHandles` создают легковесные `LambdaForm`\-цепочки, позволяя JIT\-компилятору инлайнить логику вызова, специализировать машинный код и устранять промежуточные шаги\. Именно поэтому `#invokeExact` и `#invoke` обгоняют рефлексию после прогрева — они работают с прозрачными для оптимизаций JVM примитивами, а не с чёрным ящиком в лице рефлексии\.

> Прогрев JVM — процесс оптимизации во время работы приложения\. JVM динамически адаптируется к реальной нагрузке, применяя оптимизации JIT, собирая профилировочные данные и настраивая сборку мусора\.

## CallSite над MethodHandle

Представьте, что `MethodHandle` — многофункциональный инструмент \(например, отвёртка с насадками\)\. Казалось бы, идеально для разных задач\. Но вот загвоздка: его адаптеры \(насадки под разные шлицы\) можно зафиксировать только один раз при создании \(будто вы намертво припаиваете насадку к рукоятке\)\. Это удобно, но только в том случае, если задача никогда не меняется\.

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

Окунёмся ещё глубже в метафизический мир: представьте, что вы работаете на конвейере \(например, скриптовый движок\), где:

* Детали \(объект `obj`\) постоянно меняются: сегодня квадратные, завтра — круглые\.
* Требуемые операции \(метод `doSomething`\) разные для каждой детали\.
* Инструменты для обработки \(аргументы `x`, `y`\) тоже разные\.

![1266_method_handles_faster_ru/image2.png](https://import.viva64.com/docx/blog/1266_method_handles_faster_ru/image2.png)

Что происходит с `MethodHandle` в этом хаосе?

Отвёртка с припаянной насадкой подходит только к одной конкретной детали и операции\. Как только приходит новая деталь \(другой тип `obj`\) или нужна другая операция \(другая сигнатура `doSomething`\), ваша отвёртка становится бесполезной\. Приходится выбросить её, с нуля собрать и намертво впаять новую с другой насадкой \(создать `MethodHandle` с правильными адаптерами\) и вставить её в каждое место на конвейере, где она нужна\.

Здесь на помощь приходит `CallSite` — умный держатель с системой быстрой замены: вы устанавливаете его на конвейер и уходите\. При первом запуске вызывается инженер\-наладчик \(bootstrap\-метод\), смотрящий на первую деталь и нужную операцию\. Он находит и вставляет подходящую отвёртку с насадкой в держатель\. Если приходит деталь другой формы, конвейер ломается и вызывается аварийный инженер \(ребиндер, оставленный разработчиком как раз на такой случай\), быстро повторяющий порядок действий инженера\-наладчика\. Конвейер работает дальше\.

Но почему насадка всегда паяется?

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

Как и любой хороший инструмент, `CallSite` бывает разных модификаций:

* `ConstantCallSite` — самый простой вариант: одноразовый держатель, отвёртку к которому можно только намертво припаять\. Ничего поменять нельзя\. Подходит, когда вы точно знаете, что форма деталей и операций неизменна\.
* `MutableCallSite` — держатель с механизмом для быстрой замены отвёртки \(`#setTarget`\)\. Но есть нюанс: система не гарантирует синхронизаций и может произойти так, что несколько инженеров будут пытаться одновременно поменять отвёртку\.
* `VolatileCallSite` — улучшенный вариант предыдущего\. Когда инженер меняет отвёртку, он обязательно включает блокировку \(volatile\-семантика\)\. Это гарантирует, что все рабочие \(потоки\) увидят именно новую отвёртку, а не старую, оставшуюся в чьей\-то памяти \(кэше\)\. \(Подробнее о volatile\-семантике можете узнать в [другой нашей статье](https://pvs-studio.ru/ru/blog/posts/java/1128/)\)\.

Допустим, ваш конвейер сравнивает числа\. 

Устанавливаем держатель с начальной отвёрткой \(обычное сравнение\):

```cpp
CallSite callSite = new MutableCallSite(Integer_compare);
```

Получаем пульт управления для текущей отвёртки в держателе:

```cpp
MethodHandle dynamic_Integer_compare = callSite.dynamicInvoker();
```

Обрабатываем деталь:

```cpp
result = (int) dynamic_Integer_compare.invokeExact(1, 2);
// result = -1
```

И вдруг вам приходит приказ сверху, что числа теперь должны сравниваться в обратном порядке\.  Вы, не останавливая конвейер, меняете порядок сравнения:

```cpp
callSite.setTarget(
    MethodHandles.permuteArguments(
        Integer_compare,
        MethodType.methodType(int.class, int.class, int.class),
        1, 0
    )
);
```

Новая деталь обрабатывается по новой логике:

```cpp
result = (int) dynamic_Integer_compare.invokeExact(1, 2);
// result = 1
```

Главная входная точка этого механизма — байт\-код инструкция `invokedynamic`\. При первом запуске она вызывает bootstrap\-метод для получения `CallSite`, а после обращается уже к нему\.

## MethodHandle быстрее \(и нет\)

Пусть MethodHandle API и позиционируются как высокопроизводительная альтернатива рефлексии, их эффективность сильно зависит от контекста использования\.

При первом вызове `MethodHandle` проявляет схожую с рефлексией производительность или даже меньшую из\-за затрат на компиляцию `LambdaForm` и инициализацию внутренних структур JVM\. Однако уже после активации базовых оптимизаций JIT `MethodHandle` начинают выигрывать за счёт инлайнинга и специализации машинного кода\.

Для проверки этого тезиса воспользуемся [JMH\-бенчмарком](https://gist.github.com/Pupcheg/7deae13a79e77c1de28ef7b9f7f27497) на Oracle OpenJDK 23, сравнивающим прямой вызов, рефлексивный, `#invoke`, `#invokeExact` и `#invokeWithArguments` метода `Integer#compare(int, int)`\.

Результаты "горячего" прогона после оптимизаций JIT\-компилятора:

|Способ|Операций в микросекунду|Среднее время выполнения|Относительная скорость|
|---|---|---|---|
|Прямой вызов|3123,7 ± 64,8|1 нс|1,0x|
|\`MethodHandle\#invokeExact\`|424,6 ± 7,7|2 ± 1 нс|7,4x медленнее|
|\`MethodHandle\#invoke\`|422,0 ± 12,1|2 ± 1 нс|7,4x медленнее|
|\`Method\#invoke\`|195,9 ± 11,1|5 ± 1 нс|15,9x медленнее|
|\`MethodHandle\#invokeWithArguments\`|11,1 ± 0,4|88 ± 2 нс|281,4x медленнее|



Ключевые моменты:

`#invokeExact`/`#invoke` в 7 раз медленнее из\-за проверок соответствия типов, прохода через нативный адаптер и обработки полиморфной сигнатуры\.

`#invokeWithArguments` оказался катастрофически медленным \(в 17,6 раз хуже рефлексии\)\. Такой парадокс объясняется тем, что каждый вызов требует анализа типов, упаковки аргументов и анализа сигнатур, а рефлексия кеширует часть своих проверок после первого вызова\.

Результаты анализа перцентилей в микросекундах на операцию приведены ниже\.

> Перцентиль — значение в наборе данных, ниже которого лежит определённый процент наблюдений\. Например, 90\-й перцентиль \(p0,90\), равный 10 мс, означает, что 90% замеров выполнялись меньше или ровно 10 мс\.

|Метрика|Прямой вызов|\`MethodHandle\#invokeExact\`|\`MethodHandle\#invoke\`|\`Method\#invoke\`|\`MethodHandle\#invokeWithArguments\`|
|---|---|---|---|---|---|
|p0,50|≈ 0 мкс|≈ 0 мкс|≈ 0 мкс|≈ 0 мкс|≈ 0 мкс|
|p0,90|0,1 мкс|0,1 мкс|0,1 мкс|0,1 мкс|0,1 мкс|
|p0,99|0,1 мкс|0,1 мкс|0,1 мкс|0,1 мкс|0,2 мкс|
|p0,9999|12,0 мкс|9,7 мкс|12,5 мкс|7,4 мкс|15,8 мкс|
|p1,0|83,584 мкс|87,3 мкс|85,7 мкс|848,9 мкс|1943,6 мкс|

Пусть типичная производительность методов схожа \(до 99\-го перцентиля\), хвостовые задержки различаются радикально\. Рефлексия имеет пики до 848 мкс, что в 10 раз медленнее `invokeExact`\. `invokeWithArguments` катастрофичен: его максимальное время выполнения \(1943 мкс\) в 22 раза медленнее `invokeExact`\. Это делает его нежелательным для использования в высоконагруженных системах\.

> Хвостовая задержка — значения задержки в наихудших сценариях работы системы, измеряемые высокими перцентилями \(p0,99–p0,9999\)\. Она показывает, насколько медленными могут быть самые долгие операции\.

## Как Java использует MethodHandle \(лямбды, конкатенация, VarHandles\)

Одним из ключевых применений `MethodHandle` API в Java стала реализация лямбда\-выражений\.

Возьмём в качестве примера следующее выражение:

```cpp
UnaryOperator<String> quote = str -> '"' + str + '"';
```

В байт\-коде оно будет выглядеть следующим образом:

```cpp
invokedynamic #7,  0
    // InvokeDynamic #0:apply:()Ljava/util/function/UnaryOperator;

....

private static java.lang.String lambda$main$0(java.lang.String);

....

BootstrapMethods:
  0: #36 REF_invokeStatic java/lang/invoke/LambdaMetafactory
             .metafactory:(....)Ljava/lang/invoke/CallSite;
    Method arguments:
      #28 (Ljava/lang/Object;)Ljava/lang/Object;
      #30 REF_invokeStatic MethodHandleSandbox
              .lambda$main$0:(Ljava/lang/String;)Ljava/lang/String;
      #33 (Ljava/lang/String;)Ljava/lang/String;
```

Точкой входа здесь является вызов `invokedynamic`, которая обращается к bootstrap\-методу, указанному ниже\. Bootstrap\-метод `LambdaMetaFactoty#metafactory` создаёт фабрику лямбд, вызывающих сгенерированный метод `#lambda$main$0`, в котором происходит конкатенация\.

Динамические вызовы используются и в конкатенации: с Java 9 она тоже работает через `invokedynamic` \(bootstrap\-методы находятся в `java.lang.invoke.StringConcatFactory`\), заменяя громоздкий `StringBuilder` и позволяя JIT\-компилятору оптимизировать процесс под конкретные данные\. 

Ещё одним важным применением MethodHandle API стал механизм `VarHandle` — ответ на фундаментальные проблемы `sun.misc.Unsafe`\. В отличие от `Unsafe`, где ручной расчёт офсетов полей мог привести к порче памяти и падениям JVM, `VarHandle` обеспечивает типобезопасный доступ к полям, элементам массива и памяти\. Под капотом он использует `MethodHandle` для генерации специализированных адаптеров операций \(`#get`, `#set`, `#compareAndSet` и так далее\), что позволяет JVM оптимизировать доступ, сохраняя строгую валидацию границ и типов\.

Например, так выглядит атомарный Compare\-And\-Set с помощью `Unsafe`:

```cpp
class MyClass {
    int value;
}

Field Unsafe_theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
Unsafe_theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) Unsafe_theUnsafe.get(null);

long MyClass_value_offset =
    unsafe.objectFieldOffset(MyClass.class.getDeclaredField("value"));

MyClass instance = new MyClass();
boolean result = unsafe.compareAndSwapInt(instance, MyClass_value_offset, 0, 1);
```

И аналогичный вариант с `VarHandle`:

```cpp
class MyClass {
    int value;
}

VarHandle MyClass_value = MethodHandles.privateLookupIn(
                MyClass.class, MethodHandles.lookup()
        )
        .findVarHandle(MyClass.class, "value", int.class);

MyClass instance = new MyClass();
boolean result = MyClass_value.compareAndSet(instance, 0, 1);
```

Здесь мы видим ключевые преимущества `VarHandle`\.

Во\-первых, `VarHandle` исключает опасные ручные расчёты офсетов, которые в `Unsafe` могли привести к повреждению соседних участков памяти или другим фатальным ошибкам\. 

Во\-вторых, его строгая типобезопасность гарантирует соответствие операций объявленному типу поля\. Например, `#compareAndSet` для `int` не позволит использовать аргументы типа `long`, тогда как `Unsafe` оставляет такие риски\. 

В\-третьих, код на `VarHandle` остаётся переносимым между разными реализациями JVM, тогда как поведение `Unsafe` всегда было платформенно\-зависимым\.

## И всё же, когда использовать MethodHandle?

Динамические вызовы в Java исторически опирались на рефлексию\. Её ключевая слабость — производительность: JIT не может оптимизировать вызовы, потому что они для него не прозрачны\.

Решив проблемы рефлексии, `MethodHandle` API стал не её заменой, а альтернативой, позволяющей оптимизировать динамические операции\. Его основное преимущество в контексте вызовов — интеграция с JVM через `invokedynamic`, позволяющая JIT применять оптимизации\. После прогрева `#invokeExact` и `#invoke` работают примерно в 2 раза быстрее рефлексии, хотя и остаются в 7 раз медленнее прямых вызовов\.

Идеальный сценарий использования `MethodHandle` — динамическое выполнение заранее известных операций в горячем коде \(например, реализация специализированных прокси через встроенный в Java механизм [Proxy](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/reflect/Proxy.html)\)\. В случаях, когда сигнатура вызова тоже может поменяться, подключается `CallSite`, который позволяет переключать реализацию на лету, при этом сохраняя производительность\.

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

Эта эволюция — от рефлексии к `MethodHandle` и `CallSite` — отражает один из ключевых трендов Java: стремление сочетать гибкость динамических языков со строгой типизацией и производительностью компилируемого кода\. Сегодня эта триада позволяет Java оставаться строго типизированным языком, способным адаптироваться к разным сценариям выполнения\.