JEP 371: Hidden Classes
Hidden-классы (скрытые классы)
| Ответственный | Mandy Chung |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 15 |
| Компонент | core-libs / java.lang.invoke |
| Обсуждение | valhalla dash dev at openjdk dot java dot net |
| Трудоёмкость | L |
| Длительность | L |
| Рецензенты | Alex Buckley, David Holmes, John Rose, Maurizio Cimadamore, Paul Sandoz |
| Одобрен | John Rose |
| Создан | 2019/03/13 17:37 |
| Обновлён | 2020/10/07 16:39 |
| Задача | 8220607 |
Аннотация
Ввести hidden-классы (скрытые классы), то есть классы, которые не могут напрямую использоваться байт-кодом других классов. Hidden-классы предназначены для фреймворков, которые генерируют классы во время выполнения и используют их косвенно, через рефлексию. Hidden-класс может быть определён как член гнезда контроля доступа (access control nest) и может выгружаться независимо от других классов.
Цели
-
Позволить фреймворкам определять классы как необнаружимые детали реализации фреймворка, чтобы другие классы не могли ни связываться с ними, ни обнаруживать их через рефлексию.
-
Поддержать расширение гнезда контроля доступа необнаружимыми классами.
-
Поддержать агрессивную выгрузку необнаружимых классов, чтобы фреймворки могли гибко определять столько таких классов, сколько им нужно.
-
Перевести нестандартный API
sun.misc.Unsafe::defineAnonymousClassв статус Deprecated (устаревший) с намерением в одном из будущих выпусков перевести его в статус Deprecated for Removal (устаревший, будет удалён). -
Никак не изменять язык программирования Java.
Что не является целью
- Не ставится цель поддержать всю функциональность
sun.misc.Unsafe::defineAnonymousClass, например патчинг пула констант.
Мотивация
Многие реализации языков на базе JVM ради гибкости и эффективности полагаются на динамическую генерацию классов. Например, в случае языка Java javac не транслирует лямбда-выражение в отдельный файл class во время компиляции, а генерирует байт-код, который динамически создаёт класс и его экземпляр, чтобы при необходимости получить объект, соответствующий лямбда-выражению. Аналогично среды выполнения других языков (не Java) часто реализуют возможности высшего порядка этих языков с помощью динамических прокси, которые тоже генерируют классы динамически.
Разработчики реализаций языков обычно задумывают динамически сгенерированный класс как логическую часть реализации статически сгенерированного класса. Из этого намерения вытекает ряд свойств, желательных для динамически сгенерированных классов:
-
Необнаружимость. Возможность независимо обнаружить класс по имени не только не нужна, но и вредна. Она подрывает цель, чтобы динамически сгенерированный класс был лишь деталью реализации статически сгенерированного класса.
-
Контроль доступа. Может быть желательно расширить контекст контроля доступа статически сгенерированного класса, включив в него динамически сгенерированный класс.
-
Жизненный цикл. Динамически сгенерированные классы могут быть нужны лишь ограниченное время, поэтому хранение их в течение всего времени жизни статически сгенерированного класса может без необходимости увеличить потребление памяти. Существующие обходные пути для этой ситуации, такие как отдельный загрузчик классов для каждого класса, громоздки и неэффективны.
К сожалению, стандартным API, которые определяют класс, — ClassLoader::defineClass и Lookup::defineClass — безразлично, был ли байт-код класса сгенерирован динамически (во время выполнения) или статически (во время компиляции). Эти API всегда определяют видимый класс, который будет использоваться каждый раз, когда другой класс в той же иерархии загрузчиков пытается связать класс с таким именем. Поэтому класс может оказаться более обнаружимым или жить дольше, чем нужно. Кроме того, эти API могут определить класс, который будет членом гнезда, только если хост-класс гнезда заранее знает имя класса-члена; на практике это не позволяет динамически сгенерированным классам быть членами гнезда.
Если бы стандартный API мог определять hidden-классы, которые необнаружимы и имеют ограниченный жизненный цикл, то фреймворки как внутри, так и вне JDK, генерирующие классы динамически, могли бы вместо этого определять hidden-классы. Это повысило бы эффективность всех реализаций языков на базе JVM. Например:
-
java.lang.reflect.Proxyмог бы определять hidden-классы в качестве прокси-классов, реализующих прокси-интерфейсы; -
java.lang.invoke.StringConcatFactoryмог бы генерировать hidden-классы для хранения методов конкатенации констант; -
java.lang.invoke.LambdaMetaFactoryмог бы генерировать hidden-классы — члены гнезда (nestmate) для хранения тел лямбд, обращающихся к переменным охватывающего контекста; и -
Движок JavaScript мог бы генерировать hidden-классы для байт-кода, полученного трансляцией программ на JavaScript, зная, что эти классы будут выгружены, когда движок перестанет их использовать.
Описание
API Lookup, появившийся в Java 7, позволяет классу получить lookup-объект, который предоставляет рефлексивный доступ к классам, методам и полям. Важно, что какой бы код в итоге ни использовал lookup-объект, рефлексивный доступ всегда происходит в контексте класса, который изначально получил lookup-объект, — lookup-класса. По сути, lookup-объект передаёт права доступа lookup-класса любому коду, который получает этот объект.
В Java 9 возможности передачи прав через lookup-объекты были расширены: появился метод Lookup::defineClass(byte[]). Из переданных байтов этот метод определяет новый класс в том же контексте, что и класс, который изначально получил lookup-объект. То есть у нового класса тот же определяющий загрузчик классов, тот же пакет времени выполнения и тот же домен защиты, что и у lookup-класса.
Этот JEP предлагает расширить API Lookup, чтобы поддержать определение hidden-класса, к которому можно обращаться только через рефлексию. Hidden-класс не может быть обнаружен ни JVM при связывании байт-кода, ни программами, явно использующими загрузчики классов (например, через Class::forName и ClassLoader::loadClass). Hidden-класс может быть выгружен, когда он становится недостижим, или может разделять время жизни с загрузчиком классов, так что он выгружается только тогда, когда загрузчик классов удаляется сборщиком мусора. При желании hidden-класс можно создать как член гнезда контроля доступа.
Для краткости в этом JEP говорится о «hidden-классе», но под этим следует понимать hidden-класс или hidden-интерфейс. Аналогично «обычный класс» означает обычный класс или интерфейс, результат ClassLoader::defineClass.
Создание hidden-класса
Если обычный класс создаётся вызовом ClassLoader::defineClass, то hidden-класс создаётся вызовом Lookup::defineHiddenClass. В результате JVM получает hidden-класс из переданных байтов, связывает его и возвращает lookup-объект, предоставляющий рефлексивный доступ к hidden-классу. Вызывающая программа должна бережно сохранить lookup-объект, поскольку это единственный способ получить объект Class hidden-класса.
Переданные байты должны представлять собой структуру ClassFile (JVMS 4.1). Получение hidden-класса методом Lookup::defineHiddenClass похоже на получение обычного класса методом ClassLoader::defineClass, с одним важным отличием, которое рассматривается ниже. После того как hidden-класс получен, он связывается так же, как обычный класс (JVMS 5.4), за исключением того, что ограничения загрузки не накладываются. После связывания hidden-класс инициализируется, если аргумент initialize метода Lookup::defineHiddenClass равен true; если аргумент равен false, hidden-класс будет инициализирован, когда рефлексивные методы создадут его экземпляр или обратятся к его членам.
Главное отличие в создании hidden-класса заключается в имени, которое он получает. Hidden-класс не анонимен. У него есть имя, доступное через Class::getName, которое может отображаться в диагностике (например, в выводе java -verbose:class), в событиях загрузки классов JVM TI, в событиях JFR и в трассировках стека. Однако имя имеет настолько необычную форму, что фактически делает класс невидимым для всех остальных классов. Имя представляет собой конкатенацию:
-
двоичного имени во внутренней форме (JVMS 4.2.1), указанного в
this_classв структуреClassFile, напримерA/B/C; -
символа
'.'; и -
неквалифицированного имени (JVMS 4.2.2), выбираемого реализацией JVM.
Например, если this_class указывает com/example/Foo (внутренняя форма двоичного имени com.example.Foo), то hidden-класс, полученный из структуры ClassFile, может называться com/example/Foo.1234. Эта строка не является ни двоичным именем, ни внутренней формой двоичного имени.
Для hidden-класса с именем A/B/C.x результат Class::getName — это конкатенация:
- двоичного имени
A.B.C(получается изA/B/Cзаменой каждого'/'на'.'); - символа '/'; и
- неквалифицированного имени
x.
Например, если hidden-класс называется com/example/Foo.1234, то результат Class::getName — com.example.Foo/1234. Опять же, эта строка не является ни двоичным именем, ни внутренней формой двоичного имени.
Пространство имён hidden-классов не пересекается с пространством имён обычных классов. Если в структуре ClassFile элемент this_class указывает com/example/Foo/1234, то вызов cl.defineClass("com.example.Foo.1234", bytes, ...) просто создаст обычный класс с именем com.example.Foo.1234, отличный от hidden-класса с именем com.example.Foo/1234. Создать обычный класс с именем com.example.Foo/1234 невозможно, поскольку cl.defineClass("com.example.Foo/1234", bytes, ...) отклонит строковый аргумент как не являющийся двоичным именем.
Мы признаём, что отказ от двоичных имён в качестве имён hidden-классов потенциально может быть источником проблем, но это согласуется с давней практикой
Unsafe::defineAnonymousClass(см. обсуждение здесь). Использование/для обозначения hidden-класса в выводеClass::getNameтакже стилистически согласуется с использованием/в трассировках стека для уточнения класса его определяющим модулем и загрузчиком (см.StackTraceElement::toString). В приведённом ниже журнале ошибок видны два hidden-класса, оба в модулеm1: у одного hidden-класса есть методtest, у другого — методapply.java.lang.Error: thrown from hidden class com.example.Foo/0x0000000800b7a470 at m1/com.example.Foo/0x0000000800b7a470.toString(Foo.java:16) at m1/com.example.Foo_0x0000000800b7a470$$Lambda$29/0x0000000800b7c040.apply(<Unknown>:1000001) at m1/com.example.Foo/0x0000000800b7a470.test(Foo.java:11)
Hidden-классы и загрузчики классов
Несмотря на то, что у hidden-класса есть соответствующий объект Class, а супертипы hidden-класса создаются загрузчиками классов, в создании самого hidden-класса не участвует ни один загрузчик классов. Обратите внимание, что в этом JEP нигде не говорится, что hidden-класс «загружается». Ни один загрузчик классов не регистрируется как инициирующий загрузчик hidden-класса, и не создаются ограничения загрузки, затрагивающие hidden-классы. Поэтому hidden-классы неизвестны ни одному загрузчику классов: символьная ссылка в пуле констант времени выполнения класса D на класс C, обозначенный N, никогда не разрешится в hidden-класс ни при каких значениях D, C и N. Рефлексивные методы Class::forName, ClassLoader::findLoadedClass и Lookup::findClass не найдут hidden-классы.
Несмотря на эту обособленность от загрузчиков классов, считается, что у hidden-класса есть определяющий загрузчик классов. Это необходимо для разрешения типов, которые используются в собственных полях и методах hidden-класса. В частности, у hidden-класса тот же определяющий загрузчик классов, тот же пакет времени выполнения и тот же домен защиты, что и у lookup-класса, то есть класса, который изначально получил lookup-объект, у которого вызывается Lookup::defineHiddenClass.
Использование hidden-класса
Lookup::defineHiddenClass возвращает объект Lookup, lookup-классом которого является только что созданный hidden-класс. Объект Class для hidden-класса можно получить, вызвав Lookup::lookupClass у возвращённого объекта Lookup. Через объект Class можно создавать экземпляры hidden-класса и обращаться к его членам так же, как если бы это был обычный класс, за исключением четырёх ограничений:
-
Class::getNameвозвращает строку, которая не является двоичным именем, как описано выше. -
Class::getCanonicalNameвозвращаетnull, что означает отсутствие у hidden-класса канонического имени. (Обратите внимание, что объектClassдля анонимного класса в языке Java ведёт себя так же.) -
Final-поля, объявленные в hidden-классе, нельзя изменить.
Field::setи другие методы-сеттеры для final-поля hidden-класса выбросятIllegalAccessExceptionнезависимо от флагаaccessibleэтого поля. -
Объект
Classнельзя изменить с помощью агентов инструментирования, а агенты JVM TI не могут переопределить (redefine) или повторно преобразовать (retransform) его. Однако мы расширим JVM TI и JDI для поддержки hidden-классов: например, чтобы можно было проверить, является ли класс hidden-классом, чтобы hidden-классы входили в любой список «загруженных» классов и чтобы при создании hidden-классов отправлялись события JVM TI.
Важно понимать, что другие классы могут использовать hidden-класс только косвенно, через его объект Class. Байт-код в других классах не может использовать hidden-класс напрямую, потому что на него нельзя сослаться номинально, то есть по имени. Например, пусть фреймворк узнаёт о hidden-классе с именем com.example.Foo/1234 и создаёт файл class, который пытается создать экземпляр этого hidden-класса. Код в файле class будет содержать инструкцию new, которая в конечном счёте указывает на элемент пула констант, обозначающий имя. Если фреймворк попытается записать имя как com/example/Foo.1234, файл class будет некорректным — com/example/Foo.1234 не является допустимой внутренней формой двоичного имени. Если же фреймворк попытается записать имя в допустимой внутренней форме com/example/Foo/1234, то JVM разрешит элемент пула констант так: сначала преобразует имя во внутренней форме в двоичное имя com.example.Foo.1234, а затем попытается загрузить класс с этим именем; скорее всего, это не удастся, и hidden-класс с именем com.example.Foo/1234 точно не будет найден. Hidden-класс не является по-настоящему анонимным, поскольку его имя доступно, но фактически он невидим.
Поскольку пул констант не может номинально ссылаться на hidden-класс, hidden-класс нельзя использовать как суперкласс, тип поля, тип возвращаемого значения или тип параметра. Такая ограниченность применения напоминает анонимные классы в языке Java, но hidden-классы идут дальше: анонимный класс может содержать другие классы, чтобы дать им доступ к своим членам, а hidden-класс не может содержать другие классы (их атрибуты InnerClasses не могут ссылаться на него по имени). Даже сам hidden-класс не может использовать себя как тип поля, тип возвращаемого значения или тип параметра в собственных объявлениях полей и методов.
Важно, что код в hidden-классе может использовать этот hidden-класс напрямую, не прибегая к объекту Class. Дело в том, что инструкции байт-кода в hidden-классе могут ссылаться на hidden-класс символически (независимо от его имени), а не номинально. Например, инструкция new в hidden-классе может создать экземпляр hidden-класса через элемент пула констант, который ссылается непосредственно на элемент this_class в текущем ClassFile. Другие инструкции, такие как getstatic, getfield, putstatic, putfield, invokestatic и invokevirtual, могут обращаться к членам hidden-класса через тот же элемент пула констант. Прямое использование внутри hidden-класса важно, потому что упрощает генерацию hidden-классов средами выполнения языков и фреймворками.
Как правило, у hidden-класса те же возможности рефлексии, что и у обычного класса. То есть код в hidden-классе может определять обычные и hidden-классы и может работать с обычными и hidden-классами через их объекты Class. Hidden-класс может даже выступать в роли lookup-класса. То есть код в hidden-классе может получить lookup-объект для самого себя, что помогает с hidden-классами, входящими в один nest (см. ниже).
Hidden-классы в трассировках стека
Методы hidden-классов по умолчанию не отображаются в трассировках стека. Они представляют детали реализации сред выполнения языков, и никогда не ожидается, что они будут полезны разработчикам при диагностике проблем приложений. Однако их можно включить в трассировки стека с помощью опций -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames.
Есть три API, которые представляют трассировки стека в виде объектов: Throwable::getStackTrace, Thread::getStackTrace и более новый API StackWalker, появившийся в Java 9. В API Throwable::getStackTrace и Thread::getStackTrace фреймы стека hidden-классов по умолчанию опускаются; их можно включить теми же опциями, что и для трассировок стека выше. В API StackWalker реализация JVM должна включать фреймы стека hidden-классов, только если задана опция SHOW_HIDDEN_FRAMES. Так фильтрация трассировок стека может опускать ненужную информацию, когда разработчики диагностируют проблемы приложений.
Hidden-классы в nest (гнёзда) для управления доступом
Nest, появившийся в Java 11 в JEP 181, — это набор классов, которые могут обращаться к private-членам друг друга, но без обходных методов расширения доступа, обычно связанных с вложенными классами в языке Java. Набор определяется статически: один класс служит хостом nest, и в его class-файле перечислены другие классы — члены nest; в свою очередь, члены nest указывают в своих class-файлах, какой класс является хостом nest. Статическое членство хорошо работает для class-файлов, сгенерированных из исходного кода на Java, но обычно его недостаточно для class-файлов, которые динамически генерируют среды выполнения языков. Чтобы помочь таким средам выполнения и поощрить использование Lookup::defineHiddenClass вместо Unsafe::defineAnonymousClass, hidden-класс может присоединиться к nest во время выполнения; обычный класс этого сделать не может.
Hidden-класс можно создать как член существующего nest, передав опцию NESTMATE в Lookup::defineHiddenClass. Nest, к которому присоединяется hidden-класс, не задаётся аргументом Lookup::defineHiddenClass. Вместо этого nest для присоединения определяется по lookup-классу, то есть по классу, код которого изначально получил lookup-объект: hidden-класс становится членом того же nest, что и lookup-класс (см. ниже).
Чтобы Lookup::defineHiddenClass мог добавлять hidden-классы в nest, у lookup-объекта должны быть соответствующие права, а именно доступ PRIVATE и MODULE. Эти права подтверждают, что lookup-объект был получен lookup-классом с намерением позволить другому коду расширять nest.
JVM не допускает вложенных nest. Член одного nest не может быть хостом другого nest, независимо от того, определено членство в nest статически или динамически.
Членство lookup-класса в nest может быть указано статически (через NestHost), если lookup-класс обычный, или может быть задано динамически, если lookup-класс является hidden-классом. Статическое членство в nest проверяется лениво. Для среды выполнения языка или библиотеки фреймворка важно иметь возможность добавлять hidden-классы в nest lookup-класса, у которого может быть некорректное членство в nest. В качестве примера рассмотрим фреймворк LambdaMetaFactory, появившийся в Java 8. Когда исходный код класса C содержит лямбда-выражение, соответствующий файл C.class во время выполнения использует LambdaMetaFactory, чтобы определить hidden-класс, который содержит тело лямбда-выражения и реализует требуемый функциональный интерфейс. У C.class может быть некорректный атрибут NestHost, но при выполнении C никогда не происходит обращения к классу H, указанному в атрибуте NestHost. Поскольку тело лямбда-выражения может обращаться к private-членам C, hidden-класс тоже должен иметь к ним доступ; поэтому LambdaMetaFactory пытается определить hidden-класс как член nest, хостом которого является C.
Предположим, что у нас есть lookup-класс C и что defineHiddenClass вызывается с опцией NESTMATE, чтобы создать hidden-класс и добавить его в nest класса C. Хост nest для hidden-класса определяется так:
- Если C — обычный класс без атрибута
NestHost, то C является хостом самого себя, а также хостом nest для hidden-класса. - Если C — обычный класс с корректным атрибутом
NestHost, указывающим на H, то хост nest класса C, то есть H, является хостом nest для hidden-класса. В этом случае hidden-класс добавляется как член nest класса H. - Если C — обычный класс с некорректным атрибутом NestHost, то C используется как хост nest для hidden-класса.
- Если C — hidden-класс, созданный без опции
NESTMATE, то C является хостом самого себя, а также хостом nest для hidden-класса. - Если C — hidden-класс, созданный с опцией
NESTMATEи динамически добавленный в nest класса D, то хост nest класса D используется как хост nest для hidden-класса.
Если hidden-класс создан без опции NESTMATE, то hidden-класс является хостом собственного nest. Это соответствует правилу, по которому каждый класс либо является членом nest, хостом которого служит другой класс, либо сам является хостом nest. Hidden-класс может создавать дополнительные hidden-классы как члены своего nest: код в hidden-классе сначала получает lookup-объект для самого себя, затем вызывает у этого объекта Lookup::defineHiddenClass и передаёт опцию NESTMATE.
Для объекта Class hidden-класса, созданного как член nest, Class::getNestHost и Class::isNestmateOf работают ожидаемым образом. Class::getNestMembers можно вызвать у объекта Class любого класса в nest — будь то член или хост, обычный или hidden-класс, — но этот метод возвращает только статически определённых членов (то есть обычные классы, перечисленные в NestMembers хоста) вместе с хостом nest.
Class::getNestMembers не включает hidden-классы, динамически добавленные в nest, потому что hidden-классы не обнаруживаются и должны представлять интерес только для создавшего их кода, которому членство в nest уже известно. Так hidden-класс, который должен оставаться закрытым, не станет доступен через членство в nest.
Выгрузка hidden-классов
Класс, определённый загрузчиком классов, тесно связан с этим загрузчиком. В частности, каждый объект Class содержит ссылку на ClassLoader, который его определил. По ней JVM узнаёт, какой загрузчик использовать при разрешении символов в классе. Одно из следствий этой связи: обычный класс нельзя выгрузить, пока сборщик мусора не может освободить определивший его загрузчик (JLS 12.7). Возможность освободить определяющий загрузчик означает, что на загрузчик нет живых ссылок, а это, в свою очередь, означает, что нет живых ссылок ни на один из классов, определённых этим загрузчиком. (Такие классы, будь они достижимы, ссылались бы на загрузчик.) Только в этом состоянии, когда живых ссылок нет нигде, выгрузка обычного класса безопасна.
Поэтому, чтобы повысить шансы на выгрузку обычного класса, важно свести к минимуму ссылки как на сам класс, так и на определивший его загрузчик. Обычно среды выполнения языков добиваются этого, создавая много загрузчиков классов, каждый из которых определяет только один класс или, возможно, небольшое число связанных классов. Когда все экземпляры класса освобождены и при условии, что среда выполнения не удерживает загрузчик классов, можно освободить и класс, и определивший его загрузчик. Однако получающееся большое число загрузчиков классов требует много памяти. Кроме того, по данным микробенчмарков, ClassLoader::defineClass работает значительно медленнее, чем Unsafe::defineAnonymousClass.
Hidden-класс создаётся не загрузчиком классов и лишь слабо связан с загрузчиком классов, который считается его определяющим загрузчиком. Эти факты можно обратить себе на пользу, разрешив выгружать hidden-класс, даже если сборщик мусора не может освободить его номинальный определяющий загрузчик. Пока на hidden-класс есть живые ссылки (на экземпляры hidden-класса или на его объект Class), hidden-класс поддерживает свой номинальный определяющий загрузчик в живых, чтобы JVM могла использовать этот загрузчик для разрешения символов в hidden-классе. Однако когда исчезает последняя живая ссылка на hidden-класс, загрузчик не обязан отвечать тем же и поддерживать hidden-класс в живых.
Выгружать обычный класс, пока его определяющий загрузчик достижим, небезопасно: позднее JVM или или код, использующий рефлексию, может попросить загрузчик перезагрузить класс, то есть загрузить класс с тем же именем. Это может привести к непредсказуемым последствиям, когда статические инициализаторы выполнятся второй раз. При выгрузке hidden-класса такой проблемы нет, поскольку hidden-классы создаются иначе. Имя hidden-класса является результатом Lookup::defineHiddenClass, а не входными данными, поэтому воссоздать «тот же» hidden-класс, выгруженный ранее, невозможно.
По умолчанию Lookup::defineHiddenClass создаёт hidden-класс, который можно выгрузить независимо от того, жив ли ещё его номинальный определяющий загрузчик. То есть когда все экземпляры hidden-класса освобождены и hidden-класс больше не достижим, его можно выгрузить, даже если его номинальный определяющий загрузчик ещё достижим. Такое поведение полезно, когда среда выполнения языка создаёт hidden-класс для обслуживания нескольких классов, определённых произвольными загрузчиками классов: по сравнению и с ClassLoader::defineClass, и с Unsafe::defineAnonymousClass среда выполнения получит выигрыш в объёме занимаемой памяти и в производительности. В других случаях среда выполнения языка может связать hidden-класс лишь с одним обычным классом или, возможно, с небольшим числом обычных классов, у которых тот же определяющий загрузчик, что и у hidden-класса. В таких случаях, когда время жизни hidden-класса должно совпадать со временем жизни обычного класса, в Lookup::defineHiddenClass можно передать параметр STRONG. При этом hidden-класс связывается со своим номинальным определяющим загрузчиком так же тесно, как обычный класс со своим определяющим загрузчиком, то есть hidden-класс будет выгружен, только если можно освободить его номинальный определяющий загрузчик.
Альтернативы
Альтернативы внедрению nestmate во время выполнения нет, кроме существующего обходного пути: генерации package-private мостов доступа, через которые прокси-класс обращается к закрытым членам целевого класса. Альтернативы скрытию класса от других классов, если он виден загрузчику классов, нет.
Тестирование
-
Мы обновим
LambdaMetaFactory,StringConcatFactoryиLambdaForms, чтобы они использовали новые API. Тестирование производительности позволит убедиться, что связывание лямбд и конкатенация строк не стали работать хуже. -
Для новых API будут разработаны модульные тесты.
Риски и допущения
Мы предполагаем, что разработчики, которые сейчас используют Unsafe::defineAnonymousClass, смогут легко перейти на Lookup::defineHiddenClass. Разработчикам следует учитывать три небольших ограничения функциональности hidden-классов по сравнению с VM-anonymous-классами.
-
Доступ к protected-членам. Как ни странно, VM-anonymous-класс может обращаться к членам
protectedсвоего класса-хозяина, даже если VM-anonymous-класс находится в другом пакете времени выполнения и не является подклассом класса-хозяина. Для hidden-классов, напротив, правила контроля доступа применяются корректно: hidden-класс может обращаться к членамprotectedдругого класса, только если hidden-класс находится в том же пакете времени выполнения, что и другой класс, или является его подклассом. Особого доступа hidden-класса к членамprotectedкласса поиска нет. -
Изменение пула констант. VM-anonymous-класс можно определить так, что элементы его пула констант уже разрешены в конкретные значения. Так важные константы могут совместно использоваться VM-anonymous-классом и определившей его средой выполнения языка, а также несколькими VM-anonymous-классами. Например, в адресном пространстве среды выполнения языка часто есть объекты
MethodHandle, которые пригодились бы вновь определяемым VM-anonymous-классам. Вместо того чтобы сериализовать объекты в элементы пула констант VM-anonymous-классов, а затем генерировать в этих классах байт-код, который трудоёмко выполняетldcдля этих элементов, среда выполнения может просто передать вUnsafe::defineAnonymousClassссылки на свои живые объекты. Соответствующие элементы пула констант во вновь определённом VM-anonymous-классе заранее связываются с этими объектами, что повышает производительность и уменьшает объём занимаемой памяти. Кроме того, так VM-anonymous-классы могут ссылаться друг на друга: элементы пула констант в class-файле основаны на именах. Поэтому они не могут ссылаться на безымянные VM-anonymous-классы. Однако среда выполнения языка может легко отслеживать живые объектыClassсвоих VM-anonymous-классов и передавать их вUnsafe::defineAnonymousClass, заранее связывая элементы пула констант нового класса с другими VM-anonymous-классами. У методаLookup::defineHiddenClassэтих возможностей не будет, потому что в одном из будущих улучшений может появиться единообразное предварительное связывание элементов пула констант со всеми классами. -
Самостоятельное управление оптимизацией. VM-anonymous-классы проектировались в расчёте на то, что определять их будет только код JDK. Поэтому у VM-anonymous-классов есть необычная возможность, которая раньше была доступна только классам в JDK, а именно управлять тем, как их оптимизирует HotSpot JVM. Управление осуществляется через атрибуты аннотаций в байтах определения VM-anonymous-класса:
@ForceInlineили@DontInlineзаставляет HotSpot всегда или никогда не встраивать метод, а@Stableзаставляет HotSpot считать ненулевое поле сворачиваемой константой. Однако эта возможность понадобилась очень немногим VM-anonymous-классам, динамически определяемым кодом JDK. Возможно даже, что будущие улучшения сделают эти оптимизации ненужными. Поэтому hidden-классы не смогут управлять своей оптимизацией, даже если их определяет код JDK. (Считается, что это не создаёт никакого риска для перевода кода JDK с определения VM-anonymous-классов на определение hidden-классов.)
С этим связано и то, что VM-anonymous-классы могут с помощью аннотации @Hidden скрывать свои методы из трассировок стека. Разумеется, для hidden-классов эта функциональность работает автоматически и в будущем может быть предложена другим классам.
При переходе следует учитывать следующее:
-
Чтобы вызывать закрытые методы экземпляра nestmate из кода hidden-класса, используйте
invokevirtualилиinvokeinterfaceвместоinvokespecial. Сгенерированный байт-код, который используетinvokespecialдля вызова закрытого метода экземпляра nestmate, не пройдёт верификацию.invokespecialследует использовать только для вызова закрытых конструкторов nestmate. -
Как отмечалось ранее, вызов
getNameдля объектаClasshidden-класса возвращает строку, которая не является двоичным именем, поскольку содержит символ/. Ожидается, что пользовательский код не будет сталкиваться с такими объектамиClass, но код уровня фреймворков, который предполагает, что у каждого класса есть двоичное имя, возможно, придётся обновить для поддержки hidden-классов. Код уровня фреймворков, ранее обновлённый для поддержки VM-anonymous-классов, продолжит работать, поскольку hidden-классы используют то же соглашение об именовании, что и VM-anonymous-классы. -
GetClassSignatureв JVM TI возвращает сигнатуру в стиле JNI и строку, которая не является двоичным именем во внутренней форме, например строку, содержащую символ.. Агенты и инструменты JVM TI, которые предполагают, что у каждого класса есть двоичное имя, возможно, придётся обновить для поддержки hidden-классов. С другой стороны, реализация JDI уже обновлена для поддержки hidden-классов. Hidden-классы не могут изменяться агентами JVM TI. Число инструментов, на которые повлияет сигнатура класса у hidden-классов, должно быть ограниченным.
Зависимости
JEP 181 (Nest-Based Access Control) ввёл контексты контроля доступа на основе гнёзд (nest), в которых все классы и интерфейсы гнезда имеют общий закрытый доступ между nestmate.