Back to Hidden Classes
Q1Question 1 of 2

`ClassOption.WEAK` vs `STRONG` for hidden-class GC, and what `NESTMATE` enables

Key Concepts
  • By default (no `STRONG`), a hidden class is weakly reachable and can be unloaded as soon as it becomes unreachable, even while its loader lives.
  • The `STRONG` option ties the hidden class's lifetime to its defining class loader, matching normal class behaviour.
  • Use weak (default) for short-lived, per-request generated classes to prevent metaspace exhaustion.
  • Use `STRONG` for cached, long-lived generated classes that are reused many times.
  • `NESTMATE` makes the hidden class a nestmate of the lookup class, granting shared private-member access.
  • The options compose: you can pass both `STRONG` and `NESTMATE` for a cached proxy with private host access.
  • `NESTMATE` formalises what the old `Unsafe` host-class mechanism did through an unsupported path.

Definition

STRONG keeps a hidden class alive as long as its loader lives, the default lets it unload when unreachable, and NESTMATE grants shared private-member access between the hidden class and its host.
  • Default (weak) – a hidden class is weakly reachable relative to its defining loader, so it can be garbage collected and unloaded as soon as it becomes unreachable, even while the loader stays alive.
  • STRONG – ties the hidden class's lifetime to its defining class loader, so it is only unloaded when the loader itself is collected, matching normal class behaviour.
  • NESTMATE – makes the hidden class a nestmate of the lookup class, granting it access to the lookup class's private members and vice versa.
  • Composable – you can pass both STRONG and NESTMATE together for a long-lived generated class with private host access.
  • Metaspace impact – weak hidden classes prevent metaspace exhaustion in frameworks generating many short-lived classes.

Imagine National Bank of India generates temporary helper rules. A STRONG helper is like an employee given a permanent contract tied to their whole department: they stay as long as the department exists. A weak helper is like a contractor hired for a single task who is let go the moment the task is done and nobody needs them. NESTMATE is like giving the helper a family membership in the department so they can open the private filing cabinets the regular staff use.

Understanding the Concepts

What is the default (weak) lifecycle for hidden classes?

Normal classes are reachable from their defining class loader, so they are only unloaded when the entire loader becomes unreachable. Hidden classes change this. By default (no STRONG option), a hidden class is only weakly held by the runtime, meaning that once there are no strong references to its Class object or its instances, it can be garbage collected and unloaded independently.

JAVA
package com.bank.service;

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

public class WeakHiddenClassDemo {
    public void generatePerRequest() throws Exception {
        for (int i = 0; i < 5000; i++) {
            byte[] classBytes = generateValidatorBytes(i);

            // Default: no STRONG option = weak = independently unloadable
            Lookup hidden = MethodHandles.lookup()
                    .defineHiddenClass(classBytes, true);

            Object validator = hidden.lookupClass()
                    .getConstructor().newInstance();
            // Use the validator for this request...
            // After this iteration, no strong ref remains
            // The hidden class is eligible for GC + unload
        }
        System.out.println("5000 validators created and eligible for GC");
    }

    private byte[] generateValidatorBytes(int id) {
        return new byte[0]; // placeholder for ASM-generated bytecode
    }
}

This is the memory win: frameworks generating many short-lived classes do not accumulate them in metaspace.

What does the STRONG option do?

The STRONG option changes the lifecycle. A strong hidden class is kept alive as long as its defining class loader is alive, exactly like a normal class. You choose STRONG when the hidden class is cached and reused many times, and regenerating it would waste CPU.

JAVA
package com.bank.service;

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

public class StrongHiddenClassDemo {
    private Class<?> cachedCalculator;

    public void initCachedCalculator() throws Exception {
        byte[] classBytes = generateInterestCalcBytes();

        // STRONG: lives as long as the class loader
        Lookup hidden = MethodHandles.lookup()
                .defineHiddenClass(classBytes, true,
                        Lookup.ClassOption.STRONG);

        cachedCalculator = hidden.lookupClass();
        System.out.println("Cached calculator created: "
                + cachedCalculator.isHidden());
        // This class will NOT be unloaded until the loader is collected
    }

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

When should you use weak vs STRONG?

The decision depends on the lifecycle of the generated class.

| Scenario | Option | Reason |
|---|---|---|
| Per-request validator | Default (weak) | Discarded after each request, prevents metaspace leak |
| Per-trade rule class | Default (weak) | Thousands generated, each used once |
| Cached interest calculator | STRONG | Reused for entire service lifetime, regeneration is wasteful |
| Cached serialization proxy | STRONG | Shared across all requests, stable |

JAVA
package com.bank.service;

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

public class LifecycleDecision {

