- Invoking Methods
- Finding and Invoking a Method with a Specific Declaration
- Invoking Methods with a Variable Number of Arguments
- Вызов методов через reflection
- Русские Блоги
- Отражение Java и метод Method Invoke (воспроизведено)
- 1. Введение
- Один из принципов рефлексии на самом деле заключается в динамическом генерировании байт-кода, аналогичного приведенному выше, загрузке его в jvm и запуске.
- Во-вторых, получить объект Method
- Три, метод invoke ()
- Исходный код метода invoke выглядит следующим образом
- 1. Проверка разрешения
- 2. Вызовите метод вызова метода MethodAccessor.
- 3. Java-версия
- 4. Сравнение производительности
Invoking Methods
Reflection provides a means for invoking methods on a class. Typically, this would only be necessary if it is not possible to cast an instance of the class to the desired type in non-reflective code. Methods are invoked with java.lang.reflect.Method.invoke() . The first argument is the object instance on which this particular method is to be invoked. (If the method is static , the first argument should be null .) Subsequent arguments are the method’s parameters. If the underlying method throws an exception, it will be wrapped by an java.lang.reflect.InvocationTargetException . The method’s original exception may be retrieved using the exception chaining mechanism’s InvocationTargetException.getCause() method.
Finding and Invoking a Method with a Specific Declaration
Consider a test suite which uses reflection to invoke private test methods in a given class. The Deet example searches for public methods in a class which begin with the string » test «, have a boolean return type, and a single Locale parameter. It then invokes each matching method.
Deet invokes getDeclaredMethods() which will return all methods explicitly declared in the class. Also, Class.isAssignableFrom() is used to determine whether the parameters of the located method are compatible with the desired invocation. Technically the code could have tested whether the following statement is true since Locale is final :
First, note that only testDeet() meets the declaration restrictions enforced by the code. Next, when testDeet() is passed an invalid argument it throws an unchecked java.util.MissingResourceException . In reflection, there is no distinction in the handling of checked versus unchecked exceptions. They are all wrapped in an InvocationTargetException
Invoking Methods with a Variable Number of Arguments
Method.invoke() may be used to pass a variable number of arguments to a method. The key concept to understand is that methods of variable arity are implemented as if the variable arguments are packed in an array.
The InvokeMain example illustrates how to invoke the main() entry point in any class and pass a set of arguments determined at runtime.
Источник
Вызов методов через reflection
Все программисты на Java явно или неявно пользуются reflection для вызова методов. Даже если вы не делали этого сами, это за вас наверняка делают библиотеки или фреймворки, которые вы используете. Давайте посмотрим, как этот вызов устроен внутри и насколько это быстро. Будем глядеть в OpenJDK 8 с последними обновлениями.
Начать изучение стоит собственно с метода Method.invoke. Там делается три вещи:
- Проверяются права доступа на метод.
- Создаётся и запоминается MethodAccessor , если его ещё нет (если данный метод ещё не вызывали через reflection).
- Вызывается MethodAccessor.invoke .
Проверка прав доступа состоит из двух частей. Быстрая проверка устанавливает, что и метод, и содержащий его класс имеют модификаторы public . Если это не так, то проверяется, что у вызывающего класса есть доступ к данному методу. Чтобы узнать вызывающий класс, используется приватный метод Reflection.getCallerClass() . Некоторые люди, кстати, любят им пользоваться в своём коде. В Java 9 появится публичный Stack-Walking API и в высшей степени разумно будет перейти на него.
Известно, что проверки доступа можно отменить, вызвав заранее method.setAccessible(true) . Этот сеттер устанавливает флаг override , позволяющий игнорировать проверки. Даже если вы знаете, что ваш метод публичный, установка setAccessible(true) позволит сэкономить немного времени на проверках.
Давайте посмотрим, сколько разные сценарии съедают времени. Напишем простой класс с публичным и непубличным методами:
Напишем JMH-тест, параметризованный двумя флагами: accessible и nonpublic . Такой у нас будет подготовка:
Я вижу вот такие результаты (3 форка, 5x500ms прогрев, 10x500ms измерение):
(accessible) | (nonpublic) | Time |
---|---|---|
true | true | 5,062 ± 0,056 ns/op |
true | false | 5,042 ± 0,032 ns/op |
false | true | 6,078 ± 0,039 ns/op |
false | false | 5,835 ± 0,028 ns/op |
Действительно если выполнен setAccessible(true) , то получается быстрее всего. При этом без разницы, публичный метод или нет. Если же setAccessible(false) , то оба теста медленнее и непубличный метод чуть медленнее публичного. Впрочем я ожидал, что различие будет сильнее. Главным образом тут помогает, что Reflection.getCallerClass() — это интринсик JIT-компилятора, который в большинстве случаев подменяется просто константой во время компиляции: если JIT-компилятор инлайнит вызов Method.invoke, он знает, в какой метод он его инлайнит, а значит и знает, что должен вернуть getCallerClass() . Дальше проверка по сути сводится к сравнению пакета вызываемого и вызывающего класса. Если бы пакет был разный, проверялась бы ещё иерархия классов.
Что же происходит дальше? Дальше нужно создать объект MethodAccessor . Кстати, несмотря на то что Person.class.getMethod(«getName») всегда вернёт новый экземпляр объекта Method , используемый внутри MethodAccessor будет переиспользован через поле root, что, конечно, приятно. Тем не менее сам getMethod существенно медленнее вызова, поэтому если вы планируете вызывать метод несколько раз, разумно хранить объект Method .
Созданием MethodAccessor ‘а занимается ReflectionFactory. Здесь мы видим два сценария, которые контролируются глобальными настройками JVM:
- Если установлена опция -Dsun.reflect.noInflation=true (по умолчанию выключена), то сразу генерируется вспомогательный класс, который и будет запускать целевой метод.
- Иначе создаётся обёртка DelegatingMethodAccessorImpl , внутрь которой помещается NativeMethodAccessorImpl . В свою очередь он считает, сколько раз данный метод вызывали. Если количество вызовов превысило порог, заданный через -Dsun.reflect.inflationThreshold (по умолчанию 15), то происходит «раздувание» аксессора: генерируется вспомогательный класс, как и в первом сценарии. Если же порог не достигнут, вызов идёт честно через JNI. Хотя реализация на стороне C++ тривиальна, накладные расходы на JNI весьма высоки.
Давайте посмотрим, что будет с нашим тестом, если включить -Dsun.reflect.noInflation=true и если использовать только JNI (для этого зададим большой порог -Dsun.reflect.inflationThreshold=100000000 ):
(accessible) | (nonpublic) | Default | noInflation | JNI-only |
---|---|---|---|---|
true | true | 5,062 ± 0,056 | 4,935 ± 0,375 | 195,960 ± 1,873 |
true | false | 5,042 ± 0,032 | 4,914 ± 0,329 | 194,722 ± 1,151 |
false | true | 6,078 ± 0,039 | 5,638 ± 0,050 | 196,196 ± 0,910 |
false | false | 5,835 ± 0,028 | 5,520 ± 0,042 | 194,626 ± 0,918 |
Здесь и далее все результаты в наносекундах на операцию. Как и ожидалось, JNI существенно медленнее, поэтому включать такой режим неоправданно. Любопытно, что режим noInflation оказался чуть быстрее. Это происходит из-за того, что отсутствует DelegatingMethodAccessorImpl , который убирает необходимость в одной косвенной адресации. По умолчанию вызов проходит через Method → DelegatingMethodAccessorImpl → GeneratedMethodAccessorXYZ , а с этой опцией цепочка сокращается до Method → GeneratedMethodAccessorXYZ . Вызов Method → DelegatingMethodAccessorImpl мономорфный и легко девиртуализируется, но косвенная адресация всё равно остаётся.
Кстати, о девиртуализации. Стоит заметить, что бенчмарк у нас плохой, потому что он не отображает ситуацию в реальной программе. В бенчмарке мы вызываем через reflection только один метод, а значит, у нас всего один сгенерированный аксессор, который тоже легко девиртуализуется и даже инлайнится. В реальном приложении так не бывает: аксессоров много. Чтобы сэмулировать эту ситуацию, давайте опционально отравим профиль типов в методе setup :
Обратите внимание, что мы никак не меняли код, производительность которого мы измеряем. Мы просто сделали несколько тысяч с виду бесполезных вызовов перед этим. Однако эти бесполезные вызовы немного подпортят картинку: JIT видит, что вариантов много, и не может подставить единственный возможный, делая теперь честный виртуальный вызов. Результаты будут такие (poly — вариант с превращением вызова метода в полиморфный, на JNI не влияет):
(acc) | (nonpub) | Default | Default/poly | noInflation | noInflation/poly | JNI-only |
---|---|---|---|---|---|---|
true | true | 5,062 ± 0,056 | 6,848 ± 0,031 | 4,935 ± 0,375 | 6,509 ± 0,032 | 195,960 ± 1,873 |
true | false | 5,042 ± 0,032 | 6,847 ± 0,035 | 4,914 ± 0,329 | 6,490 ± 0,037 | 194,722 ± 1,151 |
false | true | 6,078 ± 0,039 | 7,855 ± 0,040 | 5,638 ± 0,050 | 7,661 ± 0,049 | 196,196 ± 0,910 |
false | false | 5,835 ± 0,028 | 7,568 ± 0,046 | 5,520 ± 0,042 | 7,111 ± 0,058 | 194,626 ± 0,918 |
Как видно, виртуальный вызов добавляет около 1,5-1,8 нс на моём железе — даже больше, чем проверки доступа. Важно помнить, что поведение виртуальной машины в микробенчмарке может существенно отличаться от поведения в реальном приложении, и по возможности воссоздавать условия, близкие к реальности. Здесь, конечно, от реальности всё ещё далеко: как минимум, все нужные объекты в L1-кэше процессора и сборка мусора не происходит, потому что мусора нет.
Некоторые могут подумать, круто, мол, что с -Dsun.reflect.noInflation=true всё становится быстрее. Пусть всего на 0,3 нс, но всё же. Да плюс первые 15 вызовов ускорятся. Да и рабочий набор чуть уменьшился, кэш процессора экономим — сплошные плюсы! Добавим опцию в продакшн и заживём! Так делать не надо. В бенчмарке мы протестировали один сценарий, а в природе бывают и другие. Например, какой-нибудь код может по одному разу вызывать множество разных методов. С этой опцией аксессор будет генерироваться сразу на первом вызове. А сколько это стоит? Сколько времени генерируется аксессор?
Чтобы это оценить, можно через reflection очищать приватное поле Method.methodAccessor (очистив предварительно Method.root ), принудив инициализировать аксессор заново. Запись поля через reflection хорошо оптимизирована, поэтому от этого тест сильно не замедлится. Получаем такие результаты. Верхняя строка — ранее полученные результаты (polymorph, accessible), для сравнения:
(test) | Default | noInflation | JNI |
---|---|---|---|
invoke | 6,848 ± 0,031 | 6,509 ± 0,032 | 195,960 ± 1,873 |
reset+invoke | 227,133 ± 9,159 | 100195,746 ± 2060,810 | 236,900 ± 2,042 |
Как видим, если аксессор сбрасывать, то по дефолту производительность становится немного хуже, чем в варианте с JNI. А вот если мы от JNI полностью отказываемся, то получаем 100 микросекунд на запуск метода. Генерация и загрузка класса в рантайме по сравнению с однократным вызовом метода (даже через JNI), конечно, чудовищно медленна. Поэтому дефолтное поведение «пробовать 15 раз через JNI и только потом генерировать класс» кажется в высшей степени разумным.
Вообще помните, что нет волшебной опции, которая ускорит любое приложение. Если бы она была, она бы была включена по умолчанию. Какой смысл её прятать от людей? Возможно, есть опция, которая ускорит конкретно ваше приложение, но не принимайте на веру любые советы типа «врубите -XX:+MakeJavaFaster, и всё будет летать».
Как же выглядят эти сгенерированные аксессоры? Байткод генерируется в классе MethodAccessorGenerator с использованием довольно тривиального низкоуровневого API ClassFileAssembler, которое чем-то похоже на урезанную библиотеку ASM. Классам даются имена вида sun.reflect.GeneratedMethodAccessorXYZ , где XYZ — глобальный синхронизированный счётчик, вы их можете увидеть в стектрейсах и отладчике.
Сгенерированный класс существует только в памяти, но мы легко можем сдампить его на диск, добавив в метод ClassDefiner.defineClass строчку вида
После этого можно смотреть на класс в декомпиляторе. Для нашего метода getName1() сгенерировался такой код (декомпилятор FernFlower и ручное переименование переменных):
Заметьте, сколько приходится делать дополнительных вещей. Надо проверить, что нам передали непустой объект нужного типа и передали пустой список аргументов или null вместо списка аргументов (не все знают, но при вызове через reflection метода без аргументов мы можем передать null вместо пустого массива). При этом надо аккуратно соблюдать контракт: если вместо объекта передали null , то выкинуть NullPointerException . Если передали объект другого класса, то IllegalArgumentException . Если произошло исключение при выполнении person.getName1() , то тогда InvocationTargetException . И это ещё у метода нет аргументов. А если они есть? Вызовем, например, такой метод (для разнообразия теперь статический и возвращающий void ):
Теперь кода существенно больше:
Заметьте, что вместо int мы имеем право передать Byte , Short , Character или Integer , и всё это обязано преобразоваться. Именно здесь преобразование и идёт. Такой блок будет добавляться для каждого примитивного аргумента, где возможно расширяющее преобразование. Теперь также понятно, зачем в catch ловится NullPointerException : он может возникнуть при анбоксинге и тогда мы обязаны также выдать IllegalArgumentException . Зато благодаря тому, что метод статический, нас совершенно не волнует, что в параметре target . Ну и появилась строчка return null , потому что наш метод возвращает void . Вся эта магия аккуратно расписана в MethodAccessorGenerator.emitInvoke.
Вот так примерно и работает вызов методов. Похожим образом устроен вызов конструкторов. Также этот код частично переиспользован для десериализации объектов. Когда аксессор уже существует, с точки зрения JVM он не особо отличается от кода, который вы бы написали сами вручную, поэтому рефлекшн начинает работать весьма быстро.
Источник
Русские Блоги
Отражение Java и метод Method Invoke (воспроизведено)
В традиционном ООП-мышлении любой файл класса, который вы написали и скомпилировали, будет связан с экземпляром java.lang.Class после загрузки загрузчиком классов. Следовательно, собственный атрибут метода каждого класса (структура класса) естественно включен в соответствующий экземпляр, поэтому его можно получить.
1. Введение
Выше приведен самый распространенный пример отражения: строки 3 и 4 реализуют загрузку, связывание и инициализацию класса (метод newInstance фактически использует отражение для вызова методов), а строки 5 и 6 реализуют объект метода, полученный из объекта класса. Затем выполните вызов отражения. Ниже кратко анализируются принципы двух последних строк.
Представьте, что если вы хотите реализовать method.invoke (action, null) для вызова метода myMethod объекта action, вам нужно только реализовать такой класс Method:
Один из принципов рефлексии на самом деле заключается в динамическом генерировании байт-кода, аналогичного приведенному выше, загрузке его в jvm и запуске.
Во-вторых, получить объект Method
Вызовите getDeclaredMethod класса Class, чтобы получить объект метода Method с указанным именем метода и параметрами.
getDeclaredMethod () метод
Где метод privateGetDeclaredMethods получает список методов, объявленных в классе, из кэша или JVM, а метод searchMethods находит объект метода, соответствующий имени и параметрам из возвращенного списка методов.
Если соответствующий метод найден, скопируйте копию и верните, copyMethod (res) выглядит следующим образом:
А именно метод Method.copy ()
Каждый раз, когда объект Method, возвращаемый вызовом getDeclaredMethod, фактически является новым объектом, а корневой атрибут нового объекта указывает на исходный объект Method. Если вам нужно вызывать его часто, лучше всего кэшировать объект Method.
Далее рассмотрим метод privateGetDeclaredMethods (), который используется для получения списка методов, объявленных в этом классе, из кэша или JVM. Код выглядит следующим образом:
Метод refleData () реализован следующим образом:
Существует важная структура данных ReflectionData, которая используется для кэширования следующих атрибутных данных класса, считываемого из JVM:
Это можно увидеть из реализации метода refleData (): объект mirrorData имеет тип SoftReference, что указывает на то, что он может быть переработан при нехватке памяти, но синхронизацию можно также контролировать с помощью параметра -XX: SoftRefLRUPolicyMSPerMB, который будет перезагружаться всякий раз, когда происходит GC Если метод отражения выполняется после того, как reflectionData повторно используется, только такой объект может быть воссоздан с помощью метода newReflectionData. Метод newReflectionData реализуется следующим образом:
Метод вызывает casReflectionData () и сбрасывает поле ReflectionData через метод unsafe.compareAndSwapObject;
В методе privateGetDeclaredMethods, если объект ReflectionData, полученный с помощью mirrorData (), не пуст, попробуйте получить свойство DeclaMethods из объекта ReflectionData, если это первый раз или после того, как он был восстановлен GC После того, как повторная инициализация атрибута класса пуста, вам нужно снова получить его в JVM и назначить его для ReflectionData. Данные кэша могут быть использованы при следующем вызове.
Три, метод invoke ()
Исходный код метода invoke выглядит следующим образом
В соответствии с реализацией метода invoke он делится на следующие шаги:
1. Проверка разрешения
Сначала проверьте значение свойства переопределения AccessibleObject в строке 6. Класс AccessibleObject является базовым классом для объектов Field, Method и Constructor. Он позволяет помечать отраженные объекты для отмены проверки контроля доступа к языку Java по умолчанию при использовании.
Значение переопределения по умолчанию равно false, что означает, что правила разрешений требуются, и разрешения необходимо проверять при вызове методов, мы также можем использовать метод setAccessible, чтобы установить значение true. Если значение переопределения равно true, это означает, что правила разрешений игнорируются, и разрешения не проверяются при вызове методов (также То есть может быть вызван любой частный метод, который нарушает инкапсуляцию).
Если атрибут override является значением по умолчанию false, то дальнейшие проверки прав доступа:
(1) В строке 7 используется метод Reflection.quickCheckMemberAccess (clazz, modifiers), чтобы проверить, является ли метод общедоступным, и, если это так, пропустите этот шаг; если это не открытый метод, то используйте метод Reflection.getCallerClass () для получения объекта Class, вызывающего этот метод, Это нативный метод:
После получения этого вызывающего объекта Class используйте метод checkAccess, чтобы выполнить быструю проверку разрешений, которая реализована следующим образом:
Сначала выполните быструю проверку.После того, как класс вызывающего метода правильный, проверка разрешения пройдена.
Если это не удается, создайте кеш, а затем проведите несколько проверок в середине (например, проверьте, является ли это защищенным атрибутом).
Если все вышеуказанные проверки разрешений не пройдены, будет выполнена более детальная проверка, которая реализуется следующим образом:
Обычно означает, что для продолжения проверки разрешений используйте метод Reflection.ensureMemberAccess. Если проверка пройдена, кэш обновляется, поэтому в следующий раз, когда тот же класс вызывает тот же метод, выполнять проверки разрешений не нужно. Это простой механизм кэширования. Поскольку правило JMM «до и после» может гарантировать, что инициализация кеша может произойти до того, как кеш будет записан, два кеша не нужно объявлять энергозависимыми.
На этом проверка перед авторизацией окончена. Если проверка не пройдена, будет сгенерировано исключение; если она прошла проверку, она перейдет к следующему шагу.
2. Вызовите метод вызова метода MethodAccessor.
Можно видеть, что на самом деле Method.invoke () — это не логика вызова отражения, реализованная сама по себе, а делегированная для обработки sun.reflect.MethodAccessor.
Прежде всего, мы должны понимать базовую структуру объекта Method: каждый метод Java имеет один и только один объект Method в качестве корневого, который эквивалентен корневому объекту и невидим для пользователя. Этот корень не будет доступен пользователю. Когда мы получим объект Method посредством отражения, вновь созданный объект Method оборачивает корень и передает его пользователю. Полученный в нашем коде объект Method эквивалентен его копии (или ссылке). Корневой объект содержит объект MethodAccessor, поэтому все полученные объекты Method разделяют этот объект MethodAccessor, поэтому он должен обеспечивать его видимость в памяти. Объявление и комментарии корневого объекта:
Так что же такое MethodAccessor?
Вы можете видеть, что MethodAccessor — это интерфейс, который определяет метод invoke. Анализ его использования может получить его конкретные классы реализации:
- sun.reflect.DelegatingMethodAccessorImpl
- sun.reflect.MethodAccessorImpl
- sun.reflect.NativeMethodAccessorImpl
Как видно из источника метода invoke (), перед первым вызовом метода invoke () объекта Method, соответствующего фактическому методу Java, объект MethodAccessor, реализующий логику вызова, не был создан; дождитесь первого вызова с помощью метода acquMethodAccessor () Просто создайте MethodAccessor и обновите его до root, затем вызовите MethodAccessor.invoke () для завершения вызова отражения:
Вы можете видеть, что экземпляр methodAccessor генерируется путем манипулирования объектом refleFactory. Изучите исходный код класса sun.reflect.ReflectionFactory:
Фактически, объект DelegatingMethodAccessorImpl является прокси-объектом, который отвечает за вызов метода invoke делегата объекта делегата. Параметр делегата в настоящее время является объектом NativeMethodAccessorImpl, поэтому конечным методом вызова метода является метод вызова объекта NativeMethodAccessorImpl, реализованный следующим образом:
В строке 13 вы можете видеть, что каждый раз, когда вызывается метод NativeMethodAccessorImpl.invoke (), счетчик вызовов программы увеличивается на 1, чтобы увидеть, превышает ли он пороговое значение, а после превышения вызывается MethodAccessorGenerator.generateMethod () для генерации Java-реализации реализации MethodAccessor. Класс и измените MethodAccessor, на который ссылается DelegatingMethodAccessorImpl, на версию Java. Последующие вызовы через DelegatingMethodAccessorImpl.invoke () являются реализацией Java.
Можно видеть, что DelegatingMethodAccessorImpl является косвенным уровнем, который удобен для переключения между нативной и Java-версией MethodAccessor.
Две версии MethodAccessor спроектированы таким образом, одна реализована на Java, а другая реализована на собственном коде, потому что версия реализации Java требует больше времени для инициализации, но в долгосрочной перспективе производительность выше, а собственная версия — наоборот, Запуск относительно быстрый, но после долгого запуска скорость не так хороша, как у версии Java. Это характеристика производительности, обеспечиваемая методом оптимизации HotSpot, а также общая черта многих виртуальных машин: пересечение собственной границы будет препятствовать оптимизации. Это похоже на черный ящик, который мешает виртуальной машине анализировать и встроить ее, поэтому она работает Через долгое время управляемая версия кода становится быстрее.
Чтобы сбалансировать производительность двух версий, JDK от Sun использует технику «надувания»: пусть метод Java использует собственную версию первые 15 раз, когда она вызывается отражением, и генерирует специальный класс реализации MethodAccessor, когда число вызовов отражения превышает пороговое значение. , Сгенерируйте байтовый код метода invoke (), а затем используйте версию Java для вызова отражения метода Java.
3. Java-версия
Далее перейдите к приведенному выше исходному коду. Метод generateMethod генерирует соответствующий байт-код в памяти при создании объекта MethodAccessorImpl и вызывает ClassDefiner.defineClass для создания соответствующего объекта класса. Некоторые коды выглядят следующим образом:
В реализации метода ClassDefiner.defineClass объект загрузчика класса DelegatingClassLoader создается каждый раз, когда он вызывается
Новый загрузчик классов генерируется здесь каждый раз по соображениям производительности.В некоторых случаях эти сгенерированные классы могут быть выгружены, потому что выгрузка классов утилизируется только тогда, когда загрузчик классов может быть переработан. Если используется оригинальный загрузчик классов, это может привести к тому, что эти вновь созданные классы будут выгружены все время. С точки зрения дизайна они не хотят, чтобы эти классы постоянно хранились в памяти, и они будут там, когда это необходимо.
Для примера в начале этой статьи сгенерированная Java-версия MethodAccessor примерно выглядит следующим образом:
С точки зрения вызовов отражения метод invoke () очень чистый (однако с точки зрения «обычных вызовов» эти дополнительные издержки все еще очевидны). Обратите внимание, что массив параметров разобран, восстановите каждый параметр до того, каким он был до того, как он не был обернут Object [], а затем выполните обычный вызов invokevirtual для целевого метода. Поскольку массив типов параметров был пройден при генерации кода, сгенерированный код больше не содержит цикл.
4. Сравнение производительности
Исходя из тенденции изменения, первый и 16-й вызовы занимают больше всего времени (инициализация NativeMethodAccessorImpl и сборки байтового кода MethodAccessorImpl). В конце концов, инициализация неизбежна, и собственный метод инициализации будет быстрее, поэтому первые несколько вызовов будут использовать собственный метод.
По мере увеличения количества вызовов каждое отражение использует JNI для пересечения собственной границы, что будет препятствовать оптимизации. Условно говоря, собранный байт-код можно напрямую вызывать в форме Java Реализация отражения играет роль оптимизации JIT и позволяет избежать потери производительности инкапсуляции / неинкапсуляции JNI, чтобы поддерживать OopMap (структуру данных, используемую HotSpot для достижения точного GC). Поэтому, когда MethodAccessor будет создан, реализация, использующая версию Java, будет быстрее, чем собственная версия. Поэтому, когда количество вызовов достигает определенного числа (15 раз), оно переключается на версию реализации Java для оптимизации возможных более частых вызовов отражения в будущем.
Источник