Черновик JEP: переопределение отражения с помощью дескрипторов методов

Java

Черновик 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.UnsafeAPI

Представлено в 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.