A framework generates a proxy class as a byte[] at runtime. What are the ways to turn those bytes into a usable runtime type on a modern JVM, and how do the options differ in visibility, access and lifetime?
answer
- defineClass is protected — pick a namespace, not an arbitrary loader
- own loader = named, bulk-disposable, public access only
- Lookup#defineClass (9+) = same loader + package as target
- defineHiddenClass (15+) = unnamed, nestmate, host-scoped lifetime
- Unsafe.defineAnonymousClass removed in 17
basics
~20 sThree routes: define through a custom ClassLoader's protected defineClass; call MethodHandles.Lookup#defineClass (Java 9+) to define into the lookup class's own loader and package with its access; or Lookup#defineHiddenClass (Java 15+) for a class with no name in the loader's table, private access to its host, and lifetime tied to it.
solid answer
~60 s**Custom loader + `defineClass`.** The classic route: the generated class lands in a namespace you control, is findable by name, and can be discarded wholesale by dropping the loader. Cost is a loader per generation strategy and the usual visibility work — the generated class must be able to see the interfaces it implements. **`MethodHandles.Lookup#defineClass(byte[])` (Java 9+).** Defines the class into the *lookup class's* loader and runtime package, so the proxy is a package-mate of its target and needs no extra loader. It requires PACKAGE access in the lookup, and the class is a normal named class — visible, unloadable only with that loader. **`Lookup#defineHiddenClass` (Java 15+).** Produces a class that is not registered in the loader's name table: it cannot be found via `Class.forName`, may be granted `NESTMATE` access to the lookup class's private members, and with the `STRONG` option is unloadable independently, otherwise its lifetime follows its host class. This is what lambdas and modern proxy generators use. `sun.misc.Unsafe#defineAnonymousClass` was the old hack for this and was removed in JDK 17.
code
java · 15 lines// 1. own loader — named class, dies with the loader
class GenLoader extends ClassLoader {
GenLoader(ClassLoader parent) { super(parent); }
Class<?> define(String name, byte[] b) { return defineClass(name, b, 0, b.length); }
}
// 2. into the target's loader + package (Java 9+); needs PACKAGE access
MethodHandles.Lookup lookup =
MethodHandles.privateLookupIn(Target.class, MethodHandles.lookup());
Class<?> proxy = lookup.defineClass(bytes);
// 3. hidden class (Java 15+): no name in the loader, private access to Target
MethodHandles.Lookup hidden = lookup.defineHiddenClass(
bytes, true, MethodHandles.Lookup.ClassOption.NESTMATE);
Class<?> hiddenClass = hidden.lookupClass(); // unreachable via Class.forNamego deeper
Recall that generated bytes become a class only via defineClass, and that a custom class loader is the traditional way to call it.
Contrast the custom-loader route with Lookup#defineClass: which loader and package the class lands in, and what access that grants.
Add hidden classes — no name in the loader, nestmate access, host-scoped lifetime — and choose per use case, noting Unsafe.defineAnonymousClass is gone since JDK 17.
Reason about generated-code lifecycle as a resource: how many classes, what owns their lifetime, how Metaspace is bounded, and what tooling and debuggability you give up by going hidden.
## The problem Proxy generators, ORMs, mocking libraries, serializers and expression compilers all produce a classfile in memory and then need the JVM to turn it into a live type. Because `defineClass` is `protected` on `ClassLoader`, you cannot simply call it on an arbitrary loader — the API is deliberately shaped so that only a loader's own code can inject types into that loader's namespace. The generator therefore has to choose *which namespace* the class lives in, and that choice determines visibility, access and lifetime. ## Route 1: your own ClassLoader Subclass `ClassLoader`, keep the generated bytes in a map, and define them in `findClass` (or define eagerly and hand back the `Class`). - **Visibility.** The generated class can see whatever this loader can see: its parent's classes plus anything you define. Getting this wrong is the classic failure — a proxy implementing `com.acme.Service` must have a loader that can reach `com.acme.Service`, so the natural parent is the target's own loader. - **Access.** The generated class is in a *different* runtime package from the target unless it shares the target's loader, so it can only touch public members. This is why older proxy libraries required public types and public no-arg constructors. - **Lifetime.** All classes defined by the loader die together when the loader becomes unreachable. That is excellent for bulk disposal (a whole plugin, a whole test's mocks) and wasteful for one-off classes: a loader per generated class is a real Metaspace cost. - **Naming.** The class has a real binary name, is findable by `Class.forName` in that loader, and appears in stack traces and heap dumps — good for debugging, and a hazard if you accidentally generate colliding names. ## Route 2: MethodHandles.Lookup#defineClass (Java 9+) `lookup.defineClass(bytes)` defines the class into the **lookup class's own loader and runtime package**. The caller must hold a `Lookup` with PACKAGE access — typically obtained inside the target package, or via `MethodHandles.privateLookupIn(Target.class, MethodHandles.lookup())` when the target's module opens the package to yours. - No new loader is created, so no extra Metaspace loader overhead and no visibility puzzle: the generated class sees exactly what the target sees. - Being a package-mate, it can access package-private members of the target — a real capability jump over route 1. - It is a fully normal named class: registered in the loader, resolvable by name, unloadable only when that whole loader goes. - The bytes must declare a name in the lookup class's package, or the definition is rejected. ## Route 3: Lookup#defineHiddenClass (Java 15+) `lookup.defineHiddenClass(bytes, initialize, options...)` creates a **hidden class**: - **Not discoverable by name.** It is not entered into the loader's class-name table, so `Class.forName` cannot find it and no other class can link to it symbolically. Its `getName()` returns a synthetic name containing a `/`, which is not a valid binary name on purpose. - **Optional nestmate access.** With `ClassOption.NESTMATE` it joins the lookup class's nest and can read and write its private members — the capability lambdas rely on. - **Lifetime control.** By default a hidden class's lifetime is tied to its defining loader *and* its lookup (host) class, so it can be collected when the host goes even though the loader lives on. With `ClassOption.STRONG` it is strongly tied to the loader instead. - Hidden classes cannot be used as a field type or a method parameter type in other classes' descriptors — you reach them through `MethodHandle`s or an interface they implement. This is now the preferred route for one-off generated code: `LambdaMetafactory` spins lambda implementation classes this way, and modern proxy/serialization frameworks have migrated to it. ## The removed route `sun.misc.Unsafe#defineAnonymousClass` provided VM-anonymous classes with host-class access and independent unloading, and half the ecosystem depended on it. It was deprecated in JDK 15 when hidden classes arrived and **removed in JDK 17**; code still calling it fails on modern runtimes and must be ported to `defineHiddenClass`. ## Choosing - Generating **many** classes with a shared lifetime (a plugin, a deployment, a test class's mocks) → a dedicated loader you can drop as a unit. - Generating a class that must access a specific target's package-private or private members → `Lookup#defineClass` or a nestmate hidden class. - Generating **short-lived, one-off** implementation classes → hidden classes, for cheap unloading and no namespace pollution. - Needing the class to be nameable by other code, serializable by name, or visible in tooling → a named definition, not a hidden class. Across all routes the same discipline applies: generate valid bytecode with correct stack map frames (the verifier is strict since classfile version 50), keep the declared name consistent with what you pass in, and remember that every generated class is permanent Metaspace until its owning loader or host becomes unreachable — unbounded generation keyed on request data is a classic slow Metaspace exhaustion.
- What does "hidden" actually mean for a hidden class — what can't you do with it?It is never entered in the class loader's name table, so Class.forName cannot find it and no other class can reference it symbolically in a descriptor: it cannot appear as a field type, parameter type or supertype elsewhere. You use it through a MethodHandle or through an interface it implements.
- Why would you prefer a hidden class over spinning up a class loader per generated class?A loader is a relatively heavy Metaspace object and creating one per generated class multiplies that cost. A hidden class needs no new loader, is collectable when its host class or loader becomes unreachable, adds no discoverable name that could collide, and can be granted nestmate access to its host's private members.
- Why can a proxy defined through a custom loader usually only touch public members of its target?Access control uses the runtime package — package name plus defining loader. A proxy defined by a different loader is in a different runtime package even if the package name matches, so package-private and protected members are off limits. Defining it through the target's own Lookup puts it in the same runtime package and lifts that restriction.
saying these in an interview costs you the question
- Assuming you can call defineClass on any class loader, when it is protected precisely to prevent that
- Believing a generated class is garbage-collected as soon as no instances remain, rather than living until its loader or host class is unreachable
- Still recommending sun.misc.Unsafe#defineAnonymousClass, which was removed in JDK 17
- Thinking a hidden class can be looked up by name or used as a declared field or parameter type
- Generating an unbounded number of classes keyed on runtime data and treating Metaspace growth as a leak in someone else's code