Back to Anonymous Classes
Q5Question 5 of 6

Hidden classes (JEP 371): what they are, why introduced, how created with `defineHiddenClass`, and how they replace `Unsafe.defineAnonymousClass()`

Key Concepts
  • A hidden class (JEP 371, Java 15) is a runtime-generated class that is not registered in the class loader's dictionary.
  • It cannot be discovered by `Class.forName()` or referenced by name from other classes' bytecode.
  • It can be unloaded independently of its defining class loader, saving metaspace in dynamic frameworks.
  • Created via `MethodHandles.Lookup.defineHiddenClass(byte[], boolean, ClassOption...)` from raw class bytes.
  • The `NESTMATE` option grants shared private-member access between the hidden class and its host.
  • Hidden classes are the supported replacement for the internal, unsupported `Unsafe.defineAnonymousClass()`.
  • The JDK's own lambda metafactory uses hidden classes to generate lambda implementation classes.

Definition

A hidden class is a runtime-generated class that is invisible to normal class loading, usable only through its creator's reference, and independently unloadable.
  • Hidden class – a class defined from raw bytecode at runtime via MethodHandles.Lookup.defineHiddenClass(), introduced by JEP 371 in Java 15.
  • Not registered – the class has no entry in the class loader's name-to-class table, so Class.forName() cannot find it and other classes cannot reference it by name.
  • Independent unloading – by default it can be garbage collected as soon as it is unreachable, even while its defining class loader stays alive.
  • NESTMATE option – makes the hidden class a nestmate of the lookup class, granting shared private-member access.
  • Replaces Unsafe – the supported, well-specified replacement for sun.misc.Unsafe.defineAnonymousClass(), which was internal and undocumented.

Imagine National Bank of India needs a one-off, custom calculation rule generated on the fly for a single complex trade. This rule should never appear in the bank's public directory of departments. It is created privately, used by exactly the one service that made it, and quietly disposed of when that service no longer needs it. No other department can look it up by name or call it directly. That invisible, creator-only, disposable helper is a hidden class.

Understanding the Concepts

What is a hidden class?

A hidden class is defined from a byte array of class bytes at runtime, but unlike a normal class it is not registered in the class loader's dictionary. No other class can refer to it by name in its constant pool, Class.forName() cannot load it, and it does not participate in symbolic resolution. The only way to use it is through the Class object returned when you define it.

JAVA
package com.bank.service;

import java.lang.invoke.MethodHandles;

public class HiddenClassBasic {
    public static void main(String[] args) throws Exception {
        // Load pre-compiled class bytes (e.g., a fee calculator)
        byte[] classBytes = HiddenClassBasic.class
                .getResourceAsStream("FeeCalculator.class")
                .readAllBytes();

        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClass(
                classBytes, true // initialize immediately
        );

        Class<?> hiddenClass = hiddenLookup.lookupClass();
        System.out.println("Hidden class name: " + hiddenClass.getName());
        System.out.println("Is hidden: " + hiddenClass.isHidden());

        // Verify it cannot be found by name
        try {
            Class.forName(hiddenClass.getName());
        } catch (ClassNotFoundException e) {
            System.out.println("Class.forName failed: " + e.getMessage());
        }
    }
}
TEXT
Hidden class name: com.bank.service.FeeCalculator/0x00000123456
Is hidden: true
Class.forName failed: com.bank.service.FeeCalculator/0x00000123456

The name contains a / that makes it an illegal binary name, ensuring it can never collide with a real class.

Why were hidden classes introduced?

Frameworks had long relied on sun.misc.Unsafe.defineAnonymousClass(), an internal, undocumented, unsupported method, to create throwaway classes efficiently. That method had quirky, poorly specified behaviour (notably ad-hoc constant-pool patching) and was blocking the removal of internal APIs. JEP 371 provided a standard, supported, well-specified mechanism. The JDK's own lambda metafactory moved to hidden classes.

JAVA
// OLD way (removed in modern JDK):
// Unsafe.getUnsafe().defineAnonymousClass(hostClass, classBytes, null);

// NEW way (Java 15+):
// MethodHandles.lookup().defineHiddenClass(classBytes, true, ClassOption.NESTMATE);

