01-java-foundations

Core Java Fundamentals: JVM, Compilation, Memory, and Types

A staff-engineer guide to JVM architecture, Java compilation, memory model, and type behavior for backend engineers.

March 15, 2026Updated July 6, 2026
backend-engineerjavajvmmemorytypesgcclassloading

Core Java Fundamentals

This guide covers the runtime behavior and language foundation every backend engineer needs to understand when building production services. We go beyond syntax trivia into the JVM internals that determine latency, throughput, and reliability under load.


1. The Java Platform: JDK, JRE, JVM

At the center of Java is the JVM — the abstraction layer that decouples your code from the operating system. Understanding the three layers of the platform is essential for deployment, debugging, and performance tuning.

💡

Production note: In containerized environments, you typically use a JDK-based image for building and a JRE-based (or jlink-customized) image for runtime. The jlink tool can create a minimal runtime image containing only the modules your application needs — dramatically reducing container size.

Key distinctions

LayerWhat it includesWhen you need it
JDKjavac, jar, javadoc, jlink, full JREDevelopment, CI/CD builds
JREJVM + core libraries + deployment toolsRunning compiled applications
JVMBytecode interpreter, JIT compiler, GC, memory managerAlways — it is the runtime itself

Check your deployment: run java -version to see which JVM you're using. In production, prefer HotSpot (the standard Oracle/OpenJDK JVM) or GraalVM for ahead-of-time compilation use cases.


2. Compilation Pipeline

Java's compilation model is a two-stage process: source code is compiled to bytecode ahead of time, then the JVM further compiles hot paths to native code at runtime. This hybrid approach gives you portability with near-native performance.

The pipeline in detail

  • Source compilation (javac): Parses Java source, performs type checking, and emits .class files containing bytecode. This stage does no optimization — it is purely a syntactic and type-level transformation.
  • Class loading: The JVM loads classes lazily (on first reference). This is why startup time is proportional to the number of classes used, not the number on the classpath.
  • Bytecode verification: Ensures bytecode is well-formed — no stack overflow/underflow, no illegal type casts, no unsafe jumps. This is a critical security layer.
  • Interpretation: Freshly loaded methods start in interpreted mode. The JVM gathers profiling data (method call counts, branch patterns) during this phase.
  • JIT compilation: The JVM first interprets bytecode. Frequently executed ("hot") methods are compiled into native machine code by the JIT compiler. With HotSpot's default tiered compilation, methods are first compiled by C1 for fast execution and profiling, then recompiled by C2 for maximum optimization once sufficient runtime profiling data has been collected. The exact thresholds are JVM-version dependent and are based on both invocation counts and loop execution counts.
⚠️

Cold start impact: In serverless or autoscaling environments, the interpreted startup phase adds latency to initial requests. Strategies to mitigate this include:

  • Warmup loops during application initialization
  • GraalVM native-image for near-instant startup
  • Azul Prime's ReadyNow technology to cache JIT-compiled code

Tiered compilation explained

TierCompilerBehaviorWhen
0InterpreterNo compilationStartup, cold methods
1C1 (simple)Basic optimizations, profilingMildly hot methods
2C1 (limited)More profilingWarming up
3C1 (full)Full profiling, countersNearing threshold
4C2Aggressive optimizations, PGOVery hot methods

3. Bytecode and Execution Model

Bytecode is a stack-based intermediate representation. Each method invocation creates a stack frame containing:

How a simple expression executes

Consider the expression int c = a + b; after compilation to bytecode:

java
// Java source
int a = 10;
int b = 20;
int c = a + b;
java
// Corresponding bytecode (mnemonic)
ICONST_10    // Push int constant 10 onto operand stack
ISTORE_1     // Pop top value → local variable 1 (a)
BIPUSH 20    // Push int constant 20 onto stack
ISTORE_2     // Pop top value → local variable 2 (b)
ILOAD_1      // Push local variable 1 (a) onto stack
ILOAD_2      // Push local variable 2 (b) onto stack
IADD         // Pop two ints, add them, push result
ISTORE_3     // Pop result → local variable 3 (c)
💡

