面试官第一句话往往不是“自我介绍”,而是“讲讲你理解的Java平台无关性”。很多候选人脱口而出“一次编译,到处运行”,然后就没有然后了。这恰恰暴露了应试者只背了结论、没有构建知识体系的问题。真正的平台无关性,是一套从源码到字节码、再到JVM的完整链路:.java文件编译成.class字节码,字节码不面向任何具体操作系统,而是面向JVM规范。不同的操作系统安装对应的JVM,由JVM将字节码解释或JIT编译为本地机器码。面试官追问“Java为什么慢”,如果你能提到HotSpot的JIT会热点代码编译为机器码,而C2编译器甚至能做激进优化,这就说明你理解的是“运行时适应”,而非教科书上的死话。
继续深挖基础,String是高频考点中的钉子户。别只背“不可变”三个字,要理解不可变带来的安全性和字符串常量池的复用机制。面试官喜欢问String a = "a" + "b"和new String("ab")的区别,更进阶的是问StringBuilder在循环拼接中的必要性。真正拉开差距的答案,是说出“字符串拼接的优化是javac编译期的常量折叠,而循环内的拼接会在字节码层反复创建StringBuilder”。如果还能补一句“在JDK9之后,String内部存储从char[]换成了byte[],用coder字段区分Latin1和UTF-16”,面试官的眼神会不一样。
集合框架:HashMap是半个Java面试的缩影
HashMap这一关过不了,后面对话基本没法进行。先问自己:能默写出put方法的完整流程吗?从计算hash、定位桶、插入到红黑树转化、扩容,每一步都有对应的问题。例如hash函数为什么是(h = key.hashCode()) ^ (h >>> 16)?因为高16位与低16位异或,让高位信息参与低位运算,减少哈希碰撞。为什么加载因子是0.75?这是空间和时间成本的折中,泊松分布下,桶内链表长度达到8的概率极低,所以阈值定为8。HashMap的最核心思想不是“数组+链表”,而是“根据hash散列尽可能让数据均匀分布,以空间换时间”。如果记不住所有源码,至少要把“为什么树化阈值是8”和“为什么容量总是2的幂”讲清楚,这两个点是判断是否真读过源码的试金石。
ConcurrentHashMap是进阶版。JDK7的Segment分段锁与JDK8的CAS+synchronized,你都应该能画图。JDK8抛弃了Segments,直接用Node数组,put时通过CAS尝试插入空桶,失败则synchronized锁住桶头节点。这里有一个值得思考的点:为什么JDK8在并发度上比JDK7更高?因为锁粒度从Segment(每段包含多个桶)细化为单个链表/红黑树的头节点。你还可以引申到扩容时的迁移机制:sizeCtl负值表示扩容状态,多线程协助迁移数据。说出“扩容时通过ForwardingNode标记已迁移的桶”就足以证明你研究过并发扩容的实现细节。
并发编程:从volatile到AQS的思维跃迁
并发是Java面试的分水岭。volatile两个字背后是JMM(Java内存模型)、可见性、有序性和内存屏障。面试官爱问“volatile能保证原子性吗”,答案是不能,但更精彩的回答是解释为什么不能:i++是读、加、写三步,volatile只保证每次读都从主内存取最新值,但三步中间可能有其他线程穿插。真正理解volatile的人,会知道JMM的happens-before规则中有一条“对volatile变量的写,先行发生于之后对它的读”,由此推导出它适用于一写多读的状态标志。如果还能说出“JSR-133规范重新定义了volatile,禁止了与普通变量之间的重排序”,那就是加分级表现。
synchronized是另一个核心。锁升级过程(无锁→偏向锁→轻量级锁→重量级锁)几乎是必问。但很多候选人只背答案,说不清为什么需要偏向锁:因为大多数对象的锁在生命周期内只被一个线程访问,偏向锁可以减少无竞争时的CAS开销。轻量级锁则是在无竞争情况下用CAS替代互斥。在JDK15之后,偏向锁被废弃,如果你能提到这一点,会显得知识更新及时。并发编程的终极口诀是:锁不是性能问题的根源,竞争的频率才是。所以读多写少的场景用ReadWriteLock,高并发短操作用synchronized(JVM已经优化得很好了),高吞吐场景考虑LongAdder这种分段累加器。
AQS几乎是Java并发包的地基。ReentrantLock、Semaphore、CountDownLatch、ThreadPoolExecutor的Worker都基于AQS。你需要能说出AQS的核心:一个volatile int state+ 一个CLH变体队列。获取锁失败时,线程被封装成Node入队,通过LockSupport.park挂起;释放锁时,唤醒后继节点。“公平锁和非公平锁的区别在于:非公平锁在获取时先尝试一次CAS抢锁,抢不到才排队。”如果你能画出AQS的队列结构,并解释为什么用双向队列——因为需要方便地取消等待和唤醒后继,那就已经从使用层面上升到设计层面。
线程池是实战高频问题。ThreadPoolExecutor的七大参数都知道,但面试官真正想听的是你如何配置。核心线程数怎么定?CPU密集型设置为CPU核数+1,IO密集型通常设置为2 CPU核数,但更科学的说法是“线程等待时间与计算时间的比值决定了最佳线程数”。拒绝策略不是随便选的,CallerRunsPolicy用调用者线程执行任务,能起到背压效果;AbortPolicy会抛出异常,适合及时暴露问题。还要能解释为什么不能用Executors.newFixedThreadPool,因为LinkedBlockingQueue无界,任务堆积可能导致OOM。
JVM:内存与垃圾回收是检验真知的标尺
JVM的问题逃不掉,但很多人只会背几块内存区域。要分层理解:线程私有(程序计数器、虚拟机栈、本地方法栈)和线程共享(堆、方法区)。JDK8把方法区移出,变为元空间(Metaspace),并直接使用本地内存。这一改动的深层原因是永久代经常OOM,而元空间默认大小受本地内存限制,且字符串常量池和类元数据的存储分离。“栈管运行,堆管存储”这种话太肤浅,更准确的是“栈帧记录方法调用的状态,堆存对象的实例数据,而对象类型信息放在元空间”。
垃圾回收算法中,复制、标记清除、标记整理要会对比。分代收集理论怎么用?新生代用复制(对象存活率低),老年代用标记-清除或标记-整理(对象存活率高)。GC Roots有哪些?虚拟机栈中引用的对象、静态属性引用的对象、常量引用的对象、JNI引用等。要理解“可达性分析”不是扫描所有对象,而是从GC Roots出发做图遍历,因此判断对象是否可以回收,只看它是否被活动对象引用。
接下来是垃圾收集器。从Serial到G1到ZGC,要能说出适用场景。G1现在仍是主流,它把堆分成Region,维护可预测的停顿时间模型。G1最革命的设计是“标记清理”可以和用户线程并发执行,并且用Remembered Set来追踪跨Region引用。如果你想展示深度,可以提一下ZGC通过染色指针和读屏障实现几乎零停顿,突破了几十TB堆大小的限制。JVM调优问题不要只背参数,要结合案例:-Xms和-Xmx设为相同值避免扩容抖动,-XX:+HeapDumpOnOutOfMemoryError导出堆快照,然后用MAT分析。调优的本质不是把参数调得花里胡哨,而是减少Full GC的频率和单次时间。
类加载机制里,“双亲委派”必考。为什么需要双亲委派?为了安全——防止自定义类覆盖java.lang.String。破坏双亲委派的例子:Tomcat为了加载不同应用的同名类,JDBC的SPI通过Thread.currentThread().getContextClassLoader()加载驱动实现。自己能不能写一个和String同包名的类?不能,因为引导类加载器优先加载rt.jar里的类。类加载的“可见性”规则是:父加载器看不到子加载器加载的类,而子加载器可以看到父加载器加载的类。
Spring框架:IOC和AOP只是起手式
Spring面试题已经从“IOC是什么”变成了“Spring是如何解决循环依赖的”。这个问题几乎等于送分题,但拿满分的少。标准答案是:单例的setter注入循环依赖,通过三级缓存解决——一级缓存放成品对象,二级缓存放半成品对象(提前暴露引用),三级缓存放ObjectFactory(生成代理对象的工厂)。关键点在于:三级缓存的存在是为了支持AOP代理——如果对象被代理,那么其他依赖它的bean拿到的是代理对象,而不是原始对象。如果面试官问“构造函数注入的循环依赖能解决吗”,不能,因为构造函数执行时对象还没实例化,没地方提前暴露。
AOP的考点则集中在动态代理:JDK动态代理要求接口,CGLIB通过继承实现。Spring默认如果目标类有接口就用JDK代理,否则用CGLIB。更深刻的理解是AOP的底层不是反射那么简单,而是代理链的组装:通过Advisor匹配切点,把多个增强器包装成拦截器链,递归调用时依次执行前置、后置、环绕通知。事务管理也是AOP的实战应用,@Transactional失效的几种情况必须牢记:方法内部自调用、private方法、异常被catch、传播行为设置错误等。
Spring事务的传播行为,七种中最重要的REQUIRED和REQUIRES_NEW。前者支持当前事务,没有则新建,通常默认;后者挂起当前事务,开启新事务,用于日志记录或异步操作。事务失效的本质是“事务管理依赖代理,而代理只能在通过bean引用调用时生效”。所以自调用this.method()不走代理,事务自然就没效果。如果面试官问“一个大事务里调用了另一个方法,想让这个方法回滚不影响外层”,答案就是REQUIRES_NEW开一个新事务,让异常被捕获后外层按需提交。
MySQL:索引和事务是数据库的两座大山
MySQL面试,索引模型是首选。InnoDB的B+Tree为什么好?因为树的高度矮,三层就能存千万级数据;叶子节点用双向链表串联,范围查询时不需要回溯树结构。至于为什么不选B-Tree:B-Tree的节点存数据,导致单节点能存的键值少,树变高,且范围查询需要中序遍历。B+Tree的所有数据都在叶子节点,非叶子节点只存键,这样磁盘I/O次数少,且天然支持排序和范围查询。还要会看执行计划:explain中的key、rows、type,type从ref到index到all性能递减。
索引失效的常见场景:对索引列使用函数、隐式类型转换、最左前缀不满足、%开头的模糊查询。但最深的坑是“优化器可能因为数据分布而选择全表扫描,即使有索引”。索引不是越多越好,每个索引都是一棵B+Tree,写入时就要维护多棵树,牺牲写性能。联合索引的建立原则是“区分度高、查询频繁、最左匹配”。如果面试官问“为什么order by慢”,可以提到Using filesort,如果排序字段正好是联合索引的后续列,可以避免排序。
事务隔离级别是必考。四个级别:读未提交、读已提交、可重复读、串行化。MySQL默认是可重复读,为什么默认这个?因为早期基于binlog的复制在read committed下会出现主从数据不一致?不对,MySQL在read committed下也可以正常工作。更合理的解释是:MySQL的binlog格式是statement时,在read committed下无法正确复制,因为READ COMMITTED允许“间隙锁”释放,而statement日志回放时会出现问题。但到了row格式后,可重复读的默认选择更多是历史惯性。MVCC是实现“快照读”的核心:每行记录隐藏了两个隐藏列——事务ID和回滚指针,通过undo log构建回滚链,实现不同事务看到不同版本。可重复读在第一次读时生成快照,之后一直读这个快照;读已提交是每次读都生成新快照。至于当前读,需要用lock in share mode或for update,走的是锁机制,不是MVCC。
锁问题:表锁、行锁、间隙锁、临键锁。Next-Key Lock是间隙锁和记录锁的结合,前开后闭区间,用于解决幻读。在可重复读级别下,MySQL通过Next-Key Lock锁住查询范围,防止其他事务插入新记录,从而保证当前读的一致性。如果范围查询的记录不存在,会退化为纯间隙锁。还有死锁的排查:show engine innodb status查看最近死锁信息,分析两个事务持有哪些锁、等待哪些锁,通常通过调整事务顺序或缩小锁范围解决。
Redis:缓存三兄弟和分布式锁
Redis在面试中几乎和Java一样重要。首先是缓存穿透、击穿、雪崩三兄弟。穿透:请求的数据在缓存和数据源中都不存在,用“布隆过滤器”或“缓存空对象”解决。击穿:某个热点key过期瞬间大量请求打到DB,用“互斥锁”或“热点key永不过期+逻辑过期”解决。雪崩:多个key同时过期或Redis宕机,用“过期时间加随机值”、“集群高可用”、“限流降级”解决。这三件事拷问的不是你记住了多少方案,而是你是否理解“缓存是挡在DB前面的第一道防线,防不住就要保护DB”。
Redis持久化:RDB和AOF。RDB是某一时刻的内存快照,适合备份和重启加载;AOF记录写命令,文件大、恢复慢,但安全性高。混合持久化:AOF重写时把RDB内容作为AOF文件开头,这样加载快且丢失最少。面试官喜欢问“RDB持久化时还能处理写命令吗”,答案是能的,因为Redis通过fork子进程生成快照,父进程继续处理命令,写时复制(COW)保证数据一致性。如果还能提到appendfsync的everysec是默认配置,每秒刷盘,最多丢一秒数据,就很完善了。
分布式锁是应用层高频题。简单的setnx命令容易踩坑:要设置过期时间,要用set key value nx ex 3000原子命令。释放锁时要先验证value是否是自己设的,用Lua脚本保证“检查+删除”的原子性。更健壮的方案是Redisson的看门狗机制,自动续期。真正的资深回答会指出:Redis分布式锁不是银弹,在主从切换下可能丢失锁,如果必须强一致,要用Redlock或ZooKeeper锁。但Redlock本身也有争议,马丁·克莱普曼专门批判过它。能说出这个争议,说明你读过分布式系统的经典讨论。
面试不是背诵,而是用体系串联碎片
把上面这些知识点串起来看,Java面试其实考察的是“三条线”:第一条线是语言基础,从集合到并发到JVM,反映你写代码时知不知道底层在发生什么;第二条线是框架,从Spring到数据库到缓存,反映你构建系统时能不能理清组件之间的协作;第三条线是“为什么”,每个高频问题背后都有设计权衡,比如HashMap为什么用红黑树而不是AVL树?因为红黑树插入删除的旋转次数更少。Spring为什么用三级缓存?因为一级放不下代理对象,二级解决不了循环引用。MySQL为什么默认可重复读?因为早期主从复制要保证一致性。面试官想看到的不是你背诵的答案,而是你在脑海里构建的一张知识图谱,每个节点都有连接。
所以准备面试时,不要一条一条背八股文。试着从一道题切入,比如“HashMap为什么线程不安全”,你会引出数据覆盖、死循环、ConcurrentHashMap,然后到CAS到锁,到AQS,到线程池,到JUC工具类,再到Spring的单例池循环依赖……一路追问下去,你会把所有高频考点串成一张网。把知识织成网,而不是堆成山,这才是应对Java面试的最优策略。祝你拿下心仪offer。