skip to content

Inside one running JVM, is a fully-qualified name such as com.acme.Config enough to identify a type uniquely? Explain what actually determines a class's runtime identity.

level: middleimportance: must knowfreq 45%

answer

  1. identity = (binary name, defining loader)
  2. initiating loader ≠ defining loader
  3. two definitions → two Class objects, two sets of statics
  4. isAssignableFrom false both ways
  5. loader graph = namespace mechanism

basics

~20 s

No. A runtime type is identified by the pair (fully-qualified name, defining class loader). Load the same class file with two different loaders and you get two distinct, non-interchangeable types, each with its own Class object.

solid answer

~60 s

The JVM's unit of type identity is the **run-time type**, defined as the pair *(binary name, defining class loader)*. The name alone is not unique inside a single JVM. The *defining* loader is the loader that actually called `defineClass` for those bytes — not necessarily the one you asked. If a loader delegates to its parent and the parent defines the class, the parent is the defining loader; the child is merely an *initiating* loader. Delegation exists precisely so that shared types resolve to one defining loader and stay identical everywhere. When two different loaders each define the same bytes, the results are two separate types: - two distinct `Class` objects, so `c1 == c2` is false and `c1.equals(c2)` is false; - two separate sets of static fields, so any "singleton" exists twice; - no assignability between them: `isAssignableFrom` is false and a cast throws `ClassCastException` even though the printed names match. This is not a bug; it is the mechanism that makes loader-based isolation possible — containers, plugin systems and OSGi rely on it to let independent components carry conflicting library versions.

code

java · 7 lines
java
Class<?> a = loaderOne.loadClass("com.acme.Config");
Class<?> b = loaderTwo.loadClass("com.acme.Config");

a.getName().equals(b.getName());   // true  — names are identical
a == b;                            // false — different run-time types
a.isAssignableFrom(b);             // false — not interchangeable
a.getClassLoader();                // loaderOne (the defining loader)

go deeper

for a junior

Recall the rule: a type is (name + defining class loader), so the same class file loaded twice by different loaders yields two unrelated types.

for a middle

Distinguish initiating from defining loader, and list the consequences: separate Class objects, separate statics, no assignability.

for a senior

Explain why the design enables container and plugin isolation, and connect it to class unloading being tied to loader reachability.

for a principal

Reason about it as a namespace design decision — the loader graph is the process's type namespace, so what is shared versus duplicated is an architectural choice with versioning and lifecycle consequences.

## The definition The JVM specification says a class or interface is determined by its **binary name together with its defining class loader**. Two loaded types with the same name but different defining loaders are *different types*, with no more relationship to each other than to any unrelated class. It helps to distinguish two roles a loader can play for a given class: - **Initiating loader** — the loader you asked to load the class. `loadClass("com.acme.Config")` was called on it. - **Defining loader** — the loader that ultimately turned bytes into a class by calling `defineClass`. This is the loader recorded as part of the type's identity and returned by `Config.class.getClassLoader()`. Because loaders normally ask their parent first, the same class typically ends up **defined** once, high in the hierarchy, and *initiated* by many loaders below. Everyone then sees the same type. That single defining event is what makes types shareable at all: `java.lang.String` has exactly one definition (by the bootstrap loader) in a normal JVM, so every component agrees on what a `String` is. ## What changes when two loaders both define the class Suppose two loaders each read the same `com/acme/Config.class` bytes and each define it. Now there are two run-time types. Everything that is per-type is duplicated: 1. **The `Class` object.** `Class` instances are compared by reference; two definitions means two objects. `a.getClass() == b.getClass()` is false, and `Class.equals` is inherited identity equality, so that is false too. Comparing `getName()` returns equal strings — which is exactly why the situation is confusing. 2. **Static state.** Static fields belong to a type, so each definition has its own copy, and each is initialized by its own run of `<clinit>`. A class implementing a singleton, a registry, a cache, or a static counter now has two independent instances of that state. Frameworks that assume "static means global in this process" break here. 3. **Assignability.** `typeA.isAssignableFrom(typeB)` is false in both directions. A reference to one cannot be stored in a variable declared as the other, and the resulting failure is a `ClassCastException` naming the same class on both sides. 4. **Instances.** An object created from one definition is an instance of that type only; `instanceof` against the other definition is false. Note what is *not* duplicated: identity is per-type, so this has nothing to do with `equals`/`hashCode` you wrote — value equality between two objects of the two types is not even reachable, because you cannot pass one where the other is expected. ## Why the design is this way If name alone determined identity, a JVM could hold exactly one version of any given class, and two libraries needing incompatible versions of a third could never coexist in one process. Making the defining loader part of identity turns the loader graph into a **namespace mechanism**: each loader defines its own private namespace of types, and only what it delegates comes from outside. That is the foundation for: - application servers isolating deployed applications from each other; - plugin frameworks giving each plugin its own dependency set; - module systems such as OSGi wiring exports and imports explicitly; - test harnesses and hot-reload tooling loading a fresh definition of changed classes while the old ones are still referenced. It also has a lifecycle consequence: a class can only be unloaded together with its defining loader, when the loader, all its defined classes and all their instances are unreachable. Isolation and reloadability come from the same property. ## Making it visible The practical habit is to never trust the printed name alone. When two types "should" be the same, print identity: ```java System.out.println(obj.getClass().getName() + " @" + Integer.toHexString(System.identityHashCode(obj.getClass())) + " loader=" + obj.getClass().getClassLoader()); ``` Different loader instances (or different identity hashes for the `Class` objects) prove you have two types. `-verbose:class` shows each definition and the source it came from, which usually reveals the duplicate jar or the extra loader. ## The rule to remember A type's full name at run time is not `com.acme.Config`. It is `com.acme.Config` **as defined by** *this* loader. Any reasoning about "the same class" in a container, plugin host, or app server must carry that second half.

  • If a child loader delegates to its parent and the parent loads the class, which loader is part of the type's identity?
    The parent, because it called defineClass and is therefore the defining loader; the child is only an initiating loader. This is why delegation produces shared, identical types: many loaders initiate, one defines. getClassLoader() on the resulting class returns the parent.
  • What happens to static fields of a class loaded by two different loaders?
    Each definition has its own copy of every static field and runs its own static initializer. A singleton, cache or registry held in statics therefore exists twice, independently. Code that assumes static state is process-global will behave as if it were running in two separate applications.

Two people can both be named 'John Smith'; the passport number is the country plus the name. In the JVM the country is the defining class loader.

saying these in an interview costs you the question

  • Saying a fully-qualified name uniquely identifies a class within a JVM
  • Assuming getClassLoader() returns whichever loader you called loadClass on
  • Thinking two Class objects with equal getName() are equal
  • Believing static fields are always process-wide singletons
  • Treating duplicate definitions as a JVM bug rather than the isolation mechanism

context