How can a long-running JVM pick up a new version of an application class without restarting the process, using class loaders — and what are the limits of that technique?
answer
- new version = new loader, never redefine in place
- swap one volatile reference, drain the old
- unload needs loader + classes + instances unreachable
- ThreadLocals, static registries, live threads pin the old version
- JVMTI redefine keeps identity but method bodies only
basics
~20 sCreate a fresh class loader for the new version, define the new bytes there, build a new object graph, and atomically swap the reference. The old loader, its classes and all their instances are then abandoned; they are collected only when nothing references them. Live objects are not upgraded — state must be migrated explicitly.
solid answer
~60 sYou never redefine a class in place; you replace the **namespace**. Each version gets its own loader, so the new bytes can be defined even though the old ones already exist (one loader defining the same name twice throws `LinkageError`). The host then constructs the new root object through the new loader and swaps a single volatile reference, so new work uses v2 while in-flight work finishes on v1. Reclaiming v1 is a garbage-collection question: a loader's classes are unloaded only when the loader, every class it defined, and every instance of those classes are unreachable. A `ThreadLocal`, a static cache in the parent, a registered JDBC driver, a running thread or a lingering `Class` reference keeps the whole graph alive. Limits: existing instances keep their old type forever — there is no in-place upgrade, so state must be copied through shared API types; statics reinitialize; and the shared contract types must not change incompatibly. The alternative mechanism, JVMTI `redefineClasses` (what HotSwap and agents use), keeps identity but in the standard JVM can only change method bodies, not add or remove members.
go deeper
Know the headline: you make a new class loader for the new version and swap to it; you cannot change a class already defined by a loader.
Describe the pattern end to end — contract in the parent, implementation per child loader, atomic swap — and note that statics and instances do not carry over.
Own the lifecycle: what pins the old namespace, the explicit deregistration path, dual-version windows against shared external resources, and Metaspace growth over repeated reloads.
Weigh reload against a rolling restart: which failure modes you accept, whether contract churn forces restarts anyway, and what operational guarantees the dual-version window needs.
## Why in-place redefinition is not on the table Once a loader has defined `com.acme.Service`, that name is taken in that loader forever: a second `defineClass` with the same name throws `LinkageError: attempted duplicate class definition`. There is no supported "undefine". So loader-based reload works by discarding a whole namespace and building a new one. ## The loader-per-version pattern 1. **Contract layer (parent).** Interfaces and value types shared by the host and both versions. Loaded once, never reloaded. 2. **Version loader (child).** A fresh loader instance per deployment reads that version's bytes and defines the implementation classes. 3. **Bootstrap the new graph.** Through the new loader, instantiate the root object (via `Class.forName(name, true, newLoader).getDeclaredConstructor().newInstance()` or a `ServiceLoader` bound to that loader), returning it as a contract type. 4. **Swap.** Publish it into a `volatile` field or `AtomicReference`. New requests pick it up; requests already inside v1 finish on v1. Both versions coexist for a while — that is normal and is the reason each needs its own namespace. 5. **Drain and drop.** Once nothing references v1, drop the loader reference and let GC do the rest. Wrapping plugin calls in a thread-context-class-loader swap is usually necessary too, because libraries resolve names via `Thread.currentThread().getContextClassLoader()`. ## What must happen for the old version to actually go away Class metadata lives in Metaspace and is reclaimed per loader, all at once. The JVM may unload a loader's classes only when the loader object, all `Class` objects it defined, and all instances of those classes are unreachable. Common anchors that defeat this: - a `ThreadLocal` whose value is an old-version object, held by a pooled thread that outlives the deployment; - a static registry, listener list, cache or metrics map in the **parent** layer holding old objects; - JDK-level registries: `DriverManager` drivers, `java.beans` introspection caches, security providers, shutdown hooks, `ForkJoinPool` tasks; - threads started by the old version and never stopped (a running thread's context class loader and stack pin everything); - `Class` or `Method` objects cached reflectively. A cleanly reloadable component therefore needs an explicit shutdown path: stop threads and timers, deregister listeners and drivers, clear its own caches, close resources. Reload discipline is mostly *un*-registration discipline. ## State migration Existing instances are objects of the old runtime type and stay that way forever; nothing rewrites them. So carrying state across a reload is an application concern: - keep durable state outside the reloadable namespace (database, external cache, or objects whose classes live in the parent layer); - or export/import through contract types or a serialized form the new version can read. Statics are per runtime type, so the new version starts with empty caches and re-runs its static initializers — a warmup cost worth planning for. ## Limits and failure modes - **Contract changes.** If the interface itself changes, the parent layer must change, which usually means a restart. Reload buys you implementation churn, not contract churn. - **Two versions running at once.** Any shared external resource (a database schema, a queue message format, a file layout) must tolerate v1 and v2 concurrently for the drain window. - **Memory growth.** Every version that fails to unload leaves its metadata and instances behind; repeated reloads then exhaust Metaspace or heap. - **Doubled JIT work.** New classes mean new profiles and recompilation; the first minutes after a reload are slower. ## The other mechanism: JVMTI redefinition Instrumentation agents (`Instrumentation.redefineClasses`, the basis of IDE HotSwap and commercial reload tools) change a class's bytes *while keeping its identity* — same Class object, same instances, same loader. That preserves state and needs no loader gymnastics, but standard HotSpot restricts it to method-body changes: you cannot add or remove fields or methods, change signatures, or alter the hierarchy. Tools that appear to lift those limits do so by combining redefinition with their own loader tricks. Contrast the two honestly in an interview: loader-swap can change anything but loses identity and state; redefinition keeps identity and state but can change very little. Production systems usually choose neither and do a rolling restart, because a fresh process has no unloading, state-migration or dual-version hazards at all — reload is a development-velocity or plugin-lifecycle tool, not a general deployment strategy.
- Why must the old version's instances become unreachable before anything is freed, rather than just the loader?Every object holds a reference to its class, every class holds a reference to its defining loader, and the loader holds all the classes it defined. That cycle is collected only as a whole, so one surviving instance keeps the entire namespace — classes, metadata, statics — alive.
- What does JVMTI class redefinition give you that a new loader does not, and what does it cost?Redefinition changes the bytes of an existing class while keeping its identity, so live instances and their state simply continue with new method bodies and no reference swapping is needed. The cost is scope: standard HotSpot allows only method-body changes, not added or removed fields and methods or hierarchy changes.
- How do you make a component genuinely unloadable?Give it an explicit stop path that reverses everything it registered: stop its threads and scheduled tasks, remove listeners, deregister JDBC drivers and shutdown hooks, clear ThreadLocals set on pooled threads, and drop caches held in the shared parent layer. After that, dropping the loader reference is enough.
saying these in an interview costs you the question
- Claiming you can call defineClass again on the same loader to replace a class
- Assuming existing objects are magically upgraded to the new version's behaviour
- Thinking dropping the loader reference frees memory immediately, regardless of surviving instances
- Believing HotSwap/JVMTI redefinition can add methods or fields in a standard JVM
- Treating loader-based reload as a general production deployment strategy rather than a plugin or development mechanism