openjdk.ruOpenJDK на русском

JEP 416: Reimplement Core Reflection with Method Handles

Новая реализация базовой рефлексии (core reflection) на основе дескрипторов методов (method handles)

ОтветственныйMandy Chung
ТипFeature
ОбластьJDK
СтатусClosed / Delivered
Выпуск18
Компонентcore-libs / java.lang:reflect
Обсуждениеcore dash libs dash dev at openjdk dot java dot net
ТрудоёмкостьM
ДлительностьM
РецензентыAlan Bateman, John Rose
ОдобренJohn Rose
Создан2021/04/26 22:41
Обновлён2024/03/07 18:49
Задача8266010

Аннотация

Заново реализовать java.lang.reflect.Method, Constructor и Field на основе дескрипторов методов (method handles) java.lang.invoke. Если дескрипторы методов станут базовым механизмом рефлексии, затраты на сопровождение и разработку API java.lang.reflect и java.lang.invoke снизятся.

Что не является целью

Мы не ставим цель как-либо менять API java.lang.reflect. Меняется только реализация.

Мотивация

В базовой рефлексии (core reflection) есть два внутренних механизма для вызова методов и конструкторов. Для быстрого запуска первые несколько вызовов конкретного рефлексивного объекта метода или конструктора выполняются через нативные методы в HotSpot VM. Для лучшей пиковой производительности после определённого числа вызовов для рефлексивной операции генерируется байт-код, который используется при последующих вызовах.

Для доступа к полям базовая рефлексия использует внутренний API sun.misc.Unsafe.

С появлением API дескрипторов методов java.lang.invoke в Java 7 всего стало три разных внутренних механизма для рефлексивных операций:

  • нативные методы VM;

  • динамически генерируемые заглушки байт-кода для Method::invoke и Constructor::newInstance, а также доступ к полям через Unsafe для Field::get и set;

  • дескрипторы методов.

Когда мы обновляем java.lang.reflect и java.lang.invoke для поддержки новых возможностей языка, например тех, что планируются в Project Valhalla, нам приходится менять все три пути в коде, а это дорого. Кроме того, текущая реализация полагается на то, что VM особым образом обрабатывает сгенерированный байт-код, который обёрнут в подклассы jdk.internal.reflect.MagicAccessorImpl:

  • ограничения доступа ослаблены, чтобы эти классы могли обращаться к недоступным полям и методам других классов;

  • верификация отключена, чтобы обойти JLS §6.6.2 и поддержать рефлексию для Object::clone;

  • используется загрузчик классов с нестандартным поведением, чтобы обойти некоторые проблемы безопасности и совместимости.

Описание

Заново реализовать java.lang.reflect на основе дескрипторов методов как общего базового механизма рефлексии платформы, заменив реализации Method::invoke, Constructor::newInstance, Field::get и Field::set, которые генерируют байт-код.

Новая реализация напрямую вызывает дескрипторы методов для конкретных рефлексивных объектов. Нативный механизм рефлексии VM мы используем только на раннем этапе запуска VM, пока механизм дескрипторов методов не инициализирован. Это происходит вскоре после System::initPhase1 и до System::initPhase2, после чего мы переходим исключительно на дескрипторы методов. Это полезно для Project Loom, потому что сокращает использование нативных кадров стека.

Для оптимальной производительности экземпляры Method, Constructor и Field следует хранить в полях static final, чтобы JIT мог свернуть их как константы. В этом случае микробенчмарки показывают, что новая реализация значительно быстрее старой: на 43–57%.

Когда экземпляры Method, Constructor и Field хранятся в неконстантных полях (например, в поле без final или в элементе массива), микробенчмарки показывают некоторое снижение производительности. Если экземпляры Field нельзя свернуть как константы, доступ к полям значительно медленнее, чем в старой реализации: на 51–77%.

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

Мы продолжим искать возможности повысить производительность, например уточнить форму байт-кода для доступа к полям, чтобы JIT мог надёжно оптимизировать конкретные MethodHandle и VarHandle независимо от того, является ли получатель константой.

Новая реализация снизит затраты на доработку поддержки рефлексии для новых возможностей языка, а также позволит нам упростить HotSpot VM, убрав особую обработку подклассов MagicAccessorImpl.

Альтернативы

Альтернатива 1: ничего не делать

Сохранить существующую реализацию базовой рефлексии, чтобы избежать любого риска несовместимости. Динамический байт-код, генерируемый для базовой рефлексии, остался бы на версии class-файла 49, а VM продолжала бы обрабатывать такой байт-код особым образом.

Мы отвергаем эту альтернативу, потому что:

  • обновление java.lang.reflect и java.lang.invoke для поддержки примитивных классов (primitive classes) и специализации обобщённых типов из Project Valhalla обошлось бы дорого;

  • для поддержки новых возможностей языка в рамках ограничений старого формата class-файлов в VM, вероятно, понадобились бы дополнительные особые правила;

  • Project Loom пришлось бы искать способ справиться с появлением нативных кадров стека из-за базовой рефлексии.

