skip to content

How does DevTools' automatic restart work internally, and why is it faster than restarting the JVM?

level: seniorimportance: should knowfreq 40%

answer

  1. base classloader = jars, restart classloader = your dirs
  2. discard restart CL, rebuild context, reuse base
  3. restart ≠ reload (JRebel keeps state)
  4. quiet period + file watcher thread
  5. cross-restart ClassCastException

basics

~20 s

DevTools uses two classloaders: a base classloader for unchanging library jars and a restart classloader for your own code. On a file change it discards and rebuilds only the restart classloader and the application context, so libraries stay loaded and startup is much faster than a cold JVM start.

solid answer

~50 s

DevTools watches the classpath for changed files. Instead of restarting the whole JVM, it splits classes across two classloaders. The **base classloader** holds third-party jars that rarely change; the **restart classloader** holds the directories on your classpath — your compiled classes and resources. When a change is detected, DevTools closes the current Spring `ApplicationContext`, discards the restart classloader, spins up a fresh restart classloader, and reruns the app's `main`/context refresh. Because the base classloader (with all the heavy library bytecode already verified and loaded) is reused, only your relatively small amount of code is reloaded — that's why it feels near-instant compared to a full JVM boot. It's a genuine context restart (in-memory state is lost), not bytecode hot-swapping. The restart is fired by your IDE/build writing new `.class` files; a quiet period batches rapid changes.

code

properties · 10 lines
properties
# application-dev.properties
# Only restart when this file is touched (manual control)
spring.devtools.restart.trigger-file=.reloadtrigger

# Don't restart when files under these change (append to defaults)
spring.devtools.restart.additional-exclude=static/**,public/**

# Tune the file-watch batching
spring.devtools.restart.poll-interval=1000ms
spring.devtools.restart.quiet-period=400ms

go deeper

for a junior

Know it restarts the app automatically on changes.

for a middle

Name the two classloaders and that libraries stay loaded.

for a senior

Explain the restart sequence, quiet period, and restart-vs-reload distinction.

for a principal

Reason about cross-classloader ClassCastException, spring-devtools.properties, and library caching pitfalls.

## The two-classloader model A normal application uses a single classloader hierarchy. DevTools inserts a split so it can cheaply throw away and recreate *only your* classes: - **Base classloader (`RestartClassLoader`'s parent)** — loads everything that comes from **jar files** on the classpath: Spring, your libraries, the JDK. These are assumed not to change during a dev session, so they're loaded **once** and kept. - **Restart classloader (`RestartClassLoader`)** — loads everything that comes from **directories** on the classpath: your `build/classes`, `build/resources` output. This is the code you actively edit. How does DevTools decide which goes where? It reads the URLs on the running classpath: entries that are folders → restart classloader; entries that are archives → base classloader. (This is why in a fully packaged jar there are no exploded directories, so restart naturally has nothing to reload and DevTools disables itself.) ## The restart sequence 1. A **file watcher thread** (`ClassPathFileSystemWatcher`) polls the classpath directories. 2. When it sees changes and a short **quiet period** passes with no further changes (`spring.devtools.restart.poll-interval`, `spring.devtools.restart.quiet-period`), it fires a `ClassPathChangedEvent`. 3. The `Restarter` **closes the current `ApplicationContext`**, drops the restart classloader, creates a **new** restart classloader, and re-invokes the application's bootstrap on a dedicated restart thread. 4. A brand-new context is built with your recompiled classes. Because the JVM stays up and all library bytecode is already loaded and verified in the base classloader, only your classes are reloaded — typically shaving the restart from seconds to a fraction of that. ## Restart vs Reload - **Restart (DevTools)**: rebuilds the whole application context with a new classloader. **All in-memory state is lost.** Handles structural changes fine (new beans, changed method signatures, new classes). - **Reload (JRebel and similar agents)**: swaps changed bytecode into the *same* classloader, preserving state, using a JVM agent. Different tool, different tradeoffs. DevTools deliberately chose the simpler restart approach. - The plain **JVM HotSwap** (debugger) can only redefine method bodies, not add methods/fields — much more limited than DevTools restart. ## What triggers a restart vs not - Changes to **compiled classes / most resources** → restart. - Changes to paths in the **exclude list** → no restart. Defaults: `META-INF/maven`, `META-INF/resources`, `resources`, `static`, `public`, `templates`. These are static assets: editing them fires a **LiveReload** browser refresh instead of a costly context restart. Configure via `spring.devtools.restart.exclude` (replace) or `additional-exclude` (append). - With `spring.devtools.restart.trigger-file` set, DevTools restarts **only** when that specific file changes — useful to avoid restarting on every partial save; you touch the trigger file when you're ready. ## Edge cases & gotchas - **Cross-restart ClassCastException**: objects created by an old restart classloader are a *different* type from the same class loaded by the new one. Caching such objects (e.g. in a static field held by the base classloader) causes `ClassCastException`. Keep long-lived caches in the base classloader or avoid them. - **Deserialization / sessions**: serialized objects across restarts can break for the same reason. - **Libraries that need the base classloader**: some frameworks cache class references and misbehave under restart; DevTools lets you push packages to the base classloader via `META-INF/spring-devtools.properties` (`restart.exclude.*` / `restart.include.*`). - **State is not preserved**: every restart is a clean context — don't rely on in-memory data surviving. - **No recompilation by DevTools itself**: DevTools only watches for *already compiled* output. Your IDE must build the classes (IntelliJ: Build Project, or enable auto-build/build-on-save) for a restart to fire.

  • Why might you see a ClassCastException only after a DevTools restart?
    The same class loaded by the old restart classloader and the new one are distinct types. If something (often a static field on a base-classloader class) caches an instance across the restart, casting it to the freshly-loaded class fails.
  • How would you push a library to the base classloader so it survives restarts?
    Add a META-INF/spring-devtools.properties with restart.include/restart.exclude entries (regex on the jar/path) to move those classes into the base classloader instead of the restart classloader.

saying these in an interview costs you the question

  • Saying DevTools hot-swaps bytecode into the running classes without a context restart.
  • Claiming in-memory state survives a DevTools restart.
  • Thinking DevTools recompiles your source itself (the IDE/build does that).

context