Черновик JEP: переопределение отражения с помощью дескрипторов методов
Переведено сJEP draft: Reimplement Core Reflection with Method Handles (java.net)
Переводчик: Обработчик метода: Дескриптор метода
Owner | Mandy Chung |
---|---|
Type | Feature |
Scope | JDK |
Status | Submitted |
Component | core-libs / java.lang:reflect |
Effort | M |
Duration | M |
Reviewed by | Alan Bateman, John Rose |
Endorsed by | John Rose |
Created | 2021/04/26 22:41 |
Updated | 2021/07/28 23:19 |
Issue | 8266010 |
Общая
существуетjava.lang.invoke
Реализован заново на основе обработчиков методовjava.lang.reflect.Method
, Constructor
, а такжеField
.使方法句柄作为底层机制的反射将降低java.lang.reflect
а такжеjava.lang.invoke
Стоимость обслуживания и разработки API
нецелевых
не правильноjava.lang.reflect
любые изменения в API. Это всего лишь модификация его базовой реализации.
мотивация
Существует два внутренних механизма отражения вызовов методов и конструкторов. Для быстрого старта, для первых нескольких вызовов определенных методов и конструкторов отражения используются нативные методы виртуальной машины Hotspot.Для повышения производительности после нескольких вызовов она сгенерирует байт-код для операции отражения и затем использует эти байт-коды в вызов
Для доступа к полю внутреннее отражение используетсяsun.misc.Unsafe
API
Представлено в Java 7java.lang.invoke
В API обработки метода существует три различных механизма работы внутреннего отражения:
- Родной метод виртуальной машины
- для
Method::invoke
а такжеConstructor::newInstance
динамически генерируемый байт-код иField::get
а такжеset
Unsafe
механизм доступа к полю - дескриптор метода
когда мы обновляем для поддержки новых языковых функцийjava.lang.reflect
а такжеjava.lang.invoke
, например вProject ValhallaОжидаемые новые функции могут потребовать от нас изменения всех трех путей кода, и эти изменения будут очень дорогими. В дополнение к этому, существующие реализации полагаются на специальную обработку сгенерированного байт-кода виртуальной машиной, которая обернута вjdk.internal.reflect.MagicAccessorImpl
Среди подклассов:
Переводчик: специальная обработка сгенерированного байт-кода виртуальными машинами
- Ослабьте доступность, чтобы эти классы могли обращаться к недоступным полям и методам других классов.
- Контрольная сумма отключена для разрешенияJLS §6.6.2поддержать
Object::clone
отражение - Плохо работающий загрузчик классов будет использоваться для решения проблем безопасности и совместимости.
описывать
Реализован повторно на основе дескриптора методаjava.lang.reflect
Он будет использоваться в качестве базового механизма реализации общего отражения платформы для замены основанного на механизме генерации байт-кода.Method::invoke
, Constructor::newInstance
, Field::get
, а такжеField::set
Для первых нескольких вызовов одного из этих методов отражения для конкретного объекта отражения мы напрямую вызываем дескриптор соответствующего метода. Затем мы определяем вращение (spun) в [скрытом классе](docs.Oracle.com/en/java/ только что….Lookup.html#defineHiddenClassWithClassData(byte[],java.lang.Object,boolean,java.lang.invoke.MethodHandles.Lookup.ClassOption...)) байт-код, который будет получен из [ClassData](docs.Oracle.com/en/java/ только что….String, java.lang.Class)) цель загрузкиMethodHandle
так какДинамически вычисляемые константы. Загрузка дескрипторов методов из констант позволяет виртуальной машине HotSpot встраивать вызовы дескрипторов методов для повышения производительности.
Перед инициализацией механизма дескриптора метода на ранней стадии запуска по-прежнему требуется собственный метод отражения виртуальной машины. это произойдет вSystem::initPhase1
после иSystem::initPhase2
Раньше, после этого мы переключимся только на использование дескрипторов методов. Благоприятно за счет уменьшения кадров стека собственных методовProject Loom
Микробенчмарки показывают, что члены экземпляра наMethod::invoke
,Field::get
а такжеField::set
Производительность новой реализации выше, чем у старой реализации.Field::get
Производительность на статических полях сравнима со старой реализацией.Field::set
в статических полях иConstructor::newInstance
производительность немного ниже. используется на 32 методахMethod::invoke
увеличено время холодного запуска с 64 мс до 70 мс для простого приложения. Мы продолжим работать над решением этих небольших проблем.
Этот подход уменьшит стоимость обновления поддержки отражения для новых функций языка и позволит нам дополнительноMagicAccessorImpl
Специальная обработка подклассов для упрощения виртуальной машины HotSpot.
чувствительный к вызывающему абоненту метод
чувствительный к вызывающему абонентуМетод — это метод, поведение которого зависит от класса его непосредственного вызывающего объекта. Реализации методов, чувствительных к вызывающей стороне, выполняют обход стека, чтобы найти своих непосредственных вызывающих, пропуская любые кадры стека, введенные механизмом внутреннего отражения.
Вот некоторые чувствительные к вызывающей стороне методы на платформе:
-
Class::forName(String)
Используйте загрузчик класса его вызывающего класса, чтобы загрузить именованный класс и выполнить проверки разрешений, когда диспетчер безопасности включен. -
Method::invoke
,Constructor::newInstance
,Field::getX
а такжеField::setX
выполнить проверку доступа к классу вызывающего объекта, если он не переданsetAccessible(true)
Подавить проверки доступа. -
MethodHandles::lookup
использует класс своего вызывающего объекта в качестве возвращаемогоLookup
Класс поиска объекта.
Этот пример кода показывает, как передатьMethod::invoke
Вызвать метод, чувствительный к вызывающей сторонеCSM::returnCallerClass
.
class CSM {
@CallerSensitive static Class<?> returnCallerClass() {
return Reflection.getCallerClass();
}
}
class Foo {
void test() throws Throwable {
// calling CSM::returnCallerClass via reflection
var m = CSM.class.getMethod("returnCallerClass");
// expect Foo to be the caller class
var caller = m.invoke(null);
assert(caller == Foo.class);
}
}
первый,Method::invoke
ОбнаружитьFoo
Проверено как его непосредственный вызывающий абонент. это проверяетFoo
Можно ли получить доступCSM::returnCallerClass
. затем он звонит задумчивоCSM::returnCallerClass
. из-заCSM::returnCallerClass
— это чувствительный к вызывающему объекту метод, который находит свой непосредственный вызывающий класс, пропускает кадр стека отражения и возвращает его. при этих обстоятельствах,CSM::returnCallerClass
ОбнаружитьFoo
как вызывающий класс. Стек выглядит так:
CSM.returnCallerClass
jdk.internal.reflect.NativeMethodAccessorImpl.invoke0
jdk.internal.reflect.NativeMethodAccessorImpl.invoke
jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke
java.lang.reflect.Method.invoke
Foo.test
:
:
Обратите внимание, что обход стека для поиска вызывающего класса выполняется дважды, один раз дляMethod::invoke
, один раз заCSM::returnCallerClass
.
Обработчик метода вызывает метод, чувствительный к вызывающей стороне
Если делается запрос на получение дескриптора метода, чувствительного к вызывающей стороне, применяются общие правила поведения байт-кода, но они ищут класс особым образом. Результирующий дескриптор метода ведет себя так, как если бы он был вызван из инструкции, содержащейся в классе поиска, поэтому методы, чувствительные к вызывающей стороне, обнаруживают класс поиска. (Напротив, вызывающая сторона дескриптора метода игнорируется.) Таким образом, в случае метода, чувствительного к вызывающей стороне, разные классы поиска могут создавать дескрипторы методов, которые ведут себя по-разному.
Из-за такого поведения методов, чувствительных к вызывающей стороне, вызовы через дескриптор методаMethod::invoke
Вызов целевого метода, чувствительного к вызывающей стороне, работает неправильно. Например,Bar
Вызывается цепным отражениемCSM::returnCallerClass
,Следующим образом:
class Bar {
void test() throws Throwable {
//Method::invoke的方法句柄
MethodHandle mh = MethodHandles.lookup()
.findVirtual(Method.class, "invoke",
methodType(Object.class, Object.class, Object[].class));
// CSM::returnCallerClass的反射对象
Method m = CSM.class.getMethod("returnCallerClass");
//通过方法句柄和目标函数调用Method::invoke
// 被反射调用是CSM::returnCallerClass
var caller = mh.invoke(m, null, null);
assert(caller == Bar.class); // Fail!
}
}
Разумно ожидать этого звонкаCSM::returnCallerClass
Поведение вызова отражения цепочки должно быть таким же, как и статического вызова.CSM::returnCallerClass
Поведение такое же, когдаBar
Должен быть возвращенный класс. Однако текущая реализация возвращает неверный класс вызывающего объекта.
В приведенном ниже стеке показана внутренняя реализация, включая скрытый фрейм, который показывает класс вызывающего объекта, найденный при обходе стека. с другой стороны,Method::invoke
вызывается через дескриптор метода.Method::invoke
Вы должны вести себя так, как будто этоLookup
Вызов класса поиска объекта создает указанный дескриптор метода, т.е.Bar
.
CSM.returnCallerClass()
jdk.internal.reflect.NativeMethodAccessorImpl.invoke0
jdk.internal.reflect.NativeMethodAccessorImpl.invoke
jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke
java.lang.reflect.Method.invoke(mh)
java.lang.invoke.DirectMethodHandle$Holder.invokeSpecial
java.lang.invoke.LambdaForm$MH/0x0000000800003000.invoke
java.lang.invoke.LambdaForm$MH/0x0000000800004400.invokeExact_MT
Bar$$InjectedInvoker/0x0000000800003400.invoke_V <--- caller
java.lang.invoke.DirectMethodHandle$Holder.invokeStatic
java.lang.invoke.LambdaForm$MH/0x0000000800004000.invoke
java.lang.invoke.LambdaForm$MH/0x0000000800003c00.invoke_MT
Bar.test
:
:
В этом примере показана ошибка в текущей реализации: два метода, чувствительных к вызывающей стороне, работают некорректно, если вызов вызывается посредством отражения цепочки, полагаясь на обход стека для поиска вызывающей стороны.
Текущая реализация вводит скрытый классBar$$InjectedInvoker/0x0000000800003400
, что то же самое, чтоBar
в том же пакете среды выполнения и одним и тем жеBar
То же определение загрузчика определения с тем же доменом защиты. При обходе стека будет найденоBar$$InjectedInvoker/0x0000000800003400
как класс вызывающего вместоBar
. Этот подход работает для методов, чувствительных к вызывающему объекту, которые полагаются на пакеты среды выполнения, загрузчики определений или домен защиты вызывающего класса, но не работает для тех, которым требуется точный класс вызывающего объекта.MethodHandles::lookup
позвонить (см. 8013527а также8257874Узнать больше).
Специальные последовательности вызовов для методов, чувствительных к вызывающей стороне
В новой реализации представлена специальная последовательность вызовов для методов, чувствительных к вызывающей стороне. Методы, чувствительные к вызывающему объекту, могут предоставить частный адаптер с тем же именем, но с дополнительным параметром Class рядом с ним. При вызове чувствительного к вызывающей стороне метода через базовое отражение или дескриптор метода он ищет наличие метода адаптера с параметром класса. Если он найден, он вызовет метод адаптера с параметром класса вызывающей стороны. Эта специальная последовательность вызовов гарантирует, что будет передан один и тот же класс вызывающего объекта.Method::invoke
,MethodHandle::invokeExact
или смесь этих методов, переданных методу, чувствительному к вызывающей стороне.
Например,CSM::returnCallerClass
и его метод адаптера будет выглядеть так:
class CSM {
@CallerSensitive static Class<?> returnCallerClass() {
return returnCallerClass(Reflection.getCallerClass());
}
private static Class<?> returnCallerClass(Class<?> caller) {
return caller;
}
}
В новой реализации стек для приведенного выше примера выглядит так:
CSM.returnCallerClass(caller) <--- adaptor method
java.lang.invoke.DirectMethodHandle$Holder.invokeStatic
java.lang.invoke.Invokers$Holder.invokeExact_MT
jdk.internal.reflect.DirectMethodAccessorImpl$CallerSensitiveWithCaller.invoke
java.lang.reflect.Method.invoke
Foo.test
:
:
а также
CSM.returnCallerClass(caller) <--- adaptor method
java.lang.invoke.DirectMethodHandle$Holder.invokeStatic
java.lang.invoke.Invokers$Holder.invokeExact_MT
jdk.internal.reflect.DirectMethodAccessorImpl$CallerSensitiveWithCaller.invoke
java.lang.reflect.Method.invoke(caller, m) <--- adaptor method
java.lang.invoke.DirectMethodHandle$Holder.invokeSpecial
java.lang.invoke.LambdaForm$MH/0x0000000800004000.invoke
java.lang.invoke.LambdaForm$MH/0x0000000800003c00.invoke_MT
Bar.test
:
:
CSM::returnCallerClass
а такжеMethod::invoke
Оба могут иметь метод адаптера, определяющий параметры класса вызывающего объекта.Foo
передачаMethod::invoke
, он просматривает стек, чтобы найти класс вызывающего объекта. Он передает класс вызывающего абонента непосредственноCSM::returnCallerClass
Метод адаптера.
По аналогии,Bar
Вызывается дескриптором методаMethod::invoke
звонитьCSM::returnCallerClass
. при этих обстоятельствах,MethodHandle::invokeExact
Используйте метод, который создает дескрипторLookup
Класс поиска объекта действует как класс вызывающего объекта, поэтому обход стека не требуется. класс поискаBar
. это начнется сBar
вызов класса в качестве абонентаMethod::invoke
, который, в свою очередь, вызываетBar
как звонившийCSM::returnCallerClass
Метод адаптера. Новая реализация устраняет необходимость многократного обхода стека при рефлексивном вызове методов, чувствительных к вызывающей стороне.
Для чувствительных к вызывающему объекту методов, которым требуются точные классы вызывающего объекта, необходимо определить метод адаптера, чтобы обеспечить корректность.MethodHandles::lookup
а такжеClassLoader::registerAsParallelCapable
— единственные два метода в JDK, для которых требуется точный класс вызывающего объекта.
С другой стороны, методы адаптера являются необязательными для методов, чувствительных к вызывающей стороне, которые используют класс вызывающей стороны для проверки доступа или проверки разрешений безопасности, т. е. на основе его пакета среды выполнения, загрузчика определений или домена защиты.
Новая реализация будет поддерживать отражающий вызов методов, чувствительных к вызывающей стороне, с адаптером или без него, используя специальную последовательность вызова.
Опции
Вариант 1: ничего не делать
Сохраните существующую реализацию основного отражения, чтобы избежать любых рисков совместимости. Динамический байт-код, сгенерированный для отражения, останется в файле класса версии 49, и виртуальная машина продолжит специально обрабатывать такой байт-код.
Мы отвергаем этот вариант, потому что
- возобновить
java.lang.reflect
а такжеjava.lang.invoke
Стоимость специализации на примитивных классах и дженериках для поддержки Project Valhalla будет высокой. - Дополнительные специальные правила в виртуальной машине могут потребоваться для поддержки новых языковых функций в рамках ограничений старого формата файла класса, и
- Project Loom нужно было найти способ обработки нативных фреймов стека, представленных посредством отражения ядра.
Вариант 2. Перейдите на новую библиотеку байт-кода.
Заменяет средство записи байт-кода, используемое отражением, для использования новой библиотеки байт-кода, которая развивалась вместе с форматом файла класса, но сохраняет существующую реализацию отражения и продолжает специально обрабатывать динамически создаваемые байт-коды отражения.
Эта альтернатива имеет меньший риск совместимости, чем та, которую мы представили выше, но все еще требует много работы, и у нее все еще есть первый и последний недостатки первой альтернативы.
тестовое задание
Тщательное тестирование гарантирует, что новые реализации будут надежными и совместимыми с существующим поведением. Тестирование производительности гарантирует отсутствие значительного снижения производительности по сравнению с текущей реализацией. Мы будем поощрять разработчиков использовать выпуски с ранним доступом для тестирования как можно большего количества библиотек и фреймворков, чтобы помочь нам определить любое поведение или снижение производительности.
Во многих случаях микротесты не показали значительного снижения или улучшения производительности. Мы продолжим изучать возможности для повышения производительности.
ориентир
Benchmark Mode Cnt Score Error Units
ReflectionFields.getInt_instance_field avgt 10 8.058 ± 0.003 ns/op
ReflectionFields.getInt_instance_field_var avgt 10 7.576 ± 0.097 ns/op
ReflectionFields.getInt_static_field avgt 10 5.937 ± 0.002 ns/ops
ReflectionFields.getInt_static_field_var avgt 10 6.810 ± 0.027 ns/ops
ReflectionFields.setInt_instance_field avgt 10 5.102 ± 0.023 ns/ops
ReflectionFields.setInt_instance_field_var avgt 10 5.139 ± 0.006 ns/ops
ReflectionFields.setInt_static_field avgt 10 4.245 ± 0.002 ns/ops
ReflectionFields.setInt_static_field_var avgt 10 3.920 ± 0.003 ns/ops
ReflectionMethods.class_forName_1arg avgt 10 407.448 ± 0.823 ns/ops
ReflectionMethods.class_forName_1arg_var avgt 10 418.611 ± 8.790 ns/ops
ReflectionMethods.class_forName_3arg avgt 10 366.685 ± 5.713 ns/ops
ReflectionMethods.class_forName_3arg_var avgt 10 359.410 ± 3.926 ns/ops
ReflectionMethods.instance_method avgt 10 17.428 ± 0.020 ns/ops
ReflectionMethods.instance_method_var avgt 10 20.249 ± 0.065 ns/ops
ReflectionMethods.static_method avgt 10 18.843 ± 0.035 ns/ops
ReflectionMethods.static_method_var avgt 10 19.460 ± 0.050 ns/ops
новая реализация
Benchmark Mode Cnt Score Error Units
ReflectionFields.getInt_instance_field avgt 10 6.361 ± 0.002 ns/op
ReflectionFields.getInt_instance_field_var avgt 10 5.976 ± 0.112 ns/op
ReflectionFields.getInt_static_field avgt 10 5.946 ± 0.003 ns/op
ReflectionFields.getInt_static_field_var avgt 10 6.372 ± 0.014 ns/op
ReflectionFields.setInt_instance_field avgt 10 4.672 ± 0.013 ns/op
ReflectionFields.setInt_instance_field_var avgt 10 3.933 ± 0.009 ns/op
ReflectionFields.setInt_static_field avgt 10 4.661 ± 0.001 ns/op
ReflectionFields.setInt_static_field_var avgt 10 3.953 ± 0.014 ns/op
ReflectionMethods.class_forName_1arg avgt 10 404.300 ± 1.423 ns/op
ReflectionMethods.class_forName_1arg_var avgt 10 402.458 ± 0.418 ns/op
ReflectionMethods.class_forName_3arg avgt 10 394.287 ± 3.443 ns/op
ReflectionMethods.class_forName_3arg_var avgt 10 377.586 ± 0.270 ns/op
ReflectionMethods.instance_method avgt 10 13.645 ± 0.019 ns/op
ReflectionMethods.instance_method_var avgt 10 13.811 ± 0.029 ns/op
ReflectionMethods.static_method avgt 10 13.723 ± 0.026 ns/op
ReflectionMethods.static_method_var avgt 10 13.164 ± 0.046 ns/op
Риски и предположения
Код, который сильно зависит от существующих реализаций и недокументирован, может быть затронут. Чтобы уменьшить этот риск совместимости, в качестве обходного пути вы можете передать-Djdk.reflect.useDirectMethodHandle=false
Включите старую реализацию.
- Внутренне созданные классы отражения (т.е.
MagicAccessorImpl
подкласс ) больше не будет работать и должен быть обновлен. - Вызовы дескрипторов методов могут потреблять больше ресурсов, чем старые реализации отражения. Такие вызовы включают в себя вызов нескольких методов Java, чтобы гарантировать, что члены объявляющего класса инициализируются перед доступом к ним, и поэтому может потребоваться больше места в стеке для необходимых кадров выполнения. Это может привести к
StackOverflowError
, или, если брошено при инициализации классаStackOverflowError
, приведет кNoClassDefFoundError
.