skip to content

Why does the JVM have each class loader consult its parent before searching its own sources, rather than letting each one search locally first? Concretely, what would go wrong if a loader searched locally first — for instance if an application JAR contained a class named java.lang.String?

level: middleimportance: must knowfreq 55%

answer

  1. Type identity = name + defining loader
  2. Same name, two loaders = ClassCastException naming the same class twice
  3. Highest loader that has it wins → one shared definition
  4. Can't spoof java.lang.String
  5. defineClass: SecurityException, prohibited package name java.*

basics

~20 s

Two reasons: consistency and safety. Delegating upward guarantees every core type is defined exactly once, by the highest loader that has it, so all code agrees on the same type. It also stops application code substituting its own version of a trusted core class. The JVM additionally refuses outright to define classes in java.* from a non-bootstrap loader.

solid answer

~60 s

**Consistency.** A type's runtime identity is the pair (fully-qualified name, defining loader). If loaders searched locally first, two loaders could each define their own `java.lang.String`, and those would be two distinct, incompatible types with the same name — passing one where the other is expected yields a `ClassCastException` complaining that `String` cannot be cast to `String`. Delegating upward means the highest loader that can supply a type always wins, so all components share one definition of the core library. **Safety.** Without delegation, dropping a `java/lang/String.class` into an application JAR would let attacker-supplied code replace a class the entire platform trusts. Delegation makes that impossible: the bootstrap loader answers first and its answer stands. **A second line of defence.** Even if a loader breaks delegation deliberately by overriding `loadClass`, `ClassLoader.defineClass` rejects any name in the `java.*` package from a non-bootstrap loader, throwing a `SecurityException` for a prohibited package name. Package sealing and, in modern JDKs, module encapsulation add further protection. So delegation is not merely an organisational convenience — it is what makes "the same type" mean the same thing process-wide.

code

text · 6 lines
text
java.lang.ClassCastException: class com.acme.Order cannot be cast to class com.acme.Order
  (com.acme.Order is in unnamed module of loader 'pluginA' @1b6d3586;
   com.acme.Order is in unnamed module of loader 'pluginB' @2f92e0f4)

java.lang.SecurityException: Prohibited package name: java.lang
	at java.base/java.lang.ClassLoader.preDefineClass(ClassLoader.java:...)

go deeper

for a junior

Say that delegation makes core classes load once from the trusted runtime and stops user code from replacing classes like java.lang.String.

for a middle

Add the identity rule — a type is its name plus its defining loader — and explain the same-name-different-loader ClassCastException that local-first search would cause.

for a senior

Mention the layered defence: delegation as policy, defineClass's prohibited-package check as enforcement, plus sealing and module encapsulation; and describe how you would diagnose a duplicate-definition failure.

for a principal

Discuss it as the invariant that makes shared APIs possible across component boundaries, and the deliberate cost a container accepts when it inverts the order for isolation.

