Definition
There are two separate things people call "the JVM":
- The JVM specification is a formal document. It defines exactly what a valid
.classfile looks like, what each bytecode instruction does, how the heap and stacks behave, and how classes are loaded. It is maintained by Oracle as part of the Java SE platform. - A JVM implementation is actual software that obeys those rules. HotSpot (from OpenJDK), OpenJ9 (from Eclipse/IBM), and GraalVM (from Oracle Labs) are three different implementations.
The specification is the contract. The implementation is the product. Your compiled .jar file works on all of them because they all honor the same contract.
The Reserve Bank of India publishes a rulebook that defines exactly how cheques must be formatted and cleared. That rulebook is the JVM specification. The National Bank, HDFC, and ICICI each build their own cheque-clearing machines. Those machines are the JVM implementations. Each bank's machine works differently inside, but because they all follow the same Reserve Bank rulebook, any valid cheque works at any bank. The cheque is portable. The machine is not.
Understanding the Concepts
What does the specification define?
The JVM specification locks down everything that affects the correctness of your program:
- Class file format. Every
.classfile must have the magic number0xCAFEBABE, a version number, a constant pool, field and method descriptors, and attribute tables. The format is defined byte by byte. - Bytecode instruction set. All 200+ opcodes like
iadd,invokevirtual,aload, andireturnhave precisely defined behavior. Every JVM must execute them the same way. - Runtime data areas. The heap stores objects. The method area (Metaspace in HotSpot) stores class metadata. Each thread gets its own stack and program counter. The spec defines what these areas do, not how they are organized in memory.
- Class loading and linking. The spec defines the three phases: loading (reading bytes), linking (verification, preparation, resolution), and initialization (running static blocks). Every JVM must follow this sequence.
What does the specification leave open?
The spec deliberately does not define:
- Garbage collection algorithm. HotSpot offers G1, ZGC, Shenandoah, and Parallel. OpenJ9 uses gencon by default. The spec only says unreachable objects must eventually be collected.
- JIT compilation strategy. HotSpot uses tiered compilation (C1 + C2). OpenJ9 uses the Testarossa JIT. GraalVM has its own compiler. The spec does not require JIT at all.
- Internal memory layout. How objects are laid out in memory, how much padding is added, how the header looks. These are all implementation choices.
This is why different JVMs can compete. They all produce the same business result for the same bytecode, but they differ in pause times, memory usage, startup speed, and peak throughput.
What are the major JVM implementations?
- HotSpot is the default. It ships with every OpenJDK and Oracle JDK distribution. It has the most mature JIT compilers (C1 and C2) and the widest range of garbage collectors. Most Java developers use HotSpot without knowing it.
- Eclipse OpenJ9 was originally IBM's J9 JVM, open-sourced in 2017. It focuses on fast startup and low memory footprint. Good for containerized microservices where memory is expensive.
- GraalVM is built on HotSpot but replaces the C2 JIT with the Graal compiler. It also supports ahead-of-time compilation to native binaries (native-image), which eliminates JVM startup entirely. Popular for serverless and CLI tools.
How do you check which implementation is running?
You can read JVM properties and GC beans at runtime to confirm exactly which implementation and garbage collector are active:
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
public class JVMInspector {
public static void main(String[] args) {
System.out.println("JVM name : " + System.getProperty("java.vm.name"));
System.out.println("JVM vendor: " + System.getProperty("java.vm.vendor"));
System.out.println("Version : " + System.getProperty("java.vm.version"));
System.out.println("OS / Arch : " + System.getProperty("os.name")
+ " / " + System.getProperty("os.arch"));
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
System.out.println("Active GC : " + gc.getName());
}
}
}
# Run on HotSpot with G1 (default in Java 9+)
java JVMInspector
# Run on HotSpot with ZGC (low-latency trading)
java -XX:+UseZGC JVMInspector
# Same bytecode, different GC, same business result
JVM name : OpenJDK 64-Bit Server VM
JVM vendor: Eclipse Adoptium
Version : 17.0.9+9
OS / Arch : Linux / amd64
Active GC : G1 Young Generation
Active GC : G1 Old Generation
Why does this matter for production deployments?
In a real banking environment, the same compiled service runs on different fleets with different JVM configurations:
- The trading platform uses HotSpot with ZGC because it needs garbage collection pauses under 1 millisecond. Every millisecond of pause is money lost.
- The nightly batch system uses HotSpot with G1 because it cares about throughput, not pause time. G1 balances collection work across pause boundaries.
- The containerized microservices use OpenJ9 or GraalVM native-image because they need fast startup and small memory footprint in Kubernetes pods with tight resource limits.
None of these fleet decisions require rebuilding the application. The same .jar file is deployed everywhere. Only the JVM flags and the JVM distribution change.
Why is "Java is platform-independent" only half true?
The correct statement is: Java bytecode is platform-independent. The JVM itself is not.
- Your
.classfiles contain the same bytes whether compiled on Mac, Linux, or Windows. That bytecode runs unchanged on any conforming JVM. This is the portable layer. - The JVM binary is a native executable compiled for a specific OS and CPU. That is why you download different JDK builds for Linux x86, Linux ARM, macOS, and Windows. This is the platform-specific layer.
The portability boundary sits exactly at the bytecode. Above it, everything is universal. Below it, everything is native.
What are common mistakes?
- Assuming all JVMs perform the same. They honor the same spec, but G1 and ZGC have very different pause characteristics. Always benchmark on the actual JVM and GC you will use in production.
- Thinking the JVM is one product. It is a specification with many implementations. Saying "the JVM does X" is only accurate if X is defined by the spec.
- Ignoring the JVM version. Different versions of the same implementation (e.g., Java 11 HotSpot vs Java 21 HotSpot) can have significantly different GC and JIT behavior. Always test on the exact version you deploy.
- Confusing platform independence with performance portability. Your code produces the same result everywhere, but it may run faster on one implementation than another due to JIT and GC differences.
Visual Overview
┌───────────────────────────────────────────┐
│ JVM SPECIFICATION (the rulebook) │
│ class file format bytecode opcodes │
│ runtime data areas class loading rules │
│ │
│ GC algorithm? → NOT SPECIFIED │
│ JIT strategy? → NOT SPECIFIED │
│ Memory layout? → NOT SPECIFIED │
└───────────────────┬───────────────────────┘
│ implemented by
┌───────────┼────────────────┐
▼ ▼ ▼
HotSpot OpenJ9 GraalVM
G1/ZGC/Par gencon Graal JIT
C1+C2 JIT Testarossa native-image
Oracle/ Eclipse/ Oracle Labs
OpenJDK IBM
│ │ │
└───────────┴────────────────┘
▼
SAME bytecode → SAME business result
DIFFERENT operational characteristics
Key Takeaway
The JVM specification and a JVM implementation are two fundamentally different things.
The specification is a document. It defines the class file format, the bytecode instruction set, the runtime memory areas, and the rules for class loading. It is the universal contract that every JVM must honor. Crucially, it leaves garbage collection, JIT compilation, and internal memory layout unspecified on purpose.
An implementation is actual software that fulfills that contract. HotSpot, OpenJ9, and GraalVM are three competing implementations. They all produce the same business result for the same bytecode, but they differ in how they collect garbage, how they compile hot code, how fast they start, and how much memory they use.
This separation is what makes Java truly portable at the bytecode level. You compile your banking service once, and the same jar runs on HotSpot with G1 for batch processing, on HotSpot with ZGC for low-latency trading, and on OpenJ9 for memory-efficient microservices. The business logic is identical everywhere. Only the operational behavior changes.
But the JVM itself is not portable. It is a native executable built for a specific operating system and processor. That is why you download a different JDK for Linux ARM versus Windows x86. The portability line sits at the bytecode: above it, everything is universal; below it, everything is native and vendor-specific.