Menu
CoddyTech

Advanced Java Interview Questions

JVM memory, garbage collection, leaks, generics, class loading, locking and design questions.

10 questions
  1. 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(), p is on the stack and the object on the heap. Flags: -Xmx, -Xss, -XX:MaxMetaspaceSize.

  2. 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.

  3. 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, ThreadLocal values in pooled threads never removed, and keys whose hashCode() changes. Find it with a heap dump (jcmd <pid> GC.heap_dump) and its dominator tree in a tool like Eclipse MAT.

  4. What happens if you change a HashMap key after put?

    Java
    import 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. After key.add(2) the hash changes, so get(key) looks elsewhere; containsKey(List.of(1)) finds the entry, but equals compares [1] with [1, 2].

    The entry is lost but still takes memory, so map keys must not change after insertion.

  5. 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.

    Java
    enum 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 created once, and true. With double-checked locking the field must be volatile, or another thread can see a half-built object.

  6. 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 and instanceof patterns (16), sealed classes (17), and pattern matching for switch with record patterns (21). LTS releases: 8, 11, 17, 21 and 25.

    Java
    sealed 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.14 and Rect[w=2.0, h=3.0] -> 6.00. Because Shape is sealed, the switch needs no default, and adding a subtype makes every non-exhaustive switch a compile error.

  7. 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> and List<Integer> share one runtime class.

    Java
    import 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 true and 6.5. Hence no new T(), no instanceof List<String> and no generic arrays. Wildcards follow PECS: producer extends, consumer super. More in generics.

  8. 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 as java.sql) and application (your classpath). That is why you cannot replace java.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 X in app servers.

  9. What is the difference between synchronized and ReentrantLock?

    Both give mutual exclusion, visibility and reentrancy. synchronized releases the lock automatically; ReentrantLock needs unlock() in a finally block but adds tryLock with a timeout, interruptible waits, fairness and several Conditions.

    Use synchronized by default, and ReadWriteLock or StampedLock for read-heavy data.

  10. How would you design a simple LRU cache in Java?

    Extend LinkedHashMap in access order and override removeEldestEntry; get and put stay O(1).

    Java
    import 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]. Without LinkedHashMap, use a HashMap of keys to nodes plus a doubly linked list: move a node to the head on access and drop the tail on overflow.

Coddy programming languages illustration

Learn to code with Coddy

Get started