Почему рефлексивный Method.invoke медленнее прямого вызова и как снизить эту цену?
Каждый invoke платит то, чего не платит прямой вызов: проверку доступа, упаковку каждого примитивного аргумента в Object[] и упаковку результата, а также точку вызова, непрозрачную для JIT, — она не инлайнится, и зависящие от инлайнинга оптимизации теряются. Поиск члена класса ещё и копирует объекты Method. Снижают цену кешированием Method, вызовом setAccessible(true) или переходом на MethodHandle/LambdaMetafactory, которые JIT умеет инлайнить.
- ✗Списывать всю цену на
setAccessible, игнорируя упаковку и потерю инлайнинга - ✗Вызывать
getDeclaredMethodвнутри горячего цикла вместо кешированияMethod - ✗Считать, что JIT со временем превратит рефлексивный вызов в прямой
- →Почему
MethodHandleвstatic final-поле инлайнится, аMethod.invoke— нет? - →Как сериализатор амортизирует цену рефлексии, отображая тысячи объектов?
Из чего складывается цена
// ❌ поиск члена класса — внутри горячего цикла
for (Object row : rows) {
Method m = row.getClass().getDeclaredMethod("getId"); // копирует Method на каждый вызов
ids.add((Long) m.invoke(row)); // упаковка результата в Long
}
// ✅ поиск один раз, дальше только вызов
private static final Method GET_ID = init(); // кеш
static Method init() throws Exception {
Method m = Row.class.getDeclaredMethod("getId");
m.setAccessible(true); // проверка доступа снимается один раз
return m;
}
1. Поиск. getDeclaredMethod / getDeclaredFields возвращают копии объектов Method и Field (чтобы вызывающий не испортил внутреннее состояние Class). В цикле это аллокация на каждой итерации. Лечится кешированием.
2. Упаковка. Сигнатура invoke(Object obj, Object... args) вынуждает сложить аргументы в Object[], а каждый примитив — в обёртку. Результат тоже возвращается как Object. Прямой вызов не делает ни одной из этих аллокаций.
3. Инлайнинг. Это самое дорогое и самое незаметное. JIT не видит, какой метод будет вызван, поэтому точка вызова не инлайнится, а вместе с инлайнингом отваливаются девиртуализация и escape-анализ соседнего кода. Разница с прямым вызовом на горячем пути — порядок величины, а не проценты.
Что использовать вместо. MethodHandle, разрешённый один раз и положенный в static final-поле, JIT инлайнит как обычный вызов. LambdaMetafactory идёт дальше: он порождает настоящую реализацию функционального интерфейса, и дальнейшие вызовы уже ничем не отличаются от вызова лямбды. Именно так работают быстрые сериализаторы и мапперы: рефлексия платится один раз на старте, а не на каждой записи.