Java Backend Interview: The 15 Questions That Come Up
JVM memory model, GC algorithms, concurrency primitives, collections and Spring — the questions asked in nearly every round, with the answer depth interviewers want.
JVM & Garbage Collection
Q1: Describe the JVM runtime memory areas.
- Heap: objects (young gen Eden + 2 survivors, old gen)
- Metaspace: class metadata, method constants (replaced pre-8 PermGen)
- Stack: per-thread frames, local variables, operand stack
- Native stack + PC register: JNI calls and per-thread instruction pointer
Q2: How does GC find garbage?
Root set (GC Roots) + reachability traversal: objects unreachable from roots (stack locals, statics, active thread references, JNI handles) are garbage. Reference counting fails on cycles — the classic demonstration is two objects referencing each other but nothing else.
Q3: CMS vs G1 vs ZGC.
| Collector | Pause feel | Choose when |
|---|---|---|
| Parallel/ParallelOld | 全堆 STW,吞吐高 | 批处理、无低延迟要求 |
| CMS | 老年代并发,碎片多,已废弃(JDK14) | 历史项目兼容 |
| G1 | Region 化、可预测停顿目标(-XX:MaxGCPauseMillis) | 堆 6GB 以上,JDK9+ 默认 |
| ZGC/Shenandoah | 亚毫秒级停顿,堆到 TB | 低延迟服务,JDK15+(ZGC) |
Concurrency
Q4: volatile — what two things does it guarantee?
Visibility (writes flush to main memory, reads invalidate cache) and happens-before ordering; NOT atomicity (i++ still races). DCL singleton needs volatile because of instruction reordering: an object can be 'published' before its constructor finishes.
public class Singleton {
private static volatile Singleton instance; // ← volatile 不可省
public static Singleton get() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) instance = new Singleton();
// 分配内存 → 调构造 → 赋引用,重排后可能先赋引用
}
}
return instance;
}
}
Q5: synchronized vs ReentrantLock vs CAS/atomic.
| Tool | Unlock condition | Interruptible? |
|---|---|---|
| synchronized | 自动(异常/退出) | 否(不响应 interrupt) |
| ReentrantLock | 必须 unlock()(finally!) | 是,支持 tryLock 超时 |
| AtomicXxx / CAS | 无锁,竞争失败自旋 | 无阻塞,适合低-中竞争 |
Q6: Thread pool — what happens when you submit to a full pool?
ThreadPoolExecutor pool = new ThreadPoolExecutor(
8, 32,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2000), // 有界!
new ThreadFactoryBuilder().setNameFormat("order-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()); // 队列满→让提交者自己跑(反压)
Collections & Spring
Q7: HashMap internals after Java 8.
Array of buckets + linked lists that convert to red-black trees when a bucket exceeds 8 entries (and shrink back below 6). Capacity is always a power of two so hash % capacity is a cheap bit mask. Not thread-safe — concurrent put can lose data or resize-loop; the JDK8 resize fixes the JDK7 infinite-loop bug but put still races.
Q8: Spring Bean lifecycle in one breath.
Instantiate → populate properties → BeanNameAware/BeanFactoryAware/ApplicationContextAware → BeanPostProcessor.before → @PostConstruct / InitializingBean.afterPropertiesSet / init-method → BeanPostProcessor.after (AOP proxy wraps here) → ready → (destroy) @PreDestroy / DisposableBean.destroy.
Q9: How does @Transactional actually fail?
- Self-invocation: same-class call bypasses the proxy (no proxy in the call chain)
- Method not public: JDK proxies only intercept public methods
- Exception swallowed: catch it without rethrowing → no rollback
- Checked exception by default does NOT roll back — specify rollbackFor = Exception.class
JVM 与垃圾回收
Q1:描述 JVM 运行时内存区域。
- 堆:对象(新生代 Eden + 2 个 Survivor + 老年代)
- 元空间:类元数据、方法常量(8 以前叫永久带 PermGen)
- 虚拟机栈:每线程栈帧、局部变量、操作数栈
- 本地方法栈 + 程序计数器:JNI 调用与线程指令位置
Q2:GC 怎么判断对象是垃圾?
从 GC Roots(栈局部变量、静态引用、活跃线程、JNI 句柄)做可达性遍历,不可达的就是垃圾。引用计数法处理不了循环引用——经典反例就是两个对象互相引用、但外界谁也不引用它们。
Q3:CMS、G1、ZGC 的区别。
| 收集器 | 停顿特点 | 什么时候选 |
|---|---|---|
| Parallel/ParallelOld | 全堆 STW,吞吐高 | 批处理、无低延迟要求 |
| CMS | 老年代并发,碎片多,已废弃(JDK14) | 历史项目兼容 |
| G1 | Region 化、可预测停顿目标(-XX:MaxGCPauseMillis) | 堆 6GB 以上,JDK9+ 默认 |
| ZGC/Shenandoah | 亚毫秒级停顿,堆到 TB | 低延迟服务,JDK15+(ZGC) |
并发
Q4:volatile 保证哪两点?
可见性(写立即刷主存、读令缓存失效)和 happens-before 有序性;不保证原子性(i++ 仍然竞态)。双重检查单例要 volatile 就是因为指令重排:对象可能在构造函数执行完之前就被「发布」。
public class Singleton {
private static volatile Singleton instance; // ← volatile 不可省
public static Singleton get() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) instance = new Singleton();
// 分配内存 → 调构造 → 赋引用,重排后可能先赋引用
}
}
return instance;
}
}
Q5:synchronized、ReentrantLock、原子类的取舍。
| 工具 | 释放条件 | 可中断? |
|---|---|---|
| synchronized | 自动(异常/退出) | 否(不响应 interrupt) |
| ReentrantLock | 必须 unlock()(finally!) | 是,支持 tryLock 超时 |
| AtomicXxx / CAS | 无锁,竞争失败自旋 | 无阻塞,适合低-中竞争 |
Q6:线程池满了再提交任务会怎样?
ThreadPoolExecutor pool = new ThreadPoolExecutor(
8, 32,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2000), // 有界!
new ThreadFactoryBuilder().setNameFormat("order-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()); // 队列满→让提交者自己跑(反压)
集合与 Spring
Q7:JDK8 之后的 HashMap 原理。
桶数组 + 链表,桶超过 8 个元素转红黑树(降到 6 退回链表)。容量恒为 2 的幂,取模可以变成位运算。它不是线程安全的——并发 put 会丢数据;JDK8 扩容修复了 JDK7 的死循环,但 put 仍有竞态。
Q8:一口气说出 Bean 生命周期。
实例化 → 属性注入 → Aware 回调 → BeanPostProcessor 前置 → @PostConstruct / InitializingBean / init-method → BeanPostProcessor 后置(AOP 代理在此包) → 可用 → 销毁回调。
Q9:@Transactional 在什么情况下会失效?
- 同类内部方法调用:走不到代理对象,注解失效
- 方法不是 public:JDK 代理只拦截 public 方法
- 异常被吞:catch 住不重抛 → 不回滚
- 默认只回滚 RuntimeException,检查异常需指定 rollbackFor = Exception.class