Java JVM Interview Questions — Practice Real Interview Problems

Reviewed by Mark Dickie · Last updated

12 questions on this page you can answer and see marked on the spot. Go to the first one

The Java Virtual Machine (JVM) is the runtime engine that loads, verifies, and executes Java bytecode while managing memory and garbage collection. For interview preparation, you should understand the JVM memory model (heap, stack, method area, metaspace), how garbage collection works across different collectors (Serial, Parallel, G1, ZGC), the class loading process and its delegation hierarchy, and how the JIT compiler optimizes hot code. These topics appear repeatedly in Java interviews because they separate candidates who can write Java from those who understand what happens when their code runs.

What does a Java JVM interview typically test?

Interview questions on the JVM usually fall into four areas: memory management, garbage collection, class loading, and runtime optimization. A strong candidate can explain where objects live in memory, when and why the GC runs, how classloaders delegate, and what the JIT compiler does to speed up frequently executed methods.

JVM TopicWhat to KnowCommon Question Type
Heap vs. StackHeap holds objects; stack holds frames and primitivesExplain where a variable lives
Garbage CollectorsSerial, Parallel, G1, ZGC with trade-offs in latency vs. throughputChoose a collector for a given workload
Class LoadingBootstrap, Platform, Application classloaders and parent delegationWhy does parent delegation matter?
JIT CompilationC1/C2 compilers, tiered compilation, on-stack replacementWhat triggers JIT compilation?

How should I prepare for JVM-specific questions?

  1. Study the JVM memory areas: know which region stores what, and what happens when each fills up.
  2. Learn the major garbage collectors and when to use each one (G1 is the default since Java 9; ZGC targets sub-millisecond pause times).
  3. Practice reading GC logs so you can identify a stop-the-world pause, a concurrent phase, and a full GC.
  4. Review classloader delegation and the class-spoofing problems it prevents.
  5. Understand the difference between interpreted and JIT-compiled execution, and what -XX:+PrintCompilation reveals about your code.

Key facts

  • Tarmac has 31 Java interview questions on this topic, 25 of them on this page, at difficulty 1–5 of 5.
  • Tarmac tracked 1,645 job postings asking for Java in September 2026.
  • Roles asking for Java advertise a median base salary of £80,000, across 352 job postings as of September 2026.
  • Tarmac last reviewed these Java interview questions on 13 October 2026.

At a glance

Questions25 shown · 31 in the bank
Difficulty1–5 of 5
FormatsTrue / false, Short answer, Multiple choice, Fill in the blank, Code output, Ordering, Find the bug

What you'll review

  1. jvm
  2. garbage collection
  3. heap stack
  4. class loading

Practice questions

Try one before you open the answer. Pick an option and press Check; it's marked on the spot.

True or false: In Java, local variables declared inside a method are automatically initialized to their type's default value (for example, int to 0 and boolean to false).#

Options

Show answer

False. Java does not give local variables default values. Only instance and static fields are automatically set to defaults such as 0 or false. A local variable must be definitely assigned before it is read, or the compiler rejects the code.

Why:

This is false. Only instance fields and static fields receive default values, assigned by the JVM during object or class initialization. Local variables live in the method's stack frame and are not initialized. The compiler enforces definite assignment, so reading a local variable before it has been assigned is a compile-time error rather than a silent zero or false.

Which JVM runtime data area is used to allocate memory for all class instances (objects) created with the new keyword?#

Show answer

The heap. Objects and arrays created with new are allocated on the JVM heap, which is shared by all threads and is the region managed by the garbage collector.

Why:

The JVM heap is the runtime data area from which memory for all class instances and arrays is allocated. The JVM stack stores frames holding local variables and partial results, the method area stores class metadata, and the native method stack serves native (JNI) calls. The heap is shared among all threads and is the target of garbage collection, which is why it is the correct answer.

Java source code is compiled into bytecode (.class files) rather than machine code specific to a particular operating system or CPU architecture. This bytecode is interpreted and/or JIT-compiled by the JVM at runtime, which means the same compiled .class file can run on any platform that has a compatible JVM implementation.#