Альтернатива 2: перейти на новую библиотеку байт-кода

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

У этой альтернативы меньше риск несовместимости, чем у предложенного выше варианта, но она всё равно требует значительного объёма работы, и у неё остаются первый и последний недостатки первой альтернативы.

Тестирование

Всестороннее тестирование позволит убедиться, что реализация надёжна и совместима с существующим поведением. Тестирование производительности позволит убедиться, что по сравнению с текущей реализацией нет неожиданного значительного снижения производительности. Мы будем призывать разработчиков, использующих сборки Early Access (ранний доступ), протестировать как можно больше библиотек и фреймворков, чтобы помочь нам выявить любые регрессии поведения или производительности.

Базовая версия

Benchmark                                     Mode  Cnt   Score  Error  Units
ReflectionSpeedBenchmark.constructorConst     avgt   10  68.049 ± 0.872  ns/op
ReflectionSpeedBenchmark.constructorPoly      avgt   10  94.132 ± 1.805  ns/op
ReflectionSpeedBenchmark.constructorVar       avgt   10  64.543 ± 0.799  ns/op
ReflectionSpeedBenchmark.instanceFieldConst   avgt   10  35.361 ± 0.492  ns/op
ReflectionSpeedBenchmark.instanceFieldPoly    avgt   10  67.089 ± 3.288  ns/op
ReflectionSpeedBenchmark.instanceFieldVar     avgt   10  35.745 ± 0.554  ns/op
ReflectionSpeedBenchmark.instanceMethodConst  avgt   10  77.925 ± 2.026  ns/op
ReflectionSpeedBenchmark.instanceMethodPoly   avgt   10  96.094 ± 2.269  ns/op
ReflectionSpeedBenchmark.instanceMethodVar    avgt   10  80.002 ± 4.267  ns/op
ReflectionSpeedBenchmark.staticFieldConst     avgt   10  33.442 ± 2.659  ns/op
ReflectionSpeedBenchmark.staticFieldPoly      avgt   10  51.918 ± 1.522  ns/op
ReflectionSpeedBenchmark.staticFieldVar       avgt   10  33.967 ± 0.451  ns/op
ReflectionSpeedBenchmark.staticMethodConst    avgt   10  75.380 ± 1.660  ns/op
ReflectionSpeedBenchmark.staticMethodPoly     avgt   10  93.553 ± 1.037  ns/op
ReflectionSpeedBenchmark.staticMethodVar      avgt   10  76.728 ± 1.614  ns/op

Новая реализация

Benchmark                                     Mode  Cnt    Score   Error  Units
ReflectionSpeedBenchmark.constructorConst     avgt   10   32.392 ± 0.473  ns/op
ReflectionSpeedBenchmark.constructorPoly      avgt   10  113.947 ± 1.205  ns/op
ReflectionSpeedBenchmark.constructorVar       avgt   10   76.885 ± 1.128  ns/op
ReflectionSpeedBenchmark.instanceFieldConst   avgt   10   18.569 ± 0.161  ns/op
ReflectionSpeedBenchmark.instanceFieldPoly    avgt   10   98.671 ± 2.015  ns/op
ReflectionSpeedBenchmark.instanceFieldVar     avgt   10   54.193 ± 3.510  ns/op
ReflectionSpeedBenchmark.instanceMethodConst  avgt   10   33.421 ± 0.406  ns/op
ReflectionSpeedBenchmark.instanceMethodPoly   avgt   10  109.129 ± 1.959  ns/op
ReflectionSpeedBenchmark.instanceMethodVar    avgt   10   90.420 ± 2.187  ns/op
ReflectionSpeedBenchmark.staticFieldConst     avgt   10   19.080 ± 0.179  ns/op
ReflectionSpeedBenchmark.staticFieldPoly      avgt   10   92.130 ± 2.729  ns/op
ReflectionSpeedBenchmark.staticFieldVar       avgt   10   53.899 ± 1.051  ns/op
ReflectionSpeedBenchmark.staticMethodConst    avgt   10   35.907 ± 0.456  ns/op
ReflectionSpeedBenchmark.staticMethodPoly     avgt   10  102.895 ± 1.604  ns/op
ReflectionSpeedBenchmark.staticMethodVar      avgt   10   82.123 ± 0.629  ns/op

Риски и допущения

Это может затронуть код, который зависит от сугубо специфичных для реализации и недокументированных особенностей существующей реализации. Чтобы снизить этот риск несовместимости, в качестве обходного пути можно включить старую реализацию с помощью -Djdk.reflect.useDirectMethodHandle=false.

  • Код, который анализирует внутренние сгенерированные классы рефлексии (то есть подклассы MagicAccessorImpl), больше не будет работать, и его нужно будет обновить.

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

  • Мы удалим старую реализацию базовой рефлексии в одном из будущих выпусков. С этого момента обходной путь -Djdk.reflect.useDirectMethodHandle=false перестанет работать.