If the same class file is defined by two different class loaders inside one JVM, what happens to that class's static fields, its Class object, and any singleton instance it holds?
answer
- statics allocated in preparation, per defined type
- two Class objects → two singletons, two <clinit> runs
- static synchronized locks two different monitors
- registry writes and reads hit different maps
- loader discarded → its statics reclaimed
basics
~20 sEverything per-type is duplicated: two Class objects, two independent sets of static fields, two runs of the static initializer, and therefore two singletons. Neither side can see or affect the other's state, and objects from one are not instances of the other.
solid answer
~60 sTwo definitions mean two run-time types, and static state belongs to a type. So you get: - **Two `Class` objects** — distinct references, so `==` is false even though `getName()` matches. - **Two sets of static fields**, each with its own values, and **two executions of `<clinit>`**: each definition is initialized independently the first time *it* is actively used. - **Two singletons.** A `private static final INSTANCE` exists once per definition. "Singleton" means one per type, and there are now two types. - Any static registry, cache, counter, connection pool or `static` logger configuration is likewise doubled. Practical consequences: a plugin registering itself in a static registry may register into a copy the host never reads; a static counter under-reports; a library that caches configuration in statics can serve two different configurations in one process; a `static` lock object no longer provides mutual exclusion between the two sides, because they lock different monitors. When this is intended, it is exactly what container isolation is for. When it is accidental — a duplicated jar — the symptom is state that mysteriously "resets" or splits.
code
java · 9 lines// com.acme.Counter { static int n; static void inc() { n++; } static int get() { return n; } }
Class<?> c1 = loaderOne.loadClass("com.acme.Counter");
Class<?> c2 = loaderTwo.loadClass("com.acme.Counter");
c1.getMethod("inc").invoke(null);
c1.getMethod("inc").invoke(null);
c1.getMethod("get").invoke(null); // 2
c2.getMethod("get").invoke(null); // 0 — a separate static fieldgo deeper
Know that statics belong to a loaded type, so two loaders defining the same class give two independent copies of the static fields and two singletons.
Add the mechanics — statics allocated during preparation, one <clinit> per definition — and give the practical symptoms: split registries, doubled configuration, doubled initialization side effects.
Raise the correctness hazard of static locks across copies, and connect duplication to container isolation and to reclaiming state by discarding a loader.
Decide the sharing policy: which types are host-owned and single-defined because they must be globally unique, and which are deliberately duplicated so components stay independently versionable and unloadable.
## Static state is per run-time type, not per name The JVM allocates space for a class's static fields during the **preparation** step of linking, and initializes them by running the class initializer `<clinit>` on first active use. Both of those happen **per defined type**. Since a run-time type is *(binary name, defining loader)*, defining the same bytes with two loaders produces two independent copies of everything static. So, concretely, for `com.acme.Registry` defined by loaders L1 and L2: | thing | duplicated? | |---|---| | `Class` object | yes — two objects, `==` false | | static fields | yes — two sets, independent values | | `<clinit>` execution | yes — once per definition, at that definition's first active use | | instance fields | irrelevant — instances belong to one type or the other | | enum constants | yes — an enum defined twice has two sets of constants; `==` and `EnumMap`/`switch` behavior do not cross | | interned string constants | no — the string pool is JVM-wide, not per loader | | static synchronized lock | yes — two distinct monitors, so no mutual exclusion between the two sides | ## Why this bites in practice **Singletons stop being single.** The classic `private static final Foo INSTANCE = new Foo();` guarantees one instance *per type*. With two types you have two instances. Any invariant that depended on "exactly one" — a single scheduler, a single connection pool, a single metrics registry — quietly doubles. Resource-bound singletons (thread pools, native handles, file locks) double their footprint too. **Static registries lose entries.** A common plugin pattern is a static `Map` in a shared class that components register into. If the shared class is duplicated, plugins loaded by L2 register into L2's map while the host reads L1's map, and the host concludes nothing registered. This looks like a load-order bug and is not. **Static configuration diverges.** Libraries that hold configuration or a bootstrap flag in statics can end up configured on one copy and unconfigured on the other. The observable symptom is "we configured it at startup, but some code paths behave as if we did not". **Static locks stop excluding.** `static synchronized` methods lock the `Class` object. Two `Class` objects, two monitors: code in the two loaders can run the critical section concurrently. This is a genuine correctness hazard, not just duplicated state. **Initialization order becomes per-copy.** Each definition initializes lazily on its own first active use, so side effects in static initializers (registering shutdown hooks, starting threads, reading a file, incrementing a counter) happen twice, at two different times. ## When the duplication is the point All of this is exactly what a container wants when isolating deployments. Two applications in one JVM should each get their own copy of a library's static caches and configuration; that is what makes them independent, and what makes it safe for one to be redeployed while the other runs. The isolation and the duplication are the same property viewed from two sides. That also explains the lifecycle rule: a type's statics live as long as its defining loader is reachable. Discarding a loader discards its classes and their static state — which is how redeployment reclaims memory, and why a lingering reference into a discarded loader's classes keeps a whole application's worth of state alive. ## Deciding what should be shared The design question is which types must be **single-defined**. Anything that must have process-global semantics has to be defined once, by a loader that is an ancestor of every user: - types exchanged across the boundary (otherwise casts fail); - registries and coordination points that must be global; - native-library-owning classes — `System.loadLibrary` binds a native library to one defining loader, and a second definition attempting to load the same library fails. Everything else is safer duplicated: it is what makes components independently versionable and unloadable. ## How to check quickly Print the loader and `Class` identity hash where the state is written and where it is read. If they differ, you are looking at two copies. `-Xlog:class+load=info` (or `-verbose:class`) will show both definitions and the artifact each came from, which normally identifies a jar packaged in two places.
- Does the same duplication apply to string literals and enum constants?Enum constants are static fields of the enum type, so a doubly defined enum has two independent sets of constants that are never == to each other and cannot be used interchangeably in switches or EnumMaps. String literals are different: the intern pool is JVM-wide, so equal literals from either loader are the same String instance.
- Why is `static synchronized` particularly dangerous under duplication?A static synchronized method locks the monitor of its own Class object. With two definitions there are two Class objects and therefore two monitors, so threads running through the two copies do not exclude each other. Code that relied on that lock for a process-wide invariant becomes racy without any code change.
It is like two branch offices running the same policy manual: each keeps its own ledger. The rules look identical, but a transaction recorded in one ledger is invisible to the other.
saying these in an interview costs you the question
- Assuming static means process-global regardless of class loaders
- Expecting a singleton to be unique when its class is defined twice
- Thinking the static initializer runs once per class name rather than once per defined type
- Believing a static lock still serializes access across loader boundaries
- Treating duplicated static state as always a bug — for containers it is the intended isolation