    // Per-request: use weak (default)
    public Object createTransferValidator(byte[] classBytes) throws Exception {
        Lookup hidden = MethodHandles.lookup()
                .defineHiddenClass(classBytes, true);
        return hidden.lookupClass().getConstructor().newInstance();
    }

    // Cached singleton: use STRONG
    public Class<?> createCachedFeeEngine(byte[] classBytes) throws Exception {
        Lookup hidden = MethodHandles.lookup()
                .defineHiddenClass(classBytes, true,
                        Lookup.ClassOption.STRONG);
        return hidden.lookupClass();
    }
}

What does the NESTMATE option enable?

The Java nest concept lets classes in the same nest access each other's private members directly. When you pass NESTMATE, the hidden class becomes a nestmate of the lookup class, so the hidden class can read and call the lookup class's private fields and methods, and vice versa.

JAVA
package com.bank.service;

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

public class NestmateAccessDemo {
    // Private field: only accessible to nestmates
    private BigDecimal feeRate = new BigDecimal("0.015");

    public void generateFeeCalculator() throws Exception {
        byte[] classBytes = generateCalcBytes();

        // NESTMATE grants private access to this host class
        Lookup hidden = MethodHandles.lookup()
                .defineHiddenClass(classBytes, true,
                        Lookup.ClassOption.NESTMATE);

        Class<?> calcClass = hidden.lookupClass();
        System.out.println("Nest host: " + calcClass.getNestHost().getSimpleName());
        System.out.println("Is nestmate of host: "
                + calcClass.getNestHost().equals(NestmateAccessDemo.class));

        // The generated class can now read 'feeRate' directly
    }

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

    public static void main(String[] args) throws Exception {
        new NestmateAccessDemo().generateFeeCalculator();
    }
}
TEXT
Nest host: NestmateAccessDemo
Is nestmate of host: true

Can you combine STRONG and NESTMATE?

Yes. The options compose. A cached proxy that must call private host methods uses both STRONG (long-lived) and NESTMATE (private access).

JAVA
package com.bank.service;

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

public class ComposedOptionsDemo {
    private String encryptionKey = "NBI-SECRET-2024";

    public Class<?> createCachedProxy() throws Exception {
        byte[] classBytes = generateProxyBytes();

        // Both STRONG (long-lived) and NESTMATE (private access)
        Lookup hidden = MethodHandles.lookup()
                .defineHiddenClass(classBytes, true,
                        Lookup.ClassOption.STRONG,
                        Lookup.ClassOption.NESTMATE);

        return hidden.lookupClass();
        // This proxy lives with the loader and can read encryptionKey
    }

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

What is the metaspace impact in production?

In a banking microservice that generates rule classes per request, relying on the default weak behaviour avoids a slow metaspace leak that would eventually trigger OutOfMemoryError: Metaspace. The STRONG option must be used carefully because strong hidden classes accumulate like normal classes.

JAVA
package com.bank.service;

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

public class MetaspaceDemo {
    public void showMetaspaceRisk() throws Exception {
        // DANGEROUS: STRONG with many short-lived classes
        for (int i = 0; i < 100_000; i++) {
            byte[] classBytes = generateRuleBytes(i);
            MethodHandles.lookup().defineHiddenClass(
                    classBytes, true,
                    Lookup.ClassOption.STRONG // BAD: these never unload!
            );
        }
        // 100,000 classes stuck in metaspace until loader dies
        // Risk: OutOfMemoryError: Metaspace
    }

    public void showMetaspaceCorrect() throws Exception {
        // CORRECT: default weak for short-lived classes
        for (int i = 0; i < 100_000; i++) {
            byte[] classBytes = generateRuleBytes(i);
            Lookup hidden = MethodHandles.lookup()
                    .defineHiddenClass(classBytes, true); // default = weak
            Object rule = hidden.lookupClass().getConstructor().newInstance();
            // Use rule, then discard: GC can collect the class
        }
    }