Options

Show answer

True. Java source code is compiled into platform-neutral bytecode (.class files) by javac. The JVM — which is itself platform-specific — interprets and JIT-compiles that bytecode at runtime, so the same compiled class file can run on any operating system with a compatible JVM implementation.

Why:

The Java compiler (javac) translates .java source files into platform-neutral bytecode stored in .class files. The JVM — which IS platform-specific — loads and executes that bytecode, so the same compiled artifact runs unchanged on Windows, Linux, macOS, or any other OS with a JVM. This is the core of Java's 'write once, run anywhere' model. The statement is true.

In the Java Virtual Machine memory model, which runtime data area is shared among ALL threads of a running Java application?#

Options

Show answer

The Heap is the JVM runtime data area shared among all threads. It stores all object instances and arrays, and every thread in the application reads from and writes to the same heap. The JVM Stack, Program Counter Register, and Native Method Stack, by contrast, are each allocated per-thread and are private to a single thread.

Why:

The JVM Heap is a single shared memory area where all object instances and arrays are allocated. Every thread in the application accesses the same heap, which is why heap objects can be passed between threads and why thread-safety mechanisms (synchronization, volatile, etc.) are needed. By contrast, the JVM Stack, the Program Counter Register, and the Native Method Stack are all created on a per-thread basis — each thread has its own private set of these areas. Therefore the Heap is the correct answer.

True or False: Java source code is compiled into platform-independent bytecode, which is then executed by the JVM.#

Options

Show answer

True. The javac compiler converts Java source into platform-independent bytecode stored in .class files, and the JVM loads and executes that bytecode on whichever platform it is installed on. This compile-once, run-anywhere model is the core design principle of Java.

Why:

The javac compiler translates .java source files into .class files containing JVM bytecode. This bytecode is platform-independent; it is the JVM's job to load, verify, and execute (interpret or JIT-compile) it on the host machine.

What is the default value assigned to an instance (non-local) variable of type int in Java?#

Options

Show answer

The default value of an int instance variable in Java is 0. The JVM initializes all instance fields to their type's zero value during object construction; for primitive int that zero is the integer 0, whereas reference types default to null and floating-point types default to 0.0.

Why:

The JVM zeroes out all instance fields during object construction. For the primitive type int, that zero value is the integer literal 0. null is the default for object references, 0.0 is the default for double/float, and no Java primitive type defaults to a negative value.

Fill in the blank: Java source code is compiled into _____ code, which the JVM then loads, verifies, and either interprets or compiles to native machine code at runtime.#

Show answer

Fill in the blank: Java source code is compiled into bytecode code, which the JVM then loads, verifies, and either interprets or compiles to native machine code at runtime.

Why:

The javac compiler does not produce machine code directly; it emits JVM bytecode (stored in .class files). The JVM's class loader and bytecode verifier process this bytecode, and the execution engine either interprets it or the JIT compiler compiles hot paths to native instructions.

In the Java Virtual Machine, which component is responsible for automatically reclaiming memory from objects that are no longer referenced by the running program?#

Options

Show answer

The Garbage Collector is the JVM component that automatically reclaims heap memory from objects no longer reachable from any live reference. The Class Loader, JIT Compiler, and Execution Engine do not perform this reclamation; they load classes, optimize bytecode, and execute it.

Why:

The Garbage Collector periodically identifies objects that are no longer reachable from any live reference and reclaims the heap memory they occupied. The Class Loader only loads class files, the JIT Compiler optimizes bytecode into native code, and the Execution Engine interprets bytecode; none of these manage reclamation of unused objects.

In Java, the garbage collector automatically identifies unreachable objects and reclaims their heap memory without the programmer needing to explicitly free each object. True or false?#

Options

Show answer

True. Java's garbage collector automatically identifies unreachable objects and reclaims their heap memory, so programmers do not free each object by hand. It traces reachability from GC roots, which removes the need for manual deallocation calls like C's free() or C++'s delete.

Why:

This is true. The JVM's garbage collector traces object reachability from GC roots and reclaims heap memory held by objects that cannot be reached. The programmer does not call a deallocation function for each object, unlike C's free() or C++'s delete. The timing of collection is up to the JVM, but the reclamation of unreachable objects is automatic.

What is the primary purpose of garbage collection in the JVM?#

Show answer

The primary purpose is to automatically reclaim heap memory occupied by objects that are no longer reachable from any live reference, so the programmer does not have to deallocate memory manually. This helps avoid leaks caused by forgotten objects, although objects that are still referenced are never collected.

Why:

The JVM garbage collector finds objects that cannot be reached from any live reference (such as local variables, static fields, or active threads) and reclaims the heap memory they occupy. This frees the programmer from explicit deallocation and helps prevent leaks of forgotten objects. It does not collect objects that are still reachable, so a leak can still occur if references are kept unintentionally.

In the Java Virtual Machine specification (as implemented by HotSpot), which of the following runtime data areas is shared among all threads rather than allocated per thread?#

Options

Show answer

The heap is the runtime data area shared among all threads in the Java Virtual Machine. It stores class instances and arrays, which is why objects reachable from multiple threads must be synchronized. The JVM stacks, the program counter register, and native method stacks are all allocated per thread.

Why:

The heap is the runtime data area shared by all threads, where class instances and arrays are allocated. Each thread gets its own Java Virtual Machine stack, its own program counter register, and its own native method stack, which are created with the thread and destroyed when it terminates. Since only the heap is shared among the offered areas, it is the single correct answer.

What is the exact output of the following Java program when run with default JVM settings (no overridden Integer cache bounds)? Print each line exactly as it appears.#

public class Main {
    public static void main(String[] args) {
        Integer a = 127;
        Integer b = 127;
        System.out.println(a == b);

        Integer c = 128;
        Integer d = 128;
        System.out.println(c == d);
    }
}
Show answer
true
false
Why:

Autoboxing an int uses Integer.valueOf, which returns cached objects for values from -128 to 127 by default. Both 127 values therefore refer to the same cached Integer object, so the reference comparison a == b is true. The value 128 is outside the default cache range, so each autoboxing call creates a new Integer object; c == d compares two distinct references and is false. The program prints true on the first line and false on the second.

What does this Java program print when run on a standard JVM with default settings?#

Integer a = 100; Integer b = 100; Integer c = 200; Integer d = 200; System.out.println(a == b); System.out.println(c == d);
Show answer
true
false
Why:

Java's Integer autoboxing uses a built-in cache for values in the range [-128, 127] (JLS §5.1.7). When you assign 100 to both a and b, each gets the same cached Integer object, so a == b compares identical references and prints true. The value 200 falls outside the cache range, so c and d are each backed by separate newly-constructed Integer objects; reference comparison via == therefore yields false. Note that .equals() would return true in both cases because Integer.equals compares the underlying int value, not object identity.

In Java, the garbage collector reclaims an object's memory only when the object is no longer _____ from any live thread or GC root. Fill in the single standard word for the blank.#

Show answer

In Java, the garbage collector reclaims an object's memory only when the object is no longer reachable from any live thread or GC root. Fill in the single standard word for the blank.

Why:

The Java garbage collector traces from a set of GC roots (active thread stacks, static fields, local variables, etc.) and marks every object transitively reachable from those roots. Any object not found during this tracing phase is considered unreachable and is eligible for collection. Because the sentence reads 'the object is no longer ___ from any live thread or GC root', the standard term that fills the blank is 'reachable', giving 'the object is no longer reachable'.

Consider the following Java code. Assume no other references exist beyond what is shown, and Node is a class with a Node next field and no other fields.#

Options

Show answer

Both X and Y are eligible for garbage collection.

Why:

After b = null, object Y is still strongly reachable through a.next, and X is still reachable through the local variable a. After a = null, no local variable or other GC root references X, so X is unreachable. Y's only strong reference came from X's next field, and since X is itself unreachable, nothing live reaches Y either. An object is eligible for collection when no chain of strong references from a GC root leads to it, so both X and Y are eligible. The option claiming X and Y keep each other alive is wrong because the references only run one way, from X to Y, and no reference points back from Y to X.

