skip to content

A team is puzzled that the same compiled application behaves identically everywhere but performs very differently across JVM builds and flags. How would you explain what the Java Virtual Machine specification actually mandates versus what each implementation is free to decide?

level: principalimportance: nice to knowfreq 26%

answer

  1. Mandated: format, instruction semantics, verification, init order, exceptions, memory model
  2. Free: object layout, GC algorithm, JIT policy, stack representation, interning
  3. Portability of meaning, not of speed
  4. Default collector has changed across releases
  5. Concurrency is spec'd precisely because freedom would break it

basics

~20 s

The specification fixes the observable contract: class file format, instruction semantics, verification, class-initialization rules, exception behaviour and the memory model. It leaves the mechanism open — object layout, garbage collection algorithm, whether and how code is compiled, stack representation, internal caching. Portability is of semantics, not of performance.

solid answer

~50 s

Split it into mandated behaviour and free mechanism. **Mandated:** the class file format and its versioning, the meaning of every bytecode instruction, verification rules, the loading/linking/initialization ordering and its once-only guarantee, exception and error semantics, and the Java Memory Model's happens-before rules. Two conforming JVMs must agree on all observable program behaviour, which is what makes a compiled artifact portable. **Free:** how objects are laid out in memory and whether headers are compressed, which garbage collector runs and how it moves objects, whether a method is interpreted forever or compiled and how aggressively it is inlined, how stacks are represented, string interning capacity, thread scheduling handed to the OS. The practical rule I give teams: never encode an assumption about the free half into the product. Rely on documented semantics, and treat performance characteristics as environment configuration to be measured on the target JVM, version and flags — including which collector is the default, since that has changed across releases.

go deeper

for a junior

Say that the specification fixes what a program means while leaving how it is executed and collected to the implementation, so behaviour is portable but speed is not.

for a middle

Give concrete items on each side of the line — instruction semantics and class initialization versus object layout and collector choice — and note the memory model as mandated.

for a senior

Turn it into practice: pin JVM version and flags as environment configuration, benchmark on target, and treat post-upgrade performance change as expected rather than anomalous.

for a principal

Argue the platform-design rationale — the free half is what let collectors and compilers evolve without recompiling applications — and derive team policy on upgrades, flag management and what code is allowed to assume.