The JVM is defined by bytecode, not by source language. Any language that compiles to valid .class files — Scala, Kotlin, Clojure, Groovy, JRuby — can run on the JVM, enjoying the same JIT, GC, and tooling ecosystem.


4. JVM Memory Model

The JVM divides memory into several distinct regions, each with different performance characteristics and GC behavior.

Region characteristics

RegionShared?ContentGCSize control
EdenYesNew object allocationsMinor GC-Xmn, -XX:NewRatio
Survivor (S0/S1)YesObjects surviving minor GCMinor GC-XX:SurvivorRatio
Old GenerationYesLong-lived objectsMajor/Full GC-Xms, -Xmx
MetaspaceYesClass metadata, constant poolClass unloading-XX:MaxMetaspaceSize
StackNo (per-thread)Local variables, framesDeallocated on method exit-Xss (default 1MB)
Code CacheYesJIT-compiled native codeSweep of stale code-XX:ReservedCodeCacheSize
Direct BuffersYesOff-heap I/O buffersNative GC via Cleaner-XX:MaxDirectMemorySize

Key production insight: Heap allocations cost GC pressure. Stack allocations are nearly free (just a stack-pointer bump). In high-throughput services, prefer stack-allocated primitives and avoid creating intermediate objects in hot paths.

Common memory configuration flags

bash
# Heap sizing (always set both -Xms and -Xmx to avoid resize pauses)
-Xms4g -Xmx4g
 
# Young generation sizing
-Xmn2g
-XX:NewRatio=2          # Old : Young = 2 : 1
-XX:SurvivorRatio=8     # Eden : Survivor = 8 : 1 (default)
 
# Metaspace
-XX:MaxMetaspaceSize=256m
 
# Stack size per thread
-Xss512k
 
# Direct memory (for NIO)
-XX:MaxDirectMemorySize=512m

5. Garbage Collection

Garbage collection is the automatic reclamation of heap memory. The JVM uses a generational hypothesis: most objects die young.

GC algorithms compared

GC AlgorithmFlagPause typeThroughputLatencyBest for
Serial-XX:+UseSerialGCStop-the-world (all)LowPoorSingle-threaded, small heaps
Parallel-XX:+UseParallelGCStop-the-world (all)HighModerateBatch processing, high-throughput
G1-XX:+UseG1GCRegional, incrementalModeratePredictableDefault since Java 9, most services
ZGC-XX:+UseZGCConcurrent (sub-ms)ModerateUltra-lowLarge heaps, low-latency services
Shenandoah-XX:+UseShenandoahGCConcurrent (sub-ms)ModerateUltra-lowLarge heaps, low-latency services
⚠️

G1 GC has been the default garbage collector since Java 9 and is a good general-purpose choice for most server-side applications. For applications with very large heaps (tens to hundreds of GB) or very low latency requirements (e.g., sub-10 ms or sub-millisecond pause targets), consider ZGC or Shenandoah. ZGC was introduced as an experimental feature in Java 11 and became production-ready in Java 15.

GC tuning by workload type

Batch / data processing (high throughput):

bash
-XX:+UseParallelGC
-XX:ParallelGCThreads=8
-Xms8g -Xmx8g

Low-latency web services (sub-10ms p99):

bash
-XX:+UseZGC
-XX:ZAllocationSpikeTolerance=2.0
-Xms8g -Xmx8g
-XX:ConcGCThreads=4

General purpose services (default recommended):

bash
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:G1HeapRegionSize=4m
-Xms4g -Xmx4g

Practical guidance

  • Monitor GC logs: -Xlog:gc*:file=gc.log:tags,time,uptime,level (Java 9+ unified logging)
  • Watch for promotion failures: -XX:+PrintTenuringDistribution helps tune survivor space sizes
  • Avoid large young gen: Pauses scale with young gen size. 2-4 GB is typical for most services
  • Use jstat live: jstat -gcutil <pid> 1s shows GC utilization per second

6. Class Loading and Initialization