Java is strictly pass-by-value: when you pass an object to a method, the method receives a copy of the reference, so reassigning the parameter inside the method does not change which object the caller's variable points to (though mutating the object's fields is visible to the caller).#

Options

Show answer

True — Java is always pass-by-value. For objects, the value copied is the reference, so the parameter and the caller's variable point at the same object: mutating that object's fields is visible to the caller. But reassigning the parameter only rebinds the local copy of the reference, leaving the caller's variable unchanged. That is why a method can never swap two of the caller's variables — the giveaway that Java has no pass-by-reference.

Why:

Java is always pass-by-value, with no exceptions. For object types, the value that is copied is the reference itself, not the object — so the parameter and the caller's variable initially point at the same object. Mutating that shared object's state (e.g. list.add(x) or obj.setName(...)) is visible to the caller, which is why people mistakenly call it 'pass-by-reference'. But reassigning the parameter (obj = new Thing()) only rebinds the local copy of the reference; the caller's variable still points at the original object. The tell that it is pass-by-value: a method cannot swap two of the caller's variables or make the caller's variable point somewhere new.

Order the phases the JVM performs the first time a class is actively used, from locating the bytecode to the class being ready to run.#

Put these in order

Show answer

The JVM loads a class in a fixed order: first Loading reads the .class bytecode and creates the Class object, then Verification checks the bytecode for safety, then Preparation allocates static fields and sets them to default zero values, then Resolution turns constant-pool symbolic references into direct references, and finally Initialization runs static initializers and assigns static fields their real values. Verification, Preparation, and Resolution together form the linking phase, and initialization is lazy — triggered on first active use.

Why:

Class loading follows a fixed sequence: Loading reads the bytecode and produces the Class object; Linking then runs as Verification (bytecode safety checks), Preparation (static fields get memory and default zero values, NOT their declared values yet), and Resolution (constant-pool symbolic references become direct references); finally Initialization runs static initializer blocks and assigns static fields their actual values. The key distinction interviewers probe is Preparation vs Initialization — a static int x = 5 is set to 0 in Preparation and only becomes 5 in Initialization, which is why static state is observable as its zero value during early bootstrapping. Initialization is lazy: it happens on first active use (instantiation, a static access, etc.), not at JVM startup.

What does the following Java program print? Consider how the JVM handles String literals, compile-time constant concatenation, and the new String(...) constructor with respect to == versus .equals(). Give the exact output, one line per printed value.#

public class Main {
    public static void main(String[] args) {
        final String a = "a";
        final String b = "b";
        String s = "ab";
        String t = a + b;
        String u = new String("ab");
        String v = new String(a) + new String(b);
        System.out.println(s == t);
        System.out.println(s == u);
        System.out.println(s == v);
        System.out.println(s.equals(v));
    }
}
Show answer
true
false
false
true
Why:

Because a and b are final String variables initialized with literals, a + b is a compile-time constant expression, so the compiler folds it to the literal "ab", which is interned in the string pool; s == t is therefore true. The new String("ab") call allocates a new heap object distinct from the pooled literal, so s == u is false. new String(a) + new String(b) is a runtime concatenation that produces another new heap object, so s == v is false. Finally, s.equals(v) compares character content, which is ab in both, so it prints true. The four lines therefore read true, false, false, true.