## Two different promises The Java platform makes one strong promise and one weak one, and conflating them is the root of the confusion in this question. The strong promise is **semantic portability**: a class file that runs on one conforming JVM produces the same observable behaviour on another. The weak promise — really, no promise at all — is about **performance and resource behaviour**. Timing, memory footprint, pause profile and throughput are explicitly implementation territory. ## What the specification actually mandates **The class file format.** Structure, constant-pool entry kinds, attribute semantics, and major/minor version rules. This is the interchange contract, which is why an artifact built once runs on any conforming JVM of a sufficient version. **Instruction semantics.** Every bytecode's effect on the operand stack, locals and heap is defined. Integer arithmetic overflow wraps in a specified way; floating point follows specified rules; array bounds are checked and produce a specific exception type. **Verification.** The rules a class file must satisfy before it may execute — type-safety of the operand stack and locals, valid branch targets, access rules. **Loading, linking, initialization.** The ordering, the triggers for initialization, the once-only and thread-safe execution of the class initializer, the erroneous-class state after a failed initializer. Whether resolution is eager or lazy is deliberately left open — but the *observable* consequences of either choice are constrained. **Exception and error behaviour.** Which conditions raise which throwable, and the fact that certain errors (out-of-memory, stack overflow) may occur at essentially any point. **The memory model.** The happens-before ordering rules that define when a write by one thread is visible to another, the special semantics of volatile and final fields, and the rules for 64-bit values. This is written as a specification precisely so that concurrent programs have portable meaning across wildly different hardware. ## What implementations are free to choose **Object layout.** Header size and contents, field ordering and padding, whether references are compressed on 64-bit heaps. The specification says an object has these fields with these semantics; it never says where the bytes sit. **Garbage collection.** Whether collection is generational, whether it moves objects, whether it is concurrent, how it is partitioned, what the pause profile looks like — all free. The specification requires only that unreachable objects may be reclaimed and that finalization-like mechanisms follow their documented rules. Even *which collector is the default* has changed across releases, which alone can make the same artifact behave differently on two JDK versions. **Execution strategy.** Whether bytecode is interpreted, compiled, or both; when compilation triggers; how aggressively methods are inlined; whether speculative optimizations are attempted and rolled back. A conforming JVM could interpret everything forever and still be correct — just slow. **Runtime data area representation.** How stacks are laid out, their default and maximum sizes, how the metadata area is organised and whether it is bounded, whether there is a code cache and how large. **Peripheral policies.** Interned-string capacity, thread scheduling (typically delegated to the OS), whether and how internal caches are sized. ## The engineering rules that follow 1. **Depend on documented semantics, never on observed mechanism.** Code that relies on identity comparison working for boxed values in a range, on finalization order, on a particular `hashCode` value, on when a static is initialized relative to another, or on a data race "working" is depending on the free half. It will pass tests and fail somewhere else. 2. **Treat performance characteristics as environment configuration.** JVM vendor, version, flags and container limits belong in the same tier as the database version: pinned, recorded, and changed deliberately with measurement. "It got slower after the platform upgrade" is a legitimate and expected class of change, not a bug in your code. 3. **Measure on the target, not on a laptop.** Because the engine may compile differently under different flags, heap sizes and core counts, a benchmark on an unrepresentative JVM configuration measures a machine you do not deploy. 4. **The memory model is the exception that proves the rule.** Concurrency is the one place where an implementation's freedom could have destroyed portability, so the platform spends a whole specification chapter constraining it. If your concurrent code is correct only against the ordering one CPU architecture happens to provide, it is broken by definition even where it currently runs. 5. **Some observable behaviour is intentionally unspecified.** Timing of collection, whether a reference is cleared before another, exactly when an out-of-memory error fires — these are legitimately non-deterministic, and designs must not depend on their ordering. ## The framing to leave with the team The specification is a contract about *what a program means*. Everything about *how fast and in how much memory* is an implementation's competitive space. That separation is the reason the ecosystem could evolve collectors and compilers radically without recompiling a line of application code — and it is the reason performance must be re-measured whenever any part of that free half changes underneath you.

  • Give an example of code that depends on implementation freedom rather than specified semantics, and how it eventually breaks.
    Relying on a data race to be visible because it happens to work on a strongly ordered CPU is the classic one: the memory model gives no happens-before edge, so a different architecture or a JIT-compiled version of the same method can reorder or hoist the read and loop forever. Others include depending on finalization order, on identity comparison of boxed values, or on the specific value returned by a default hashCode. All pass tests on one configuration and fail on another.
  • If garbage collection strategy is implementation-defined, what can a portable program actually rely on?
    It can rely on reachability semantics: an object reachable from a root is not reclaimed, and one that is unreachable may be. It can rely on the documented behaviour of the reference and cleanup APIs. It cannot rely on when collection happens, whether objects move, how long a pause lasts, or that requesting a collection does anything at all.
  • How should this distinction change how a team handles a JDK upgrade?
    Semantic behaviour should be validated by the existing test suite, since conforming implementations must agree there. Performance and footprint must be re-measured, because the default collector, JIT heuristics and internal sizing may all have changed. In practice the upgrade plan should pin flags explicitly rather than inherit defaults, and compare latency, throughput and native footprint on representative load before and after.

It is like a building code versus a construction method. The code fixes what the finished building must do — loads it carries, exits it provides — while leaving the builder free to choose materials and sequencing. Two compliant buildings behave the same for occupants and cost wildly different amounts to build and heat.

saying these in an interview costs you the question

  • Believing the specification mandates a particular garbage collector, generational layout, or that JIT compilation is required at all.
  • Assuming performance behaviour is portable because bytecode is portable.
  • Treating a data race that works on one machine as correct code rather than as a memory-model violation.
  • Assuming the default collector and heap ergonomics are stable across JDK releases.
  • Claiming an object's memory layout or header size is specified and can be relied upon.

context