Class loading is a three-phase process that happens lazily — the JVM only loads a class when it is first referenced.

Class loader hierarchy

The JVM uses a delegation model for class loaders:

When initialization happens

A class is initialized (static blocks execute) only when:

  1. A new instance is created
  2. A static method is invoked
  3. A static field is accessed
  4. Class.forName() is called
  5. It is the initial class of a JVM launch
⚠️

Production risk: Heavy static initialization (database connections, thread pools in static blocks) delays application startup and can cause cold-start timeouts in containerized environments. Use lazy initialization or dependency injection to defer expensive setup.

java
public class ExpensiveInit {
    // BAD: runs during class loading, delays startup
    static {
        DatabasePool.init("jdbc:...", 50);
    }
 
    // GOOD: initialized on first use
    private static class LazyPool {
        static final DatabasePool INSTANCE = DatabasePool.init("jdbc:...", 50);
    }
 
    public static DatabasePool pool() {
        return LazyPool.INSTANCE;
    }
}

7. Primitive vs Reference Types

Java's type system is split into two worlds. Understanding the distinction is critical for writing high-performance backend code.

Memory and performance comparison

AspectPrimitiveReference (wrapper)
StorageDirect value on stack or inline in objectPointer (8 bytes) + heap object (16+ bytes)
NullCannot be nullCan be null
Identity== compares values== compares references; use .equals()
GenericsNot allowed directlyRequired (List<Integer> not List<int>)
CollectionsNot supportedSupported
Memory overhead1–8 bytes16–24 bytes (object header + value + padding)
Array memoryint[1M] = 4 MBInteger[1M] = ~20 MB (objects + array of refs)

Autoboxing pitfalls

java
// Hidden allocation: each addition creates a new Long object
Long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
    sum += i;  // ← unbox + add + box = 1M allocations!
}
 
// Better: use primitive in loop, box only at the end
long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
    sum += i;
}
Long result = sum;  // single allocation

JMH benchmark insight: A hot loop using Long instead of long can be 10-50x slower due to allocation, GC pressure, and cache misses. Always reach for primitives in performance-sensitive code.

Equality pitfalls with wrappers

java
Integer a = 127;
Integer b = 127;
System.out.println(a == b);      // true (cached: -128 to 127)
 
Integer c = 200;
Integer d = 200;
System.out.println(c == d);      // false (not cached!)
 
// Always use .equals() or unbox
System.out.println(c.equals(d)); // true
System.out.println((int)c == (int)d); // true

8. equals(), hashCode(), and Object Contracts

The equals() and hashCode() methods form the backbone of all hash-based collections. Getting them wrong is one of the most common sources of production bugs.

The contract

RuleDescription
Reflexivex.equals(x) must be true
Symmetricx.equals(y)y.equals(x)
Transitivex.equals(y) && y.equals(z) ⇒ x.equals(z)
ConsistentMultiple invocations return same result (if no mutation)
hashCode contractIf x.equals(y), then x.hashCode() == y.hashCode()
hashCode consistencyhashCode() must return the same value across invocations
java
public final class UserId {
    private final String value;
 
    public UserId(String value) {
        this.value = Objects.requireNonNull(value);
    }
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof UserId other)) return false;
        return value.equals(other.value);
    }
 
    @Override
    public int hashCode() {
        return value.hashCode();  // same fields as equals()
    }
}
⚠️

Never use mutable fields in equals() or hashCode(). If an object's hash code changes while it is stored in a HashMap or HashSet, the collection becomes corrupted — the object is "lost" in the wrong hash bucket and will never be found.

Production patterns for equals/hashCode

PatternApproachWhen to use
Objects.equals() + Objects.hash()return Objects.equals(field1, field2)Simple value objects
record (Java 16+)record UserId(String value) {}Immutable data carriers
AutoValue / Lombok @DataAnnotation-based generationTeams, consistent conventions
ManualHand-written with instanceof pattern matchingFine-grained control, legacy code

Common bug: mutable fields in hashCode