The class below implements double-checked locking for lazy singleton initialization. It compiles cleanly, but another thread can observe a partially constructed Singleton. Count lines from 1 (line 1 is public class Singleton {) and give the number of the buggy line.#

public class Singleton {
    private static Singleton instance;
    private int value;
    private Singleton() { value = 42; }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
Show answer

The bug is on line 2.

Why:

Line 2 declares instance without volatile. Under the Java Memory Model (JLS §17.4), a plain write to instance on line 9 may become visible to other threads before the writes to value performed in the constructor on line 4, because the JIT and CPU may reorder these stores. A second thread can then pass the unsynchronized null-check on line 6, skip the lock, and return an object whose value is still 0. The fix is to declare the field volatile on line 2: a volatile write happens-before any subsequent volatile read of the same field, so the constructor's writes are visible before the reference is published. The lock on line 7 alone does not fix this, because the outer null-check on line 6 reads instance without holding the lock.

Consider the following Java code, written inside a method body:#

Show answer

2 objects. After a = null and b = null, neither object is reachable from any GC root (local variables, static fields, and similar). The JVM uses reachability-from-roots tracing rather than reference counting, so the cycle A→B→A does not keep either object alive; both are unreachable and therefore eligible for garbage collection.

Why:

The JVM's garbage collectors trace reachability starting from GC roots such as local variables on the stack, static fields, and JNI references. After both local variables a and b are set to null, no root holds a reference to object A or object B, so neither is reachable. The reference cycle between them is only a path between two unreachable objects and cannot make either one reachable from a root. Because the JVM does not use reference counting, the cycle is irrelevant, and both objects are eligible for collection, giving an answer of 2.

Consider the following Java program. Trace the JVM's class initialization and instance initialization order to determine the exact console output. Write the integers printed, one per line, in order.#

class Parent {
    static { System.out.println(1); }
    { System.out.println(2); }
    Parent() { System.out.println(3); }
}

class Child extends Parent {
    static { System.out.println(4); }
    { System.out.println(5); }
    Child() { System.out.println(6); }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(7);
        new Child();
        System.out.println(8);
    }
}
Show answer
7
1
4
2
3
5
6
8
Why:

When Main.main is invoked, Main is loaded and initialized first (it has no static block), so System.out.println(7) runs first. Then new Child() triggers initialization of Child. Per JLS 12.4.1, a class's superclass is initialized before the class itself, so Parent's static block runs first (1), then Child's static block (4). For instance creation (JLS 12.5), the Child constructor implicitly calls super() before its body, which runs Parent's instance initializer (2) and then the Parent constructor body (3). Control returns to Child, whose instance initializer runs (5), followed by the Child constructor body (6). Finally, main prints 8. The output is therefore 7, 1, 4, 2, 3, 5, 6, 8, one per line.

In the HotSpot JVM (Java 17+), explain what a safepoint is, why the JVM requires it, and describe the key constraints on how and when threads reach one.#

Show answer

A safepoint is a program location at which the JVM guarantees every application thread is in a known, stable state: stack maps are available, registers are spilled, and no object reference is in an intermediate state. The JVM requires safepoints because operations such as garbage collection (and deoptimization) need a globally consistent view of the heap so they can walk the object graph, enumerate roots (thread stacks, registers, globals), and relocate or free objects. Threads must be paused at these well-defined points. In JIT-compiled code, HotSpot inserts safepoint poll checks only at specific locations, such as method returns and loop back-edges, so a thread running compiled code reaches a safepoint only at one of those polls, not at an arbitrary instruction. A thread that is running Java code must poll and block, while a thread that is blocked in native code or otherwise already in a safe state can be treated as being at a safepoint. Stop-the-world collections (young, mixed, or full) require a safepoint to pause all threads. Even ZGC, which performs most of its work concurrently, still has brief stop-the-world pauses (such as mark start and relocate start) that depend on safepoints.

Why:

A safepoint is a point where the JVM can assume every application thread is in a known, stable state, so it can safely inspect or change the heap. The garbage collector and other VM operations need this consistent snapshot to traverse live objects, enumerate roots, and relocate or free memory. In JIT-compiled code, safepoint polls are placed only at specific spots such as method returns and loop back-edges, so a thread in compiled code stops only at those polls rather than at any instruction. Stop-the-world collections require all threads to reach a safepoint, and even ZGC needs brief safepoint pauses for its mark start and relocate start phases despite doing most work concurrently.

In Java 8, HotSpot replaced the Permanent Generation (PermGen) with Metaspace for storing class metadata. Describe where Metaspace is allocated compared with PermGen, and what problem this change mainly addresses. Be specific about the memory region.#

Show answer

Metaspace is allocated in native (off-heap) memory obtained from the operating system, while PermGen was a fixed-size region inside the Java heap. Because Metaspace is not bounded by a pre-set heap partition, it can grow dynamically, limited only by -XX:MaxMetaspaceSize (unlimited by default). This removes the OutOfMemoryError: PermGen space failures that occurred when frameworks or runtime code generators (such as Spring AOP proxies, Groovy scripts, or bytecode-manipulation libraries) loaded many classes beyond a fixed PermGen size.

Why:

PermGen lived in a fixed-size region inside the Java heap, so the number of classes an application could hold was capped by a size set at startup. Metaspace stores class metadata in native memory outside the Java heap, obtained from the operating system, and grows on demand up to -XX:MaxMetaspaceSize, which is unlimited by default. The change therefore mainly targets the OutOfMemoryError: PermGen space failures seen when frameworks generated many classes at runtime.

In the HotSpot JVM, escape analysis is a JIT compiler optimization. Briefly explain what escape analysis determines about an object and which two optimizations it can enable for objects that do not escape.#

Show answer

Escape analysis determines whether an object allocated within a method can be accessed outside that method or by another thread. If the object does not escape, the HotSpot JIT can apply scalar replacement, breaking the object into its individual fields and placing them on the stack or in registers instead of allocating on the heap, and lock elision, which removes synchronized locks on objects that cannot be shared between threads.

Why:

Escape analysis is performed by the HotSpot JIT compiler (the C2 compiler). It tracks whether a reference to a newly allocated object can leave the allocating method or be seen by another thread. If the object is classified as 'no escape' (or 'scalar replaceable'), the JIT can avoid heap allocation via scalar replacement and can also elide synchronization (lock elision) since no other thread can ever observe the object. These two optimizations, scalar replacement and lock elision, are the primary concrete benefits the HotSpot JVM derives from escape analysis.

Consider a HotSpot JVM (Java 17 LTS) running with default tiered compilation (C1 and C2 enabled). During profiling, a virtual call site dispatching through the interface MyInterface always received objects of type ConcreteA, the only implementor of MyInterface loaded at that time. C2 speculatively compiled the call site as a monomorphic dispatch: a receiver-type guard followed by a direct call to ConcreteA.compute(). C2 recorded a dependency asserting that ConcreteA is the sole concrete implementor of compute(). No uncommon trap has fired at this call site yet. Later, a second class ConcreteB, also implementing MyInterface, is loaded via Class.forName(). Which option best describes the JVM's response to this class-loading event?#

Options

Show answer

When HotSpot loads a second implementor of an interface whose compiled call site assumed a single implementor, it invalidates the dependent compiled code and deoptimizes it, so execution falls back to the interpreter and the method can be recompiled with updated profile data. Loading the class does not throw an IncompatibleClassChangeError, and the existing machine code is not patched in place.

Why:

C2 compiled the call site with a monomorphic receiver guard and a direct call, and it recorded a dependency that ConcreteA is the only concrete implementor of compute(). Loading ConcreteB violates that dependency, so the JVM marks the dependent nmethod as not entrant and deoptimizes it, converting active frames back to interpreter frames. Execution continues in the interpreter, which re-profiles the site, and the method may later be recompiled with both receiver types observed. Loading a second legal implementor does not throw an IncompatibleClassChangeError, and the JVM does not rewrite the inline cache of compiled code. The option claiming the existing guard keeps the code valid is wrong because the guard is not the mechanism that protects the dependency: a ConcreteB receiver would fail the guard and hit an uncommon trap, and the dependency check is what forces invalidation.

Related interview questions

Job market

See java salaries and hiring demand from live job postings.

The other 6 questions

This page shows 25 and marks what you pick. That's as far as a page can go. A free account opens the other 6 and keeps every answer. What you miss comes back until it's right: after a day, then at longer gaps.

Start with this topic

Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan

Interview booked? Practise the job ad instead

What moved, monthly

One email a month when the bulletin comes out: what moved in the markets we track, and the new question topics we published. Confirm your address to join. Unsubscribe any time.