How Does Java Execution Differ from C? A 3‑Minute JVM Deep Dive
This article compares the compilation and runtime processes of C and Java programs, explains Java's class loading, linking, initialization, memory layout, execution engine components, and native interface, and highlights why Java startup is slower than native C execution.
C Compilation Process
Given source files a1.c, a2.c and a3.c, the C compiler first produces three object files ( a1.obj, a2.obj, a3.obj). The linker then combines these object files into a single native executable ( a.exe). At runtime the operating‑system loader maps the executable into RAM and the CPU executes the native machine code directly.
Java Compilation and Execution
For Java source files a1.java, a2.java and a3.java, the Java compiler ( javac) generates three bytecode files ( a1.class, a2.class, a3.class). These .class files are not native code; they are loaded by the JVM ClassLoader into the method area. The Byte‑Code Verifier checks each class for structural correctness and security constraints. After verification the Execution Engine interprets the bytecode or, for frequently executed methods, the Just‑In‑Time (JIT) compiler translates the bytecode into native machine code. This extra loading, verification and JIT compilation explains why Java programs have a slower startup compared with native C programs.
JDK, JRE, and JVM Implementations
The JDK (Java Development Kit) bundles development tools (compiler, debugger, etc.) together with a JRE (Java Runtime Environment). The JRE contains the minimal runtime components required to run Java applications, namely the JVM and core class libraries.
Major JVM implementations include:
IBM JVM / J9
HotSpot VM (the reference implementation in OpenJDK)
JRockit (discontinued)
Dalvik (Android, discontinued)
Class Loading Mechanism
Java classes can be grouped into three logical categories:
JDK internal classes (e.g., java.*, jdk.*) located under jdk/jre/lib Extension classes (built‑in or third‑party) located under jdk/jre/lib/ext (deprecated after Java 9 in favor of modules)
Application‑specific business classes defined by developers
The JVM uses three built‑in class loaders that follow the parent‑delegation model:
Bootstrap ClassLoader – loads core JDK classes from jdk/jre/lib Extension ClassLoader – loads classes from jdk/jre/lib/ext Application ClassLoader – loads classes from the application classpath
When a request to load Student.class is issued, the request is first delegated to the Application ClassLoader, which forwards it to its parent (Extension ClassLoader), which in turn forwards it to the Bootstrap ClassLoader. If none of the parents find the class, the Application ClassLoader finally searches the classpath and loads the class. Failure at all levels results in ClassNotFoundException. The delegation can be overridden by custom class loaders, SPI mechanisms, or modular frameworks such as Tomcat.
Linking Phase
After a class file is loaded, the JVM performs the linking phase, which consists of three sub‑steps.
Verification
The Byte‑Code Verifier checks that the .class file conforms to the JVM specification: correct file format, valid constant‑pool entries, proper use of types, and absence of illegal instructions. If verification fails, the JVM throws VerifyError.
Preparation
During preparation the JVM allocates memory for all static fields of the class in the method area and assigns them their default values (zero, null, etc.). No Java code is executed at this stage.
Resolution
Resolution converts symbolic references in the constant pool (e.g., com.example.Teacher#getScore()) into direct references such as memory addresses or method table indices. This enables the execution engine to invoke methods without further indirection.
Initialization
Initialization is the final step of class loading. The JVM executes static initializers and static blocks, assigning explicit values to static fields and running any code inside static { … } blocks.
Class Lifecycle
The complete lifecycle of a class is:
Loading – the binary .class data is read into the method area.
Linking – verification, preparation, and resolution.
Initialization – execution of static initializers.
Active use – the class is referenced by running code.
Unloading – when the class and its class loader become unreachable, the JVM may reclaim the memory.
JVM Runtime Data Areas
The JVM memory model is divided into several logical regions:
Method Area (non‑heap) – stores class metadata, constant pool, static variables, and JIT‑compiled code. Each loaded class has a single CLASS object that holds its name, loader, method table, and annotations.
Heap – the largest shared area where all object instances and arrays reside. The heap is further divided into the Young Generation (Eden + Survivor spaces) and the Old Generation, each collected by different GC algorithms.
Java Stack – a thread‑private stack of frames. Each frame contains a local variable table, an operand stack, a dynamic link, and a return address.
Program Counter (PC) Register – a per‑thread pointer to the next bytecode instruction to be executed.
Native Method Stack – supports execution of native (C/C++) methods.
Method Area Details
Static fields and the bytecode of each class are stored here. The CLASS object is created once per class, regardless of how many instances exist.
Heap Details
All objects and arrays are allocated on the heap. The Young Generation is collected frequently using a copying collector, while the Old Generation is collected less often using a mark‑sweep‑compact algorithm.
Stack Details
Each thread creates its own JVM stack. Method invocation pushes a new frame; method return pops the frame. The local variable table holds primitive values and object references; the operand stack is used for intermediate computation.
Program Counter Register
Each thread maintains an independent PC register so that after a context switch the JVM can resume execution at the correct bytecode offset.
Execution Engine
The execution engine runs bytecode and consists of three components:
Interpreter – reads bytecode instruction by instruction and executes it directly.
JIT Compiler – identifies hot methods, compiles their bytecode into native machine code, and replaces the interpreted version for subsequent calls.
Garbage Collector – reclaims memory occupied by objects that are no longer reachable.
Java Native Interface (JNI) and Native Method Libraries
JNI is a specification that allows Java code to call native libraries written in C/C++ and vice‑versa. Native method libraries are collections of compiled native code (e.g., .so on Linux, .dll on Windows) that are loaded by the JVM when a native method is invoked.
Summary
In contrast to C, which compiles directly to native machine code, Java follows a two‑step process: source → bytecode → runtime execution by the JVM. The JVM performs class loading (with a parent‑delegation hierarchy), linking (verification, preparation, resolution), and initialization before the classes are used. At runtime the JVM manages several memory areas (method area, heap, stacks, PC register, native method stack) and executes bytecode via an interpreter or a JIT compiler, while a garbage collector reclaims unused objects. Understanding these components provides a solid foundation for JVM‑related interview questions and performance tuning.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