## Runtime type identity is not just the name The fact that surprises people first: in the JVM, a type is identified by **its fully-qualified name plus the loader that defined it** — the *runtime package* / defining-loader pair. Two `Class` objects with identical names loaded by different loaders are two different types. The JVM will not silently unify them; assigning one to a variable of the other produces a `ClassCastException` whose message names the same class twice, which is one of the most confusing errors a Java developer can meet. Everything about delegation follows from this. If types are identified by (name, loader), then getting everyone to agree on shared types requires getting everyone to use the *same loader* for them. Parent-first delegation is exactly the rule that achieves it: because every loader asks upward before searching, whichever loader is highest in the chain and can supply a type always defines it, and every descendant sees that one definition. ## What consistency buys you concretely Imagine a process with a container loader and two plugin loaders beneath it. Both plugins use `java.util.List`. With parent-first delegation, all three loaders resolve it to the bootstrap loader's definition, so a plugin can hand a `List` to the container or to the other plugin and it just works. Without it, each plugin could define its own `java.util.List` from its own bundled copy, and passing a list across a boundary would fail at runtime with a nonsensical cast error. The whole idea of a shared API between components depends on the shared types being *literally* shared. This also matters for correctness of things the VM itself does. Type checks in the verifier, `instanceof`, array covariance, exception catching by type — all compare loader-qualified types. `catch (IOException e)` catches only the `IOException` your code was linked against; a differently-loaded `IOException` sails straight past. ## What safety buys you concretely Now the security half. Suppose delegation were child-first and a JAR on the application class path contained `java/lang/String.class` with an added method that copied every constructed string to a network socket. The application loader would find and define it locally, and from that moment every string in that loader's world would be attacker-controlled — including strings used for permission checks, file paths, and SQL. The same trick against `java.lang.SecurityManager`, `java.lang.ClassLoader` itself, or `java.security.*` would let untrusted code define the very machinery meant to constrain it. Parent-first delegation removes the possibility for the built-in loaders: the bootstrap loader is asked first, it has `java.lang.String`, and its answer is final. Your file is never even opened. ## The independent second check Delegation alone would be a fragile defence, because delegation is *policy implemented in Java code* — anyone can subclass `ClassLoader` and override `loadClass` to search locally first. So the platform does not rely on it alone: - **Prohibited package names.** `ClassLoader.defineClass` refuses to define any class whose name begins with `java.` unless the caller is the bootstrap loader, throwing `SecurityException: Prohibited package name: java.lang`. This is enforced in the platform, not by convention, so breaking delegation still cannot get you a custom `java.lang.String`. - **Package sealing.** A JAR can declare a package sealed, meaning all classes in that package must come from that same JAR — preventing an attacker from injecting a new class *into* a trusted package to gain package-private access. - **Module encapsulation.** In modern JDKs the module system adds strong encapsulation: packages are exported explicitly, split packages across modules are rejected, and the runtime image's contents cannot be shadowed by a class path entry. A strong answer names delegation as the primary mechanism *and* knows that the platform has a hard check behind it. ## The nuance that separates a good answer Delegation protects the *core platform* absolutely, but it does not protect *your* classes from being shadowed by a loader that inverts the order. A container that deliberately runs child-first for application packages is making a considered trade — isolation of library versions in exchange for the risk of duplicate definitions. That is a legitimate design, and the protection that survives it is the `java.*` defineClass check, not delegation. ## How to answer in an interview "Two reasons. Consistency: type identity is name plus defining loader, so delegating upward makes sure a shared type has exactly one definition and components can pass instances across boundaries. Security: user code cannot substitute a core class, because the bootstrap loader answers first. And even if someone overrides `loadClass` to break delegation, `defineClass` still refuses any `java.*` name from a non-bootstrap loader."

  • If delegation is just Java code in ClassLoader.loadClass, what actually stops a malicious loader from defining its own java.lang.String?
    ClassLoader.defineClass performs a check before defining: any class name in the java.* package coming from a loader other than the bootstrap loader is rejected with a SecurityException for a prohibited package name. So overriding loadClass to search locally first lets you shadow application packages, but it cannot get you a substitute core class. Package sealing and module encapsulation cover the related attack of injecting a new class into an existing trusted package.
  • What error do you actually see when two loaders each define a class with the same name, and how do you diagnose it?
    A ClassCastException whose message names what appears to be the same class on both sides; modern JVM messages helpfully append each type's module and defining loader, which makes the cause obvious. To diagnose, print getClass().getClassLoader() on both objects, or use the JDK's class-loading trace to see which loader defined each one. The fix is to move the shared type into a common parent loader so there is exactly one definition.

saying these in an interview costs you the question

  • Saying delegation exists 'to avoid loading the same class twice for performance' and missing type identity and security entirely
  • Believing two classes with the same fully-qualified name are automatically the same type regardless of loader
  • Claiming you can replace java.lang.String simply by putting it earlier on the class path
  • Thinking delegation alone is the only protection, unaware of the defineClass prohibited-package check
  • Confusing shadowing an application class (possible with an inverted loader) with shadowing a core JDK class (blocked by the platform)

context