How do you create a hidden class with defineHiddenClass?

The method signature is Lookup.defineHiddenClass(byte[] bytes, boolean initialize, ClassOption... options). The bytes are the compiled class file contents. The initialize flag controls whether to run static initializers immediately. ClassOption values include NESTMATE and STRONG.

JAVA
package com.bank.service;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.math.BigDecimal;

public class HiddenClassCreation {
    private BigDecimal feeRate = new BigDecimal("0.015");

    public void generateAndUseCalculator() throws Exception {
        byte[] classBytes = getClass()
                .getResourceAsStream("DynamicFeeCalc.class")
                .readAllBytes();

        Lookup lookup = MethodHandles.lookup();

        // Define with NESTMATE for private access to host
        Lookup hiddenLookup = lookup.defineHiddenClass(
                classBytes,
                true, // initialize
                MethodHandles.Lookup.ClassOption.NESTMATE
        );

        Class<?> hidden = hiddenLookup.lookupClass();
        Object instance = hidden.getConstructor().newInstance();
        System.out.println("Created instance: " + instance.getClass().isHidden());
    }
}

What does the NESTMATE option do?

The NESTMATE option makes the hidden class a nestmate of the lookup class. Java's nest concept lets classes in the same nest access each other's private members directly. This is essential for generated code that must touch private internals of its host, which is exactly what the lambda metafactory needs.

JAVA
package com.bank.service;

import java.lang.invoke.MethodHandles;
import java.math.BigDecimal;

public class NestmateDemo {
    // Private field that the hidden class needs to access
    private BigDecimal interestRate = new BigDecimal("0.085");

    public void demonstrateNestmate() throws Exception {
        byte[] classBytes = loadCalculatorBytes();

        MethodHandles.Lookup lookup = MethodHandles.lookup();
        // NESTMATE grants access to NestmateDemo's private members
        MethodHandles.Lookup hidden = lookup.defineHiddenClass(
                classBytes, true,
                MethodHandles.Lookup.ClassOption.NESTMATE
        );

        // The hidden class can now read 'interestRate' directly
        System.out.println("Hidden class is nestmate of: "
                + hidden.lookupClass().getNestHost().getSimpleName());
    }

    private byte[] loadCalculatorBytes() {
        return new byte[0]; // placeholder
    }
}

How does a hidden class replace Unsafe.defineAnonymousClass?

The Unsafe method created a VM-anonymous class that was likewise not registered and could be unloaded, but it did so through an unsupported path and bundled a constant-pool patching capability. Hidden classes provide the same core benefits through a public API while deliberately dropping the unsafe constant-pool patching.

| Feature | Unsafe.defineAnonymousClass | defineHiddenClass |
|---|---|---|
| API status | Internal, unsupported | Public, supported (Java 15+) |
| Name registration | Not registered | Not registered |
| Independent unloading | Yes | Yes (default) |
| Private host access | Via host class param | Via NESTMATE option |
| Constant-pool patching | Yes (ad-hoc) | No (dropped intentionally) |
| Module integration | Poor | Full JPMS integration |

What about garbage collection and metaspace?

By default, a hidden class can be unloaded as soon as it is unreachable, even while its defining class loader stays alive. This prevents metaspace exhaustion in applications that generate thousands of short-lived classes.

JAVA
package com.bank.service;

import java.lang.invoke.MethodHandles;

public class HiddenClassGC {
    public void demonstrateUnloading() throws Exception {
        for (int i = 0; i < 10_000; i++) {
            byte[] classBytes = generateRuleClassBytes(i);
            MethodHandles.Lookup hidden = MethodHandles.lookup()
                    .defineHiddenClass(classBytes, true);

            // Use and discard: the hidden class is eligible for GC
            Object instance = hidden.lookupClass().getConstructor().newInstance();
            // After this iteration, no strong reference remains
        }
        // All 10,000 hidden classes can be unloaded independently
        System.out.println("All rule classes created and eligible for GC");
    }

    private byte[] generateRuleClassBytes(int ruleId) {
        return new byte[0]; // placeholder for ASM-generated bytes
    }
}

What are the limitations of hidden classes?

