How did the JVM's built-in class loaders change when Java 9 introduced the module system, compared with Java 8, and what kinds of existing code commonly break as a result?
answer
- Extension loader -> platform loader
- Built-in loaders are not URLClassLoader any more
- addURL hack and URLClassLoader cast both fail
- No rt.jar; runtime image instead
- Only -Xbootclasspath/a survives
basics
~20 sJava 8's extension loader (lib/ext) was replaced by the platform loader, and the built-in loaders are no longer URLClassLoader instances. Casting the system loader to URLClassLoader or reflectively calling addURL fails, and code scanning rt.jar breaks because platform classes now come from the runtime image.
solid answer
~60 sThree changes matter. **The middle loader changed role.** Java 8 had an *extension* loader reading `lib/ext` and `java.ext.dirs`; Java 9 replaced it with the **platform loader**, which defines JDK platform modules. The extension mechanism is gone, so dropping JARs into the JDK directory no longer works. **The built-in loaders are no longer `URLClassLoader`.** They are internal types built around the module system. The very common idiom `((URLClassLoader) ClassLoader.getSystemClassLoader())` now throws `ClassCastException`, and the reflective `addURL` hack for appending to the classpath at runtime fails outright, in later releases also blocked by strong encapsulation. **Platform classes come from the runtime image.** There is no `rt.jar`; classes live in a packaged image, so tools that enumerated `rt.jar` entries find nothing. Some classes that the bootstrap loader defined in Java 8 are now defined by the platform loader, so code asserting `getClassLoader() == null` for a given JDK class can be wrong. On top of this, deep reflection into JDK internals is denied by default from Java 16, so the workarounds themselves stopped compiling over.
code
java · 11 lines// Java 8: worked. Java 9+: ClassCastException
URLClassLoader ucl = (URLClassLoader) ClassLoader.getSystemClassLoader();
URL[] cp = ucl.getURLs();
// Supported alternatives
String classpath = System.getProperty("java.class.path");
// Need extra locations at runtime? build your own loader instead of mutating a built-in one
URLClassLoader plugin = new URLClassLoader(
new URL[]{ pluginJarUrl },
ClassLoader.getSystemClassLoader());go deeper
Recall the headline: the extension loader was replaced by the platform loader, and the built-in loaders can no longer be cast to URLClassLoader.
Explain why the cast and the addURL trick fail, and give the supported alternatives: read java.class.path, or build your own URLClassLoader for extra locations.
Lead a migration: enumerate the breakage patterns, replace runtime classpath mutation with a purpose-built loader, and use module APIs instead of rt.jar scanning or loader-null checks.
Frame it as encapsulation policy: the platform stopped being implicitly extensible, so plugin and tooling designs must own their loaders and use supported module options, with --add-opens treated as a temporary escape hatch that carries upgrade debt.
## The Java 8 arrangement Before modules, a JVM had three loaders: bootstrap (native, loading `rt.jar` and friends from `lib`), the **extension** loader (`sun.misc.Launcher$ExtClassLoader`, loading anything in `lib/ext` or the directories named by `java.ext.dirs`), and the **application** loader (`sun.misc.Launcher$AppClassLoader`, loading the class path). Both non-bootstrap loaders were `URLClassLoader` subclasses, which meant they exposed a list of URLs and a protected `addURL` method. That detail leaked into an enormous amount of tooling. Two idioms in particular became folklore: casting `ClassLoader.getSystemClassLoader()` to `URLClassLoader` to *read* the effective classpath, and reflectively calling `addURL` to *append* to the classpath at runtime, used by hot-reloaders, test harnesses, script engines and build tools. ## What Java 9 changed **The extension loader is gone.** It was replaced by the **platform class loader**, obtained with `ClassLoader.getPlatformClassLoader()`. Its purpose is different: rather than loading arbitrary extension JARs, it defines JDK platform modules that are not in the core set. The endorsed-standards and extension mechanisms were removed, so the old "install a JAR into the JDK to override a platform API" trick no longer exists. The upgradeable-module path replaced part of that need for a narrow set of modules. **Loader identity changed.** The built-in loaders are now internal implementation classes designed around modules. Crucially, they are **not** `URLClassLoader` instances. The consequences are direct: - `((URLClassLoader) ClassLoader.getSystemClassLoader()).getURLs()` throws `ClassCastException`. - The reflective `addURL` hack fails: there is no such method to reach, and from Java 16 the default denial of illegal reflective access blocks equivalent tricks against JDK internals anyway. - Libraries that discovered the classpath through the loader had to switch to reading the `java.class.path` system property, or to a supported mechanism such as accepting an explicit loader or building their own `URLClassLoader` for extra locations. **Where platform classes live changed.** `rt.jar` and `tools.jar` no longer exist; the runtime ships as a packaged image. Anything that enumerated `rt.jar` to list JDK classes (old scanners, coverage tools, some obfuscators) stopped working. The supported replacements are the module APIs and, for tools, the JRT filesystem view. **Loader assignment for some JDK classes changed.** With the platform split, classes previously defined by the bootstrap loader may now be defined by the platform loader. Code that tested `SomeJdkClass.class.getClassLoader() == null` as a proxy for "is a JDK class" gives wrong answers; the module-aware check is to look at the class's `Module` instead. **Boot class path options changed.** The old `-Xbootclasspath` (full replacement) and `-Xbootclasspath/p` (prepend) were removed; only `-Xbootclasspath/a` (append) remains. Overriding platform classes is intentionally hard now, with `--patch-module` as the supported and narrower alternative. ## The typical breakage list In migration work, the recurring failures are: 1. `ClassCastException` casting the system loader to `URLClassLoader`, usually deep inside a build tool, an application server, or a testing library. 2. Runtime classpath augmentation via reflective `addURL` silently unavailable, so dynamic plugin loading has to be redesigned around a purpose-built loader. 3. Class scanners that walked `rt.jar` finding zero platform classes. 4. Code relying on `lib/ext` to inject an implementation of a platform API. 5. Assumptions that any class with a `null` loader is a JDK class, or that every JDK class has a `null` loader. 6. From Java 16 onward, `setAccessible` on JDK internals throwing `InaccessibleObjectException` unless opened explicitly with `--add-opens`, which turned several of the above workarounds from "deprecated but functioning" into hard failures. ## The migration posture The healthy pattern is to stop treating built-in loaders as configurable containers. If you need extra code locations at runtime, construct **your own** loader (a `URLClassLoader` with an explicit parent is still perfectly supported) rather than mutating a built-in one. If you need to know the classpath, read the property. If you need to enumerate platform classes, use module APIs. If you need to override platform behaviour, use the supported module options rather than the deleted extension mechanism. The underlying theme is that Java 9 turned the loader hierarchy from an implicitly extensible structure into a defined, encapsulated one. The three roles remained, application over platform over bootstrap, but the middle role was redefined and the implementation stopped being something applications may reach into.
- An old library appends a JAR to the running classpath by reflectively invoking addURL on the system class loader. What is the modern replacement?Create a dedicated URLClassLoader with the extra URLs and the system loader as its parent, then load the plugin types through it. That keeps the added code in its own namespace, works without reflective access to JDK internals, and lets you discard the loader when the plugin is unloaded rather than permanently growing the application loader's search path.
- Is checking getClassLoader() == null a reliable way to tell whether a class is a JDK class?No. Since Java 9 many JDK classes are defined by the platform loader rather than the bootstrap loader, so they return a non-null loader. The module-aware check is to inspect the class's Module and see whether it belongs to a named platform module, which is stable across releases.
saying these in an interview costs you the question
- Believing the system class loader is still a URLClassLoader you can cast or mutate
- Expecting JARs dropped into the JDK's lib/ext directory to be picked up
- Scanning rt.jar to enumerate platform classes on a modern JDK
- Assuming every JDK class still has a null class loader
- Reaching for --add-opens as the first fix instead of the supported API