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%.
Однако это снижение может почти не сказаться на производительности реальных приложений. Мы запустили несколько бенчмарков сериализации и десериализации с реальными библиотеками и не обнаружили снижения производительности в следующих случаях:
- собственный бенчмарк сериализации и десериализации JSON с использованием Jackson;
- бенчмарк типов конвертеров XStream;
- бенчмарк сериализатора полей Kryo.
Мы продолжим искать возможности повысить производительность, например уточнить форму байт-кода для доступа к полям, чтобы 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перестанет работать.