С переходом Java на более безопасные и стандартизированные подходы к динамической генерации классов, скрытые (hidden) классы стали ключевым механизмом замены устаревшего Unsafe::defineAnonymousClass. Они решают проблемы доступности, управления жизненным циклом и контроля доступа, особенно актуальные для разработчиков фреймворков и языков на JVM. Хотя скрытые классы пока не полностью заменяют функциональность Unsafe, они лежат в основе ряда важных механизмов, такие как, например, реализация лямбд в JDK. Подробнее - в новом переводе от команды Spring АйО.
Как известно, использование API sun.misc.Unsafe не рекомендуется вне JDK, поскольку даже незначительная ошибка может привести к сбою JVM. В некоторых случаях такой код может быть непереносим между различными платформами, а также вызывать множество других проблем.
С другой стороны, Unsafe предоставляет ряд полезных возможностей, для которых в языке до недавнего времени не существовало стандартных альтернатив. Чтобы устранить необходимость в использовании Unsafe, разработчики JDK постепенно внедряют соответствующие стандартные языковые функции — одной из таких функций стали «скрытые классы» в Java 15. После их появления метод sun.misc.Unsafe::defineAnonymousClass был объявлен deprecated и будет удалён в будущих версиях.
Скрытые классы — это классы, которые не могут использоваться напрямую в байткоде других классов. Они позволяют фреймворкам и языкам на JVM создавать классы как некоторые недоступные в рантайме компоненты, которые не могут быть слинкованы с другими классами.
Следующие характеристики скрытых классов помогают лучше понять их природу:
не могут использоваться в качестве суперкласса;
не могут использоваться в качестве типа поля;
не могут быть параметром метода или типом возвращаемого значения;
не могут быть найдены загрузчиком классов с помощью
Class::forName,ClassLoader::loadClass,Lookup::findClassи других подобных методов.
Зачем нужны скрытые классы
Разработчики фреймворков и языков на JVM часто хотят, чтобы динамически создаваемый класс логически имел свойства статически определённого класса реализации.
При этом для таких динамически создаваемых классов важны следующие свойства:
Комментарий от Евгения Сулейманова
Конечно же, вы сами редко генерируете байткод, при этом почти наверняка вы пользуетесь скрытыми классами через фреймворки (лямбды, Spring/Hibernate-прокси, ByteBuddy/Mockito и т.п.). Практическая польза для прикладного разработчика - меньше "мусорных" классов, которые "торчат" в ClassLoader’е и раздувают Metaspace. Понимание скрытых классов помогает быстрее диагностировать странные классы в stacktrace/профайлере и осознанно выбирать инструменты (агенты/моки/прокси), которые не тащат за собой проблемы жизненного цикла классов.
Недоступность (non-discoverability). Динамически создаваемые классы не должны быть доступны другим классам в JVM, например, через
Class::forNameилиClassLoader::loadClass.Жизненный цикл. Динамически создаваемые классы могут быть нужны только на ограниченное время, и их сохранение на весь срок жизни статического класса может неоправданно увеличивать потребление памяти. Существующие обходные пути, например, создание отдельных загрузчиков классов для каждого случая, являются громоздкими и неэффективными.
Комментарий от Михаила Поливаха
Тут автор не поясняет, но тема не всем известная.
Дело в том, что используя условный Proxy.newProxyInstance(), вы создаете новый класс в рантайме. Помимо интерфейсов, которые должен реализовывать JDK прокси, вы передаете в том числе InvocationHandler, и что нам сейчас важно - ClassLoader, которым должен быть загружен данный класс.
Не все знают, но в Java класс опередляется не просто именем пакета и непосредственно именем класса - он в том числе определяется class loader-ом, который загрузил тот или иной класс.
Так вот смотрите, нюанс такой, что экземпляры дискрипторов класса (java.lang.Class) не могут быть собарны сборщиком мусора до тех пор, пока тот класслоадер, что загрузил данный java.lang.Class не сам по себе не станет доступен для сборки мусора.
То есть надо убить условный AppClassLoader - мы понимаем, что это вообще внештатная ситуация, такое просто так не случается. Одно из решений частых (костыль по сути) - создавать отдельный classloader на каждый сгенерированный класс. Вот о чем говорил автор.
Контроль доступа. Может потребоваться расширить контекст контроля доступа статического класса на динамически создаваемый класс.
Комментарий от Михаила Поливаха
Речь о том, чтобы сгенерированный в рантайме hidden класс имел тот же контекст привилегий, что и оригинальный Lookup объект, который исопльзовался при генерации класса. Читайте ниже, будет понятнее.
Существующие стандартные API, такие как ClassLoader::defineClass и Lookup::defineClass, всегда создают видимые классы, которые сохраняются в ��истеме дольше, чем хотелось бы. В отличие от них, скрытые классы создаются динамически и соответствуют всем трём требованиям, важным для разработчиков фреймворков и языков.
Как создавать скрытые классы
Скрытые классы создаются с помощью метода Lookup::defineHiddenClass. Он позволяет JVM загрузить скрытый класс из переданных байтов, связать его и вернуть объект Lookup, предоставляющий рефлексивный доступ к этому классу.
Ниже описаны четыре шага по созданию и использованию скрытых классов.
1. Создание объекта Lookup
Сначала необходимо получить объект Lookup, который будет использоваться для создания скрытого класса на следующих этапах:
MethodHandles.Lookup lookup = MethodHandles.lookup();2. Создание байткода класса с помощью ASM
Мы используем библиотеку ASM для манипуляции байткодом.
Комментарий от Михаила Поливаха
Начиная с Java 24 уже можно и даже желательно использовать Class File API. ASM это довольно старое решение, хотя Spring Framework например его активно использует до сих пор, хотя уже в планах переход на Class File API.
Сначала создаётся объект ClassWriter с помощью вспомогательного класса GenerateClass. Внутри GenerateClass реализован интерфейс Test, который мы используем в дальнейшем:
ClassWriter cw = GenerateClass.getClassWriter(HiddenClassDemo.class);
byte[] bytes = cw.toByteArray();3. Определение скрытого класса
На этом шаге создаётся скрытый класс. Важно: вызывающая программа должна сохранить объект lookup, так как это единственный способ получить дескриптор java.lang.Class скрытого класса:
Class<?> c = lookup.defineHiddenClass(bytes, true, NESTMATE).lookupClass();4. Использование скрытого класса
Теперь используем рефлексию для доступа к скрытому классу. Сначала получаем конструктор, затем создаём объект, приводим его к интерфейсу Test и вызываем метод test. Метод игнорирует переданный аргумент и выводит в консоль строку "Hello test":
Constructor<?> constructor = c.getConstructor(null);
Object object = constructor.newInstance(null);
Test test = (Test) object;
test.test(new String[]{""});Ниже приведена общая структура описанного выше кода.
Полный пример доступен в репозитории на GitHub
public class HiddenClassDemo {
public static void main(String[] args) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
ClassWriter cw =
GenerateClass.getClassWriter(HiddenClassDemo.class);
byte[] bytes = cw.toByteArray();
Class<?> c = lookup.defineHiddenClass(bytes, true, NESTMATE).lookupClass();
Constructor<?> constructor = c.getConstructor(null);
Object object = constructor.newInstance(null);
Test test = (Test) object;
/* This way of creating instance is deprecated.
Test test = (Test)c.newInstance();
*/
test.test(new String[]{"sample"});
System.out.println("End of main method in class " + HiddenClassDemo.class.getName());
}
}public interface Test {
void test(String[] args);
}public static ClassWriter getClassWriter(Class<HiddenClassDemo> ownerClassName) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, getHiddenClassName(ownerClassName),
null, "java/lang/Object", new String[] {"com/vip/jfeatures/jdk15/hiddenclass/Test"});
...
...
...
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "test",
"([Ljava/lang/String;)V", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello test");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
}Скрытые классы как альтернатива Unsafe
До Java 15 для генерации динамических классов использовался нестандартный API Unsafe::defineAnonymousClass. Однако, как известно, использование Unsafe не рекомендуется, и теперь скрытые классы считаются предпочтительным способом создания динамических классов.
Перед тем как перейти с Unsafe::defineAnonymousClass на Lookup::defineHiddenClass (скрытые классы), следует учитывать несколько ограничений:
Отсутствие поддержки патчинга пула констант. В отличие от
Unsafe::defineAnonymousClass, скрытые классы не поддерживают модификацию пула констант. Ведётся работа по улучшению этого аспекта, но пока это ограничение остаётся.Оптимизация с помощью аннотации @ForceInline недоступна. JVM может оптимизировать VM-анонимные классы (созданные через
Unsafe) с помощью аннотации@ForceInline, но такая оптимизация недоступна для скрытых классов.Ограниченный доступ к защищённым членам. VM-анонимный класс может получить доступ к защищённым членам своего хост-класса, даже если он находится в другом пакете и не является сабклассом для хост-класса. Скрытый же класс может получить доступ к защищённым членам только в том случае, если он находится в том же пакете времени выполнения или является подклассом. Скрытые классы не имеют специального доступа к защищённым членам lookup-класса.
Из-за этих ограничений в Java 15 скрытые классы пока не являются полноценной заменой Unsafe::defineAnonymousClass.
Лучшим примером использования скрытых классов является реализация лямбда-выражений в JDK. Разработчики JDK стремятся не раскрывать классы, создаваемые для лямбда-выражений, поэтому компилятор javac не транслирует их в отдельные классы. Вместо этого он генерирует байткод, который динамически создаёт и инстанцирует соответствующий класс по мере необходимости. До Java 15 для этого использовался Unsafe::defineAnonymousClass, а теперь используется defineHiddenClass.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
