Briefly introducing Hidden Classes – Sealed and Hidden Classes

176. Briefly introducing Hidden Classes

Hidden Classes have been introduced in JDK 15 under JEP 371. Their main goal is to be used by frameworks as dynamically generated classes. They are runtime-generated classes with a short lifespan that are used by frameworks via reflection.

Hidden Classes cannot be used directly by the bytecode or other classes. They are not created via a class loader. Basically, a Hidden Class has the class loader of the lookup class.

Among other characteristics of Hidden Classes, we have that:

They are not discoverable by the JVM internal linkage of bytecode or by the explicit usage of class loaders (they are invisible to methods such as Class.forName(), Lookup.findClass(), or ClassLoader.findLoadedClass()). They don’t appear in stack traces.

They extend Access Control Nest (ACN) with classes that cannot be discovered.

Frameworks can define Hidden Classes as many as needed since they benefit from aggressive unloading. This way, a large number of Hidden Classes shouldn’t have a negative impact on performance. They sustain efficiency and flexibility.

They cannot be used as field/return/parameter type. They cannot be superclasses.

They can access their code directly without the presence of a class object.

They can have final fields, and those fields cannot be modified regardless of their accessible flags.

They deprecated the misc.Unsafe::defineAnonymousClass, which is a non-standard API. Starting with JDK 15, lambda expression uses Hidden Classes instead of anonymous classes.

Next, let’s see how we can create and use a Hidden Class.

177. Creating a hidden class

Let’s assume that our hidden class is named InternalMath and is as simple as follows:

public class InternalMath {
  
  public long sum(int[] nr) {
    return IntStream.of(nr).sum();
  }
}

As we mentioned in the previous problem, Hidden Classes have the same class loader as the lookup class which can be obtained via MethodHandles.lookup() as follows:

MethodHandles.Lookup lookup = MethodHandles.lookup();

Next, we must know that Lookup contains a method named defineHiddenClass(byte[] bytes, boolean initialize, ClassOption… options). The most important argument is represented by the array of bytes that contains the class data. The initialize argument is a flag specifying if the Hidden Class should be initialized or not, while the options argument can be NESTMATE (the created hidden class become a nestmate of the lookup class and has access to all the private members in the same nest) or STRONG (the created hidden class can be unloaded only if its defining loader is not reachable).So, our goal is to obtain the array of bytes which contains the class data. For this, we rely on getResourceAsStream() and JDK 9, readAllBytes() as follows:

Class<?> clazz = InternalMath.class;      
String clazzPath = clazz.getName()
  .replace(‘.’, ‘/’) + “.class”;
InputStream stream = clazz.getClassLoader()
  .getResourceAsStream(clazzPath);      
byte[] clazzBytes = stream.readAllBytes();

Having clazzBytes in our hands, we can create the hidden class as follows:

Class<?> hiddenClass = lookup.defineHiddenClass(clazzBytes,
  true, ClassOption.NESTMATE).lookupClass();

Done! Next, we can use the hidden class from inside our framework as follows:

Object obj = hiddenClass.getConstructor().newInstance();
Method method = obj.getClass()
  .getDeclaredMethod(“sum”, int[].class);
System.out.println(method.invoke(
  obj, new int[] {4, 1, 6, 7})); // 18

As you can see, we use the hidden class via reflection. The interesting part here is represented by the fact that we cannot cast the hidden class to InternalMath, so we use Object obj = …. So, this will not work:

InternalMath obj = (InternalMath) hiddenClass
  .getConstructor().newInstance();

However, we can define an interface implemented by the hidden class:

public interface Math {}
public class InternalMath implements Math {…}

And, now we can cast to Math:

Math obj = (Math) hiddenClass.getConstructor().newInstance();

Starting with JDK 16, the Lookup class was enriched with another method for defining a hidden class named defineHiddenClassWithClassData(byte[] bytes, Object classData, boolean initialize, ClassOption… options). This method needs the class data obtained via MethodHandles.classData(Lookup caller, String name, Class<T> type) or MethodHandles.classDataAt(Lookup caller, String name, Class<T> type, int index). Take your time to explore this further.

Summary

This chapter covered 13 problems. Most of them were focused on the sealed classes feature. The last two problems provided a brief coverage of hidden classes.