    private byte[] generateRuleBytes(int id) {
        return new byte[0]; // placeholder
    }
}

How does NESTMATE replace the old Unsafe host-class mechanism?

The old Unsafe.defineAnonymousClass(hostClass, bytes, null) had an implicit host-class parameter that gave the anonymous class access to the host's internals. NESTMATE formalises that same access through the specified nest-based rules, making it standard and auditable.

JAVA
// OLD (internal, removed):
// Class<?> old = Unsafe.getUnsafe()
//     .defineAnonymousClass(HostClass.class, bytes, null);
// old could access HostClass private members implicitly

// NEW (Java 15+, standard):
// Lookup hidden = MethodHandles.lookup()
//     .defineHiddenClass(bytes, true, ClassOption.NESTMATE);
// hidden class can access host private members via nest rules

What should a senior engineer know?

In production, the choice between WEAK and STRONG determines whether your framework leaks metaspace. WEAK (the default) is correct for most use cases: generated proxies, lambda implementation classes, and script-compiled classes should be garbage-collected when no longer referenced. STRONG is reserved for classes that must outlive their defining class loader, such as JIT-compiled code or shared interop stubs. Monitor jdk.ClassDefine JFR events and metaspace usage to detect leaks.

JAVA
package com.bank.runtime;

public class HiddenClassProductionAdvice {
    public static void main(String[] args) {
        System.out.println("Production advice for hidden classes:");
        System.out.println("  Default (WEAK): class GC'd when unreferenced");
        System.out.println("    Use for: proxies, lambdas, script classes");
        System.out.println("    Monitor: metaspace should stay stable");
        System.out.println();
        System.out.println("  STRONG: class lives as long as defining loader");
        System.out.println("    Use for: JIT code, shared interop stubs");
        System.out.println("    Risk: metaspace leak if loader is long-lived");
        System.out.println();
        System.out.println("  Monitoring:");
        System.out.println("    JFR event: jdk.ClassDefine (count hidden defines)");
        System.out.println("    JMX: java.lang:type=MemoryPool,name=Metaspace");
    }
}
TEXT
Expected output:
Production advice for hidden classes:
  Default (WEAK): class GC'd when unreferenced
    Use for: proxies, lambdas, script classes
    Monitor: metaspace should stay stable

  STRONG: class lives as long as defining loader
    Use for: JIT code, shared interop stubs
    Risk: metaspace leak if loader is long-lived

  Monitoring:
    JFR event: jdk.ClassDefine (count hidden defines)
    JMX: java.lang:type=MemoryPool,name=Metaspace

Visual Overview

DIAGRAM
┌─────────────────────────────────────────────┐
│   Hidden Class: ClassOption Decision Tree   │
├─────────────────────────────────────────────┤
│                                             │
│  defineHiddenClass(bytes, init, options...)  │
│        │                                    │
│        ├── No options (default)              │
│        │   └── WEAK lifecycle                │
│        │       └── Unloads when unreachable  │
│        │           (even if loader alive)    │
│        │                                    │
│        ├── STRONG                            │
│        │   └── Tied to class loader          │
│        │       └── Unloads when loader dies  │
│        │                                    │
│        ├── NESTMATE                          │
│        │   └── Shared private access         │
│        │       └── Hidden ↔ Host privates    │
│        │                                    │
│        └── STRONG + NESTMATE                 │
│            └── Long-lived + private access   │
│                └── Cached proxy pattern      │
│                                             │
├─────────────────────────────────────────────┤
│  Metaspace Impact                           │
│                                             │
│  WEAK:   ■□□□□□ (low, classes unload)       │
│  STRONG: ■■■■■■ (grows with loader)         │
└─────────────────────────────────────────────┘

Key Takeaway

When you define a hidden class with defineHiddenClass, the class options control its lifecycle and access. By default, with no STRONG option, a hidden class is only weakly reachable, meaning it can be garbage collected and unloaded on its own as soon as nothing strongly references its Class object or instances, even though the loader stays alive. This is the memory benefit for frameworks generating many short-lived classes.

The STRONG option ties the hidden class to the lifetime of its defining class loader, exactly like a normal class. You choose STRONG when a generated class is cached and reused many times and regenerating it would waste CPU. In a banking project at National Bank of India, Pankaj uses the weak default for a validator generated per transfer request so it unloads after the request, and STRONG for a single cached interest calculator that lives for the whole service.

NESTMATE is about access rather than lifecycle. Java's nest concept lets classes in the same nest read each other's private members directly. When you pass NESTMATE, the hidden class becomes a nestmate of the lookup class, so the generated class can read the host's private fields and methods. This is exactly what the JDK's own lambda metafactory needs. The options also compose, so you can pass both STRONG and NESTMATE for a long-lived cached proxy with private host access.

The production concern in banking is metaspace. Using STRONG carelessly with per-request generated classes creates a slow metaspace leak that eventually crashes the service. The weak default prevents this by letting each class unload as soon as it is unreachable, which is essential for microservices that generate thousands of dynamic rule classes.

Remember one sentence: STRONG keeps a hidden class alive with its loader, the default lets it unload when unreachable, and NESTMATE gives the generated class shared private access to its host.

---

Share
Use ← → arrow keys to navigate