JEP draft: Isolated Methods
Изолированные методы
| Автор | Michael Haupt |
| Ответственный | Maurizio Cimadamore |
| Тип | Feature |
| Область | JDK |
| Статус | Draft |
| Компонент | core-libs / java.lang.invoke |
| Обсуждение | mlvm dash dev at openjdk dot java dot net |
| Трудоёмкость | L |
| Длительность | XL |
| Рецензенты | Alex Buckley, Brian Goetz, Jim Laskey, Paul Sandoz, Vladimir Ivanov |
| Создан | 2016/06/06 14:00 |
| Обновлён | 2018/04/16 21:06 |
| Задача | 8158765 |
Аннотация
Расширить класс MethodHandles.Lookup пакета java.lang.invoke, чтобы он поддерживал загрузку байт-кода методов без привязанного класса и представлял такие методы в виде method handle.
Цели
-
В классе
MethodHandles.Lookupпакетаjava.lang.invokeпредоставить новый методloadCode, который загружает массив байт-кода вместе с константами как изолированный метод и возвращаетMethodHandle, представляющий этот метод. -
На уровне JVM предоставить оптимизированный способ хранения изолированных методов.
Что не является целью
-
Новая стратегия компиляции лямбда-выражений не входит в рамки этого JEP.
-
Расширения на уровне языка Java явно не входят в рамки.
-
Расширения набора инструкций виртуальной машины Java (байт-кодов) также не входят в рамки.
Критерии успеха
-
Повышение производительности инфраструктуры method handle там, где она использует генерацию байт-кода (в том числе при запуске).
-
Уменьшение объёма памяти, занимаемой инфраструктурой method handle (в частности, объектами
LambdaForm,BoundMethodHandleи объектами invoker). -
Аналогичный наблюдаемый эффект в реализациях динамических языков после того, как они перейдут на новый API.
Мотивация
И в базовых библиотеках JDK, и в реализациях языков, работающих поверх JVM, распространён приём генерации классов без состояния с единственным статическим методом. Такие классы служат для представления того, что логически является методом без класса, или «изолированным методом». Генерировать их громоздко, поскольку для этого требуется сгенерировать полный класс, и это создаёт определённую нагрузку на виртуальную машину в части загрузки и сопровождения классов.
Чтобы для этого сценария было возможно более легковесное решение, должен существовать способ напрямую описать и загрузить изолированный метод, получить его в форме, пригодной для вызова, и гарантировать, что с его помощью не совершаются нарушения доступа.
Method handle — пригодная абстракция для представления кода, который можно вызвать. Кроме того, средство управления контекстами поиска и доступа уже существует в виде класса MethodHandles.Lookup. Не хватает способа загрузить метод изолированно. Если добавить в класс MethodHandles.Lookup единственную точку входа API, принимающую представление изолированного метода, такой метод можно будет загрузить с контекстом поиска, который задаёт имеющийся экземпляр Lookup.
В базовых библиотеках JDK есть несколько мест, прежде всего низкоуровневая инфраструктура method handle, где новую абстракцию изолированных методов можно использовать, чтобы уменьшить размер кода и объём занимаемой памяти и ускорить загрузку. Кроме того, движок JavaScript Nashorn может использовать эту возможность аналогичным образом, поскольку он генерирует байт-код из исходного кода на JavaScript. Наконец, клиентами возможности загрузки изолированных методов могут стать все реализации языков, которые работают поверх JVM и генерируют байт-код.
Следует отметить, что во всех перечисленных сценариях нужен доступ к внутреннему API Unsafe. Упорядоченный и безопасный способ определять собственный код в форме изолированных методов позволит сделать многие из этих применений Unsafe ненужными. Тем самым можно будет уменьшить зависимость от внутреннего API, которая часто есть у реализаций гостевых языков на JVM.
Описание
Класс MethodHandles.Lookup пакета java.lang.invoke предлагается расширить методом такого вида:
MethodHandle loadCode(String name, MethodType type, byte[] instructions, Object[] constants)
Параметр name необязателен. Он задаёт имя, по которому изолированный метод можно будет опознать в трассировках стека.
Параметр type определяет тип возвращаемого значения и типы параметров метода. Массив instructions содержит инструкции байт-кода метода в том виде, в каком они были бы записаны в обычном class-файле. Важное отличие в том, что все индексы в пуле констант класса, которые обычно содержал бы байт-код, теперь являются индексами в сопутствующем массиве constants. Он служит заменой пула констант на уровне метода.
Метод loadCode создаёт метод из переданных инструкций байт-кода и констант и возвращает MethodHandle, с помощью которого этот метод можно вызвать. Реализация loadCode возьмёт на себя верификацию загружаемого кода.
Этот метод изолирован от любого класса и ведёт себя в основном как статический метод. Method handle, полученный в результате вызова loadCode, относится к виду REF_static. Его нельзя разобрать с помощью MethodHandles.Lookup.revealDirect().
Контекст метода, определённого таким образом, задаётся экземпляром Lookup, который получает вызов loadCode. Если привилегий поиска недостаточно, будет выброшено исключение.
Массив constants
Массив constants, предназначенный для констант, на которые ссылается байт-код, заслуживает отдельного внимания. Прежде всего, его не следует принимать за пул констант. Скорее, он предоставляет более высокий уровень абстракции над содержимым пула констант и делает работу удобнее для клиентов.
Похожую роль играет массив исправлений пула констант, который можно передавать при вызовах Unsafe.defineAnonymousClass. Например, массив исправлений пула констант позволяет передать String там, где нужно исправить запись CONSTANT_Utf8_info; на самом деле эта запись состоит из байта тега, двухбайтовой длины и массива символов. Unsafe.defineAnonymousClass поддерживает подобное удобство и для других записей пула констант.
Для массива constants, передаваемого в loadCode, должно быть возможно аналогичное удобство. Например, там, где инструкции метода ссылаются на класс Java, массив constants может содержать экземпляр Class, а не низкоуровневые структуры, которые встречаются в пулах констант. Точно так же инструкция INVOKEVIRTUAL может ссылаться на элемент массива constants, который сам является MethodHandle, представляющим нужный метод.
В следующей таблице перечислены различные виды возможных записей пула констант и классы Java, которыми их можно представить в массиве constants.
-
CONSTANT_Utf8_info:java.lang.String -
CONSTANT_Integer_info:int,java.lang.Integer -
CONSTANT_Float_info:float,java.lang.Float -
CONSTANT_Long_info:long,java.lang.Long -
CONSTANT_Double_info:double,java.lang.Double -
CONSTANT_Class_info:java.lang.Class -
CONSTANT_String_info:java.lang.String -
CONSTANT_Fieldref_info:java.lang.invoke.DirectMethodHandleнужного вида, полученный через соответствующий API вjava.lang.invoke.MethodHandles.Lookup -
CONSTANT_Methodref_info:java.lang.invoke.DirectMethodHandleнужного вида, полученный через соответствующий API вjava.lang.invoke.MethodHandles.Lookup -
CONSTANT_InterfaceMethodref_info:java.lang.invoke.DirectMethodHandleнужного вида, полученный через соответствующий API вjava.lang.invoke.MethodHandles.Lookup -
CONSTANT_NameAndType_info: (не должно требоваться) -
CONSTANT_MethodHandle_info:java.lang.invoke.MethodHandle -
CONSTANT_MethodType_info:java.lang.invoke.MethodType -
CONSTANT_InvokeDynamic_info: либо кортеж(java.lang.invoke.MethodType,java.lang.invoke.MethodHandle), гдеMethodTypeописывает сигнатуру точки вызова, аMethodHandleпредставляет bootstrap-метод с уже привязанными статическими аргументами; либо уже инициализированныйjava.lang.invoke.CallSite
Кроме того, в проекте Valhalla предлагается несколько новых типов записей пула констант, для которых замены в массивах constants могут быть следующими. Обратите внимание, что таблица предполагает наличие в языке кортежей, которые могут появиться с Valhalla.
-
CONSTANT_ArrayType_info: кортеж(byte,java.lang.Class) -
CONSTANT_MethodDescriptor_info: массивjava.lang.Class(по мере продвижения Valhalla экземплярамClass, возможно, придётся предоставлять дополнительную информацию) -
CONSTANT_ParameterizedType_info: кортеж(java.lang.Class,java.lang.Class[]) -
CONSTANT_TypeVar_info: кортеж(java.lang.String,java.lang.Class)
Кроме того, новые типы записей пула констант, рассматриваемые в предложении об общих данных в пулах констант, можно представить следующим образом.
-
CONSTANT_Dynamic: кортеж(java.lang.Class,java.lang.invoke.MethodHandle), гдеClassпредставляет ожидаемый тип, аMethodHandleописывает bootstrap-метод с уже привязанными статическими параметрами -
CONSTANT_Group: массив илиjava.util.List -
CONSTANT_Bytes:byte[]
В качестве замечания об обобщённых методах следует указать, что у изолированного метода нет объемлющего класса, который мог бы определять переменные типа. Вместо этого все переменные типа, упомянутые в сигнатуре обобщённого изолированного метода, принадлежат только этому методу.
Массив constants может также содержать любые объекты, которые можно загрузить инструкцией LDC. Это можно использовать для привязки определённых конкретных данных, известных на этапе компиляции.
Преобразовывать эти вспомогательные объекты в правильные низкоуровневые представления, похожие на записи пула констант, будет реализация loadCode. Детали зависят от решений, которые будут приняты для внутреннего представления изолированных методов.
Реализация
Функциональность loadCode можно реализовать в несколько этапов. Глубина их интеграции с существующей системой возрастает.
Этап 1: внутреннее использование для генерации LambdaForm и объектов invoker
Первоначальную версию loadCode следует предоставить как часть непубличного API для Invokedynamic, например в виде непубличного метода в классе MethodHandles.Lookup или в классе MethodHandleImpl. Там её можно использовать для генерации LambdaForm и других объектов invoker в реализации java.lang.invoke. Реализация не должна обрабатывать изолированные методы как таковые, а должна, как обычно, оборачивать методы LambdaForm в класс.
Этап 2: оптимизированное внутреннее использование для генерации LambdaForm и объектов invoker
Внутреннюю реализацию loadCode этапа 1 можно, сохраняя API стабильным, оптимизировать на уровне HotSpot. В настоящее время можно исследовать две идеи дизайна.
-
Внутренне представлять каждый изолированный метод как метод плюс пул констант. Класс, которому принадлежит изолированный метод, чтобы соответствовать общим ожиданиям виртуальной машины, — это псевдокласс, экземпляры которого нельзя создать. Это похоже на то, как сейчас строятся классы с единственным статическим методом.
-
Добавить в HotSpot понятие псевдокласса (под названием
Gargantuan), который будет содержать все методы, определённые через интерфейсloadCode. Это будет полностью статический класс, невидимый извне (не считая поддержкиgetCallerClass).Gargantuan— класс, который должен расти по мере определения новых методов. Методы могут освобождаться, когда на них больше не ссылается ни одинMethodHandle. Каждый метод вGargantuanможет иметь контекст, отличный от всех остальных методов, в зависимости от контекста поиска, имеющегося при вызовеloadCode. Этот контекст поиска сохраняется вGargantuanи связан с изолированным методом в течение всего его времени жизни.Массивы
constantsнескольких изолированных методов весьма вероятно будут содержать общие константы. РеализацияloadCodeна уровне виртуальной машины будет добавлять в пул константGargantuanтолько те константы, которых в нём ещё нет, и соответствующим образом исправлять массив инструкций байт-кода. Такое исключение дублирующихся записей пула констант может также выполняться при сборке мусора, чтобы ускорить загрузку изолированных методов. В любом случае все изолированные методы используют общий пул констант.Класс
Gargantuanможет также существовать в одном экземпляре на модуль, что позволит эффективно освобождать константы, хранимые для изолированного метода, и, возможно, другие структуры при выгрузке модуля.
Этап 3: публичный API
В конечном итоге метод loadCode должен стать публичным в MethodHandles.Lookup, чтобы его можно было использовать шире. Тем временем доступность через репозиторий MLVM позволит применять возможность loadCode в существующих реализациях языков для экспериментов.
Примеры использования
Приведённые ниже примеры показывают, какой может быть в будущем инфраструктура, необходимая для генерации массива instructions. Во всех примерах описаны генерация и загрузка метода с сигнатурой (Ljava/lang/String;)I, который возвращает длину своего аргумента. Он состоит из следующих инструкций:
ALOAD_0
INVOKEVIRTUAL #0 <String.length()>
IRETURN
В первом примере используется более высокий уровень абстракции над пулом констант, предложенный выше:
MethodHandlee stringLength = lookup.loadCode("isoToString",
methodType(int.class, String.class),
new byte[]{42, 182, 0, 0, 172},
new Object[]{
lookup.findVirtual(String.class, "length", methodType(int.class))});
В приведённом выше примере массив instructions задан непосредственными константами. Чтобы удобно генерировать такие массивы и массивы constants, на которые они ссылаются, можно представить удобный генератор изолированных методов (его API вдохновлён GeneratorAdapter из ASM):
MethodHandle stringLength =
new IsolatedMethodBuilder("isoToString", methodType(int.class, String.class)).
loadArg(0).
invokeVirtual(lookup.findVirtual(String.class, "length", methodType(int.class))).
returnValue().
load();
Приведённые выше примеры служат отправной точкой для обсуждения того, какими могут быть API загрузки изолированных методов и возможный вспомогательный API.
Альтернативы
Анонимные классы (получаемые через Unsafe.defineAnonymousClass()) — уже существующий способ динамически определять классы. Они специально поддерживают сценарии, в которых анонимному методу нужен доступ к связанному с ним состоянию, например в случае лямбда-выражений, замыкающих локальное состояние. Изолированные методы могут заменить те применения анонимных классов, которые попадают в категорию «один статический метод, без состояния». С анонимными классами изолированные методы объединяет то, что их нельзя найти по имени.
Альтернативный подход к ускорению генерации байт-кода в основных библиотеках JDK — использовать шаблоны байт-кода с заранее определёнными пулами констант, в которые вставляется только байт-код методов. У этого подхода два основных недостатка: его было бы легко применить только в основных библиотеках, но он не масштабировался бы на реализации гостевых языков; и он всё равно требовал бы генерировать соответствующий байт-код, а отделить эту генерацию от понятия класса в ASM трудно.
Тестирование
Особых требований к платформе или оборудованию для тестирования нет. Поскольку основные библиотеки JDK сами используют дескрипторы методов (method handles), а система модулей особенно сильно опирается на лямбды и связанную с ними генерацию байт-кода, сам JDK — отличный испытательный стенд. Существующие тесты функциональности дескрипторов методов также будут полезны.
Что касается тестирования реализациями гостевых языков, все такие реализации, которые уже используют API дескрипторов методов, неявно будут доступны для тестирования с помощью их собственных наборов тестов. Экспериментальные расширения таких реализаций гостевых языков могут перейти на схему реализации на основе изолированных методов для постоянного тестирования. Например, движок JavaScript Nashorn способен выполнять большой объём стандартного кода на JavaScript, включая бенчмарки.
Риски и допущения
Введение нового API для загрузки кода в VM рискованно само по себе. Если эта возможность будет сочтена слишком рискованной, её можно перенести в API Unsafe.
Зависимости
Этот JEP зависит от наличия фреймворка генерации байт-кода, который обеспечивает простой доступ к пулу констант и позволяет отделить генерацию методов от генерации классов.