Definition
- 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.
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());
}
}
}
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.
// 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.
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.
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.
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.
// 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).
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
┌─────────────────────────────────────────────┐ │ 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().
---