Hidden classes cannot be the superclass or field type of normal classes because they are nameless to the resolver. Their names contain an illegal / so they can never collide with a real binary name. You cannot serialize them by class name, and you must generate valid bytecode to use them.

JAVA
// Cannot do this:
// class MyClass extends HiddenFeeCalc { } // HiddenFeeCalc has no resolvable name

// Cannot do this:
// Class.forName("com.bank.service.FeeCalc/0x123"); // ClassNotFoundException

What should a senior engineer know?

Hidden classes integrate with method handles and JPMS modules. The hidden class lives in the same runtime package and module as its lookup class, inheriting that module's access and readability. This makes them a clean fit for strongly-encapsulated modular banking applications. The STRONG option ties the hidden class to the loader's lifetime (useful for cached proxies), while the default weak behaviour lets it unload independently (useful for per-request rules).

JAVA
package com.bank.service;

import java.lang.invoke.MethodHandles;

public class HiddenClassModuleDemo {
    public void showModuleIntegration() throws Exception {
        byte[] classBytes = new byte[0]; // placeholder

        MethodHandles.Lookup hidden = MethodHandles.lookup()
                .defineHiddenClass(classBytes, true,
                        MethodHandles.Lookup.ClassOption.NESTMATE);

        Class<?> hiddenClass = hidden.lookupClass();
        System.out.println("Module: " + hiddenClass.getModule().getName());
        System.out.println("Package: " + hiddenClass.getPackageName());
        System.out.println("Nest host: " + hiddenClass.getNestHost().getSimpleName());
    }
}

Visual Overview

DIAGRAM
┌─────────────────────────────────────────────┐
│      Hidden Class Lifecycle (JEP 371)       │
├─────────────────────────────────────────────┤
│                                             │
│  1. Generate class bytes (ASM / ByteBuddy)  │
│           │                                 │
│           ▼                                 │
│  2. MethodHandles.lookup()                  │
│       .defineHiddenClass(bytes, init, opts) │
│           │                                 │
│           ▼                                 │
│  3. JVM creates class:                      │
│     ┌─────────────────────────────┐         │
│     │ Name: Host/0x00000123456    │         │
│     │ NOT in classloader dict     │         │
│     │ Module: same as host        │         │
│     │ Package: same as host       │         │
│     │ Nest: host (if NESTMATE)    │         │
│     └─────────────────────────────┘         │
│           │                                 │
│           ▼                                 │
│  4. Use via Lookup/Class reference only     │
│           │                                 │
│           ▼                                 │
│  5. No strong refs remaining?               │
│     ├── Default (weak): GC + unload         │
│     └── STRONG: lives with classloader      │
│                                             │
└─────────────────────────────────────────────┘

Key Takeaway

A hidden class, introduced by JEP 371 in Java 15, is a class generated at runtime that is invisible to normal class loading. It has no entry in the class loader's dictionary, so no other class can reference it by name, Class.forName() cannot find it, and it does not participate in symbolic resolution. The only way to use it is through the Class object or method-handles lookup returned when you define it.

They were introduced to give frameworks a supported replacement for the internal Unsafe.defineAnonymousClass() method. That method was undocumented, unsupported, and had quirky constant-pool patching behaviour, and it was blocking the removal of internal APIs. JEP 371 provides a standard, well-specified mechanism, and the JDK's own lambda machinery moved to hidden classes.

You create a hidden class with MethodHandles.Lookup.defineHiddenClass(), passing the class bytes, an initialize flag, and class options. The NESTMATE option makes the hidden class a nestmate of the lookup class so it can share private access, and the STRONG option ties its lifetime to the loader. In a banking project at National Bank of India, Pankaj's fee rules engine generates a tiny fee-calculation class for high-value trades, defines it as a hidden class, instantiates it, and computes the fee.

The memory benefit is the key production advantage. By default, hidden classes can be unloaded as soon as they are unreachable, even though the loader stays alive. When thousands of trades each generate a rule class, hidden classes prevent metaspace from growing unboundedly, which is exactly what a dynamic rules engine needs.

Remember one sentence: a hidden class is a runtime-generated class that is invisible to normal class loading, independently unloadable, and the supported replacement for Unsafe.defineAnonymousClass().

---

Share
Use ← → arrow keys to navigate