Explain the JVM memory structure: heap, stack and metaspace.
The heap, shared by all threads, holds objects and arrays and is garbage collected. Each thread's stack holds one frame per call with locals and references; recursion that goes too deep gives
StackOverflowError. Metaspace (Java 8+, replacing PermGen) holds class metadata in native memory.In
Person p = new Person(),pis on the stack and the object on the heap. Flags:-Xmx,-Xss,-XX:MaxMetaspaceSize.How does garbage collection work in Java? Which collectors do you know?
The GC frees objects no longer reachable from roots such as thread stacks and static fields, so cycles are collected too. Most collectors are generational: most objects die young, so the young generation is collected often and survivors move to the old one.
G1 is the default since Java 9 and aims at a pause-time goal; through JDK 26 the JVM picks Serial instead on one CPU or under 1792 MB of memory, a rule JEP 523 drops in JDK 27. Parallel favors throughput. ZGC and Shenandoah (left out of Oracle's builds) keep pauses very low on large heaps; ZGC gained a generational mode in Java 21, the only mode since Java 24.
System.gc()is only a hint.Can Java have memory leaks? Give examples.
Yes. The GC frees only unreachable objects, so a leak is an object you no longer need that is still referenced.
Typical causes are static caches without eviction, listeners never removed,
ThreadLocalvalues in pooled threads never removed, and keys whosehashCode()changes. Find it with a heap dump (jcmd <pid> GC.heap_dump) and its dominator tree in a tool like Eclipse MAT.What happens if you change a
HashMapkey afterput?Javaimport java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class Main { public static void main(String[] args) { List<Integer> key = new ArrayList<>(List.of(1)); Map<List<Integer>, String> map = new HashMap<>(); map.put(key, "v"); key.add(2); System.out.println(map.get(key) + " " + map.containsKey(List.of(1))); } }It prints
null false. The entry sits where its original hash put it. Afterkey.add(2)the hash changes, soget(key)looks elsewhere;containsKey(List.of(1))finds the entry, butequalscompares[1]with[1, 2].The entry is lost but still takes memory, so map keys must not change after insertion.
How do you write a thread-safe singleton in Java?
Use an enum or the holder idiom; both rely on the JVM initializing a class once, thread safely.
Javaenum Config { INSTANCE; private final String env = "prod"; String env() { return env; } } class Registry { private Registry() { System.out.println("Registry created"); } private static class Holder { // initialized on first getInstance() static final Registry INSTANCE = new Registry(); } static Registry getInstance() { return Holder.INSTANCE; } } public class Main { public static void main(String[] args) { System.out.println(Config.INSTANCE.env()); System.out.println(Registry.getInstance() == Registry.getInstance()); } }It prints
prod,Registry createdonce, andtrue. With double-checked locking the field must bevolatile, or another thread can see a half-built object.Which features from Java 11 to 21 do you use? Show records, sealed classes and pattern matching.
var(10), switch expressions (14), text blocks (15), records andinstanceofpatterns (16), sealed classes (17), and pattern matching forswitchwith record patterns (21). LTS releases: 8, 11, 17, 21 and 25.Javasealed interface Shape permits Circle, Rect {} record Circle(double r) implements Shape {} record Rect(double w, double h) implements Shape {} public class Main { static double area(Shape s) { return switch (s) { // exhaustive: no default needed case Circle c -> Math.PI * c.r() * c.r(); case Rect(double w, double h) -> w * h; // record pattern }; } public static void main(String[] args) { var shapes = java.util.List.of(new Circle(1), new Rect(2, 3)); for (Shape s : shapes) { System.out.printf("%s -> %.2f%n", s, area(s)); } } }It prints
Circle[r=1.0] -> 3.14andRect[w=2.0, h=3.0] -> 6.00. BecauseShapeis sealed, the switch needs nodefault, and adding a subtype makes every non-exhaustiveswitcha compile error.What is type erasure in Java generics?
Generic type arguments exist only at compile time: the compiler checks them, inserts casts and erases them, so
List<String>andList<Integer>share one runtime class.Javaimport java.util.ArrayList; import java.util.List; public class Main { static double sum(List<? extends Number> nums) { // producer: extends double total = 0; for (Number n : nums) total += n.doubleValue(); return total; } public static void main(String[] args) { List<String> a = new ArrayList<>(); List<Integer> b = new ArrayList<>(); System.out.println(a.getClass() == b.getClass()); System.out.println(sum(List.of(1, 2.5, 3L))); } }It prints
trueand6.5. Hence nonew T(), noinstanceof List<String>and no generic arrays. Wildcards follow PECS: producerextends, consumersuper. More in generics.How does class loading work in Java?
Classes load lazily through a chain of loaders using parent delegation, each asking its parent first: bootstrap (core modules such as
java.base), platform (other modules such asjava.sql) and application (your classpath). That is why you cannot replacejava.lang.String.A class is a separate type per loader, so the same class loaded by two loaders gives
ClassCastException: X cannot be cast to Xin app servers.What is the difference between
synchronizedand ReentrantLock?Both give mutual exclusion, visibility and reentrancy.
synchronizedreleases the lock automatically;ReentrantLockneedsunlock()in afinallyblock but addstryLockwith a timeout, interruptible waits, fairness and severalConditions.Use
synchronizedby default, andReadWriteLockorStampedLockfor read-heavy data.How would you design a simple LRU cache in Java?
Extend
LinkedHashMapin access order and overrideremoveEldestEntry;getandputstay O(1).Javaimport java.util.LinkedHashMap; import java.util.Map; class LruCache<K, V> extends LinkedHashMap<K, V> { private final int capacity; LruCache(int capacity) { super(16, 0.75f, true); // true = access order this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; } } public class Main { public static void main(String[] args) { LruCache<String, Integer> cache = new LruCache<>(2); cache.put("a", 1); cache.put("b", 2); cache.get("a"); // a is now most recently used cache.put("c", 3); // evicts b System.out.println(cache.keySet()); } }It prints
[a, c]. WithoutLinkedHashMap, use aHashMapof keys to nodes plus a doubly linked list: move a node to the head on access and drop the tail on overflow.
Advanced Java Interview Questions
JVM memory, garbage collection, leaks, generics, class loading, locking and design questions.