java
// BAD: hashCode changes when setName() is called
public class BadEntity {
    private String name;
 
    @Override
    public int hashCode() { return name.hashCode(); }
}
 
// Usage
var map = new HashMap<BadEntity, String>();
var e = new BadEntity();
e.setName("Alice");
map.put(e, "value");
e.setName("Bob");
map.get(e); // → null (wrong bucket!)

9. final, finally, and finalize

These three keywords sound alike but serve completely different purposes.

final

java
public final class ImmutableConfig {       // cannot be subclassed
    private final int maxConnections;       // must be set once, never reassigned
 
    public ImmutableConfig(int maxConnections) {
        this.maxConnections = maxConnections;
    }
 
    public final int getMaxConnections() {  // cannot be overridden
        return maxConnections;
    }
}

Effective Java principle: "Minimize mutability." Declare fields final by default. Mark classes final unless designed for inheritance. This makes code easier to reason about and safer in concurrent contexts.

finally

java
// Prefer try-with-resources (Java 7+)
try (Connection conn = dataSource.getConnection();
     PreparedStatement stmt = conn.prepareStatement(sql)) {
    return stmt.executeQuery();
}
// Resources are auto-closed, no finally needed
 
// Use finally only for non-Closeable cleanup
CountDownLatch latch = new CountDownLatch(1);
try {
    processAsync(latch);
} finally {
    latch.countDown();  // ensures thread unblocks even on exception
}

finalize()

java
// DEPRECATED — never use in production
@Override
@Deprecated(since = "9")
protected void finalize() throws Throwable {
    // Runs unpredictably, if at all
    // Use Cleaner or try-with-resources instead
}
Featurefinalfinallyfinalize()
CategoryKeyword (modifier)BlockMethod
PurposePrevent modificationGuarantee cleanupObject finalization
Use it?Always (for immutability)When try-with-resources won't fitNever
Replacementtry-with-resourcesCleaner, AutoCloseable

10. Production Observations

Java fundamentals are not just theory — they shape how backend services behave under real traffic.

Performance decision tree

Key takeaways

  • JVM startup and class loading affect deployment times, especially in autoscaling environments
  • Heap layout and GC choices determine your latency profile — benchmark with your actual workload
  • Primitive vs reference decisions matter in high-throughput code; use long not Long in hot loops
  • equals()/hashCode() correctness is one of the most common backend bugs in caches, session stores, and collections
  • final is a design tool, not an optimization hint — modern JIT compilers are not constrained by final for optimization
  • Profile before tuning: use async-profiler or JFR (Java Flight Recorder) to identify real bottlenecks before optimizing

Essential JVM flags for production

bash
# Always set these
-Xms4g -Xmx4g                    # Fixed heap (no resize)
-XX:+UseG1GC                     # Modern GC
-XX:MaxGCPauseMillis=100         # Pause target
-Xlog:gc*:file=gc.log:tags,time,uptime,level   # GC logging
-XX:+ExitOnOutOfMemoryError      # Fail fast on OOM
-XX:+HeapDumpOnOutOfMemoryError  # Capture heap dump
-XX:HeapDumpPath=/var/log/app/   # Dump location
-Djava.security.egd=file:/dev/urandom  # Faster secure random

Interview Questions

  • What is the difference between JDK, JRE, and JVM?
  • How does Java compile source code to executable form? Explain the role of javac, bytecode, and JIT compilation.
  • What are the main JVM memory regions, and how do they differ?
  • Why does the JVM use generational garbage collection? Describe the lifecycle of an object from Eden to Old Gen.
  • What happens during class loading and initialization? Describe the three phases.
  • How do primitive types differ from reference types? When does autoboxing hurt performance?
  • Why must equals() and hashCode() be overridden together? What happens if only one is overridden?
  • What is the difference between final, finally, and finalize()?
  • How does tiered compilation work? What are the four tiers?
  • What GC algorithm would you choose for a low-latency payment service? Why?
  • What happens if you use mutable fields in hashCode()? How would you debug it?
  • How would you reduce JVM cold start time in a serverless environment?