Java Hidden Classes: Enhancing Runtime Class Management

Explore Java 15's innovative hidden classes designed for frameworks that generate runtime classes. Understand their unique access control, benefits for dynamic class generation, and how they can be unloaded independently, enhancing application performance and security.



Java - Hidden Classes

Java 15 has introduced hidden classes that cannot be used directly by other classes' bytecode. These hidden classes are intended for use by frameworks that generate classes at runtime and utilize them through reflection.

A hidden class is defined as a member of the Based Access Control Context and can be unloaded independently of other classes.

This proposal, JEP 371, aims to enhance all languages on the JVM by providing a standard API for defining hidden classes that are not discoverable and have a limited lifecycle. Both JDK frameworks and external frameworks can dynamically generate hidden classes.

JVM languages rely heavily on dynamic class generation for flexibility and efficiency.

Goals

  • Frameworks should define classes as non-discoverable implementation details, meaning these classes cannot be linked to other classes or discovered using reflection.
  • Extend Access Control Nest with non-discoverable classes.
  • Aggressive unloading of hidden classes will allow frameworks to define numerous hidden classes without degrading performance.
  • Deprecate the non-standard API, misc.Unsafe::defineAnonymousClass, which will be removed in future releases.

Creating a Hidden Class

To create a hidden class, we first need to create a Lookup instance as shown below:

Code Snippet

MethodHandles.Lookup lookup = MethodHandles.lookup();

Once the lookup instance is available, we can use the defineHiddenClass() method to create a hidden class using a byte array:

Code Snippet

Class hiddenClass = lookup.defineHiddenClass(getByteArray(), true, ClassOption.NESTMATE).lookupClass();

The byte array of the hidden class can be retrieved using the classpath of the hidden class:

Code Snippet

public static byte[] getByteArray() throws IOException {
InputStream stream = Util.class.getClassLoader().getResourceAsStream("com/tutorialsarena/Util.class");
byte[] bytes = stream.readAllBytes();
return bytes;
}

Once the hiddenClass is loaded, we can create its instance using the getConstructor() method:

Code Snippet

Object hiddenClassObj = hiddenClass.getConstructor().newInstance();

Using the hidden class instance, we can get the method and execute it as shown below:

Code Snippet

Method method = hiddenClassObj.getClass().getDeclaredMethod("square", Integer.class);
Object result = method.invoke(hiddenClassObj, 3);

As the hidden class is hidden and cannot be instantiated using reflection, its hidden property is true and the canonical name is null.

Example to Create and Use a Hidden Class

The following example shows the creation and use of a hidden class. First, we've created a public class Util as shown below:

Code Snippet

package com.tutorialsarena;

public class Util {
public Integer square(Integer n) {
    return n * n;
}
}

Now, we've created this class as a hidden class and accessed its method to get the square of a number:

Code Snippet

package com.tutorialsarena;

import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup.ClassOption;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Tester {
public static void main(String args[]) throws IllegalAccessException, IOException, InstantiationException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
    // create the lookup object
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    
    // define the hidden class using the byte array of Util class
    // Using NESTMATE option so that hidden class has access to 
    // private members of classes in the same nest
    Class hiddenClass = lookup.defineHiddenClass(getByteArray(), true, ClassOption.NESTMATE).lookupClass();

    // get the hidden class object
    Object hiddenClassObj = hiddenClass.getConstructor().newInstance();

    // get the hidden class method
    Method method = hiddenClassObj.getClass().getDeclaredMethod("square", Integer.class);

    // call the method and get result
    Object result = method.invoke(hiddenClassObj, 3);

    // print the result
    System.out.println(result);

    // as hidden class is not visible to jvm, it will print hidden
    System.out.println(hiddenClass.isHidden());

    // canonical name is null thus this class cannot be instantiated using reflection 
    System.out.println(hiddenClass.getCanonicalName());
}

public static byte[] getByteArray() throws IOException {
    InputStream stream = Util.class.getClassLoader().getResourceAsStream("com/tutorialsarena/Util.class");
    byte[] bytes = stream.readAllBytes();
    return bytes;
}
}

Output

Output

9
true
null