
1. 别急着背题先搞清楚面试官到底在问什么这两年Java面试行情大家都懂投简历前先打开招聘网站看一看十条JD里有八条写着“熟悉Java基础、掌握常用框架、了解分布式中间件”。我面试过不少候选人也帮朋友做过模拟面试发现一个很普遍的现象很多人把题库背得滚瓜烂熟什么HashMap原理、Redis持久化策略、JVM内存模型张口就来但一问到“你为什么这么设计”“这个参数在什么场景下要调”瞬间卡壳。先说结论Java面试题学习的正确姿势不是背题而是透过题目理解背后的知识点体系再用能落地的方式把知识串起来。本文不打算给你列一份“1000道大厂真题”那种流水账而是从实际面试的高频考点出发把每个模块的核心原理、常见坑点、回答思路和排查经验拆开揉碎让你看完之后不仅知道“怎么答”更知道“为什么这么答”。这套内容适合三类人正在准备校招或社招的Java开发工程师想系统梳理自己知识盲区的在职人员以及平时写业务代码居多、对底层原理和面试常见套路不太熟悉的同学。接下来我会按Java基础、并发编程、JVM、数据库、缓存与分布式、框架源码、算法与工具这条主线来展开每一部分都会给出练习题、答题思路和实操经验。2. Java核心基础不是背概念而是看你的代码嗅觉2.1 String、Integer、泛型这些“老朋友”其实全是坑我先说一道出现频率极高的入门题String s1 abc和String s2 new String(abc)有什么区别很多新手能答出“前者在常量池后者在堆”但再往下问就卡住了那s1 s2为什么是falseString的intern()方法到底做了什么为什么字符串拼接要用StringBuilder而不是直接加号这道题真正想考察的是你对对象创建机制、引用比较、JVM内存分布的综合理解。我的建议是别只记结论把下面这段代码自己跑一遍观察输出String s1 abc; String s2 abc; String s3 new String(abc); System.out.println(s1 s2); // true常量池复用 System.out.println(s1 s3); // false堆对象地址不同 System.out.println(s1 s3.intern()); // trueintern返回常量池引用 Integer a 127; Integer b 127; Integer c 128; Integer d 128; System.out.println(a b); // true-128到127走缓存 System.out.println(c d); // false超出缓存范围创建新对象Integer这个例子更绝很多工作两三年的同学都会踩坑。-128到127之间的Integer对象会走IntegerCache缓存所以用比较是同一个对象超出这个范围就各自new必须用equals比。我平时看代码评审只要看到包装类型用了基本都会要求改成equals这已经成了团队约定俗成的规矩。泛型这块面试常见的是“Java泛型是编译期还是运行期生效”答案很明确编译期类型擦除运行期没有泛型信息。所以List 和List 在运行时都是List它们无法用getClass区分。这也是为什么通过反射往List 里塞Integer不会报错的原因。我建议把泛型的边界、通配符? extends和? super的区别也理清楚面试官很喜欢在这里设个连环问。2.2 集合框架HashMap的底层结构与扩容时机HashMap是Java面试的“必考题”从校招问到P7年年不落。但很多人的理解还停留在“数组加链表”这个层面。现在主流JDK版本里HashMap的底层是数组加链表加红黑树当链表长度超过8且数组长度大于等于64时链表会转红黑树目的是把查询时间复杂度从O(n)降到O(logn)。为什么是8这是JDK作者根据泊松分布算出来的概率值正常情况下链表长度到8的概率已经非常低转树是为了防止极端hash冲突。扩容机制也要说清楚默认初始容量16负载因子0.75当元素数量超过容量乘以负载因子时触发扩容每次扩容翻倍。这里有个关键细节JDK 8之后的扩容元素要么留在原位置要么移动到“原位置加旧容量”的新位置因为数组长度是2的幂重新计算hash时只是多了一位参与运算这样设计既高效又避免了死循环问题。老生常谈的JDK 7并发扩容死循环问题就是用头插法加多线程并发导致的JDK 8改成尾插法后这个问题就没了。我建议准备HashMap时顺带把这几个问题一起想明白为什么负载因子是0.75而不是0.5或1为什么容量一定要是2的幂次为什么key要重写hashCode和equals这几个问题能回答上来面试官基本会认为你是真懂而不是背了八股。3. 并发编程从synchronized到volatile再到线程池参数3.1 synchronized底层原理和锁升级过程并发这一块是Java面试的分水岭。先说synchronized很多人知道它是“内置锁”但不知道它在JDK 6之后经历了锁升级无锁到偏向锁偏向锁到轻量级锁轻量级锁到重量级锁。偏向锁的意思是同一个线程再次获取锁时不需要做CAS操作直接判断线程ID就行当有竞争时偏向锁撤销升级为轻量级锁轻量级锁通过自旋等待持有锁的线程释放自旋超过一定次数或竞争太激烈就升级为重量级锁此时依赖操作系统互斥量线程会进入阻塞状态。面试官经常会问“synchronized和ReentrantLock有什么区别”从功能上说ReentrantLock支持公平锁、支持超时获取锁、支持多个条件变量、可以用tryLock避免死锁。但最核心的区别是synchronized是JVM层面的监视器锁而ReentrantLock是基于AQS的并发组件底层用CAS加volatile实现。从性能上说JDK 6之后两者差距已经很小synchronized的锁升级机制在低竞争场景下反而更省事。我的建议是除非你需要公平锁或可中断获取锁否则优先用synchronized代码更简洁不容易出错。3.2 volatile的可见性与禁止重排序volatile的关键作用就两个保证多线程下的可见性禁止指令重排序。可见性靠的是内存屏障写入volatile变量时会强制把工作内存的修改刷到主内存读取时强制从主内存拉最新值。禁止重排序的最典型应用就是DCL单例模式中的双重检查锁private static volatile Singleton instance; public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; }为什么instance必须要加volatile因为new Singleton()在底层有三步分配内存、调用构造方法初始化、把引用赋值给变量。如果没有volatile编译器和CPU可能把后两步重排序导致其他线程读取到一个“还没初始化完成”的对象。这一步几乎每年面试都会考答好了很加分。注意volatile不能保证原子性。经典的i场景即使变量被volatile修饰多线程下结果依然不对因为i在字节码层面是读、加、写三步期间线程可能被切换。这个点务必主动说清楚否则面试官会认为你对并发模型的理解有漏洞。3.3 线程池参数怎么设拒绝策略怎么选线程池这道题面试官最常用的套路是给我一个场景比如“每秒1000个请求单个任务耗时50ms你来算一下线程池核心参数”。这里有个经典的估算公式当任务以IO操作为主时核心线程数约等于CPU核数乘以2当任务以CPU计算为主时核心线程数约等于CPU核数加1。为什么因为IO密集型的线程大部分时间在等待更多线程能提高吞吐CPU密集型线程多了反而因为上下文切换降低效率。线程池的参数一共有七个核心线程数、最大线程数、空闲线程存活时间、时间单位、阻塞队列、线程工厂、拒绝策略。其中最容易答糊涂的是“任务提交后线程池的执行顺序”。记住这个口诀先占核心线程核心满了放队列队列满了开新线程到最大数最大也满了就走拒绝策略。也就是说不是“核心满了就开新线程”队列这个中间缓冲层是很多人忽略的。拒绝策略有四种AbortPolicy抛异常、CallerRunsPolicy让提交任务的线程自己执行、DiscardPolicy直接丢、DiscardOldestPolicy丢弃队列里最老的任务。实际生产中我比较推荐CallerRunsPolicy它能让执行速度降下来起到自然限流的作用而不是粗暴丢任务。另外绝对不要用Executors.newFixedThreadPool去写生产代码因为它的阻塞队列是无限长的LinkedBlockingQueue任务堆积时会导致内存爆炸这也是阿里Java开发规范中明确禁止的。4. JVM内存分区、垃圾回收和两招排查命令4.1 内存区域划分与对象生死判断JVM考题里最底层的就是内存模型。堆、虚拟机栈、本地方法栈、方法区、程序计数器这五块要能说清楚。堆里再细分年轻代和老年代年轻代又分Eden、Survivor From、Survivor To默认比例8:1:1。对象优先在Eden分配Minor GC之后存活的对象进入Survivor每熬过一次GC年龄加1默认到15就晋升到老年代。大对象直接进老年代这是为了避免在Eden和Survivor之间反复复制。判断对象是否可回收除了引用计数法主流的其实是可达性分析。从GC Roots开始向下搜索搜索不到的就可以回收。GC Roots包括虚拟机栈中引用的对象、静态属性引用的对象、常量引用的对象、JNI引用的对象。我面试时多次被问“循环引用会不会导致内存泄漏”如果你答“引用计数法会”那基本就稳了因为可达性分析天然规避了循环引用问题。4.2 GC算法对比与G1收集器的特点垃圾回收的三大基础算法是标记-清除、标记-复制、标记-整理。标记-清除会产生内存碎片标记-复制用空间换时间适合年轻代标记-整理适合老年代避免碎片又节省空间。到了G1收集器这里它把堆划分成一个个Region同时兼顾年轻代和老年代用可预测的停顿时间模型来控制GC。G1的特点是可以设定最大GC停顿时间比如-XX:MaxGCPauseMillis50它会根据这个目标动态调整每次回收的Region数量。我面试时会问“你线上有没有调过JVM参数”很多候选人答不上来。其实哪怕你没有真正做过大型调优只要你用过jstat、jmap这些命令排查过线上问题就能讲出东西。比如用jstat -gcutil看GC频率发现Full GC频繁再用jmap -dump导出堆转储文件用MAT分析是谁占用了内存。这套排查路径本身就是一个很好的面试素材比背一堆XX参数有用得多。4.3 类加载机制双亲委派到底在防什么类加载机制里最常考的就是双亲委派模型。应用类加载器收到加载请求后不会自己先去加载而是委托给父加载器一直往上到启动类加载器父加载器能加载就用父的加载不了才往下退。这套机制的核心目的是保证Java核心库的类型安全防止你写一个java.lang.String来冒充JDK自带的String。你想想如果核心API可以随便被覆盖那整个Java生态就乱了。破坏双亲委派的经典案例是JDBC和Tomcat。比如Tomcat为了隔离不同web应用的类自己实现了WebAppClassLoader先加载自己应用目录下的类而不是严格遵守父优先。SPI机制的ServiceLoader也是反着来的线程上下文类加载器被用来突破双亲委派的限制。这些延伸点未必每个面试官都会问但能主动提出来会显得你对类加载机制的理解不是背概念。5. 数据库索引失效、事务隔离级别和一条慢SQL的排查5.1 MySQL索引结构为什么说B树是最优选MySQL的索引题是绝对的必考。InnoDB用的是B树而不是B树、哈希或者二叉树。B树的特点非叶子节点只存索引键不存数据所以每个节点能存更多的键树更矮更宽查询时磁盘IO次数更少叶子节点用双向链表串起来区间查询只需要顺序扫描叶子节点就行。为什么不用哈希索引因为哈希只支持等值查询不支持范围查询而数据库里like、between、大于小于都是高频操作。覆盖索引和回表这两个概念也要滚瓜烂熟。覆盖索引指的是查询的字段都在索引中不需要再回表查主键回表则是先通过二级索引找到主键再用主键去聚簇索引查完整行。这里面试官喜欢出题“select * from user where name 张三name有索引会走索引吗”答案是会走二级索引但查到主键后要回表去拿其它字段。如果改成覆盖索引查询explain结果里Using index会提示没回表执行效率高很多。5.2 索引失效的常见场景与explain读法我平时帮同事排查慢SQL至少一半问题出在索引失效上。最常用的几个坑对索引列使用了函数比如where DATE(create_time) 2024-01-01索引直接失效前导模糊查询like %abc走不了索引而like abc%可以索引列参与了隐式类型转换比如字符串字段用数字去查or条件中有一个字段没索引整个查询可能退化为全表扫描。遇到这里我建议自己动手练建一张带有联合索引(a, b, c)的表分别执行where a 1、where a 1 and b 2、where b 2 and c 3等组合再用explain看key_len和ref很快就能建立直觉。explain是最直接的排查工具重点看这几个字段type从高到低是system、const、eq_ref、ref、range、index、ALL、key实际用的索引、rows预估扫描行数、Extra是否Using filesort、Using temporary。凡是看到type为ALL且rows很大的SQL就要警惕了。我个人习惯是生产环境慢查询日志打开阈值设1秒每天看一遍把高频的慢SQL都拎出来优化一轮。5.3 事务隔离级别与MVCC实现原理事务部分必问隔离级别。MySQL默认是可重复读四个级别是读未提交、读已提交、可重复读、串行化。它们各自解决的问题读未提交会有脏读读已提交解决了脏读但会有不可重复读可重复读解决了不可重复读但理论上还有幻读串行化最安全但性能最差。InnoDB在可重复读级别下通过MVCC 间隙锁的手段实际上已经基本解决了幻读问题。MVCC的原理是每个事务有一个read view通过版本链来控制可见性。每行数据有两个隐藏列事务ID和回滚指针。查询时根据当前事务的read view判断该行版本是否可见。这个机制最妙的地方是普通的select不加锁就能做到快照读读写不互斥并发性能很高。面试时如果能把MVCC讲清楚再用一条update语句举例说明当前读要加锁面试官对你数据库功底的判断会明显上一个档次。6. 缓存与分布式Redis持久化、缓存穿透和分布式锁6.1 Redis持久化RDB和AOF到底怎么选Redis在Java面试里出现频率极高尤其现在分布式系统成为标配面试官默认你是用过Redis的。持久化这块RDB是定时生成全量快照恢复快但可能丢失最后一次快照之后的数据AOF是记录每次写操作的日志实时性高但文件体积大恢复慢。生产中我建议两者一起用AOF负责保证数据不丢RDB负责快速恢复。Redis 4.0之后支持AOF重写和混合持久化AOF文件前半段是RDB快照后半段是增量日志兼顾恢复速度和实时性。缓存穿透、缓存击穿、缓存雪崩这三个“兄弟”几乎是必考而且经常连着问。穿透是查询一个根本不存在的key导致请求直接打到数据库击穿是一个热点key过期瞬间大量请求同时打到数据库雪崩是大量key同时过期或Redis宕机导致数据库被压垮。解决穿透可以用布隆过滤器先过滤掉不存在的key或者把空值也缓存起来但设置较短的过期时间击穿可以用互斥锁让只有一个线程去重建缓存雪崩可以用过期时间加随机值错开或者用集群保证高可用。我面试时最怕听到的答案是“用缓存可以解决”但说不出具体方案所以大家一定要把场景和方案对应起来。6.2 分布式锁的三种实现与Redisson的看门狗机制分布式锁的实现从简单到复杂有数据库唯一约束锁、Redis SETNX锁、ZooKeeper临时顺序节点锁。现在最主流的还是Redis加锁用SET key value NX EX 10这条命令实现原子性value里放一个唯一标识释放锁时用Lua脚本判断是否是自己的锁再删除避免误删别人的锁。这里有个经典问题“为什么不用setnx和expire两条命令分开执行”因为不是原子的如果setnx成功而expire失败锁永远不释放会死锁。Redisson解决这个问题的方式是看门狗机制加锁成功后会启动一个后台任务默认每10秒续期一次把锁的过期时间重置为30秒只要业务逻辑没执行完锁就不会被自动释放。看门狗虽然有争议因为它会使锁在极端情况下长期hold住但在多数业务场景下是合理的取舍。面试时能说出看门狗的原理和释放锁用Lua脚本基本可以证明你确实在分布式场景下踩过坑。7. 主流框架与微服务Spring生命周期、MyBatis原理和分布式事务7.1 Spring Bean生命周期和循环依赖Spring这块的重点是Bean的生命周期完整流程从实例化、属性填充、初始化、AOP代理到销毁。面试官最爱问的其实是“Spring怎么解决循环依赖”。三级缓存的机制要能讲清楚一级缓存存完整Bean二级缓存存早期暴露的Bean三级缓存存Bean的ObjectFactory。创建A时发现需要注入B就去创建BB又需要注入A此时A通过三级缓存的ObjectFactory提前暴露一个半成品给BB注入完成后A再继续初始化。关键点在于三级缓存是为了处理AOP代理如果Bean不需要代理二级缓存其实就够用了。很多候选人会把循环依赖和代理工厂搞混建议亲手写一个两个Bean互相注入的小项目构造器注入、字段注入、setter注入各试一遍看哪个能成功哪个报错印象会深很多。注意构造器注入的循环依赖Spring是解决不了的直接启动报错这也是为什么现在推荐字段注入和setter注入的原因之一。7.2 MyBatis的#和$区别及插件原理MyBatis有两个高频题第一#{}和${}的区别。首选答案是#{}会生成预编译占位符配合PreparedStatement防止SQL注入${}是字符串直接拼接存在注入风险。所以动态排序、动态表名这些必须用${}的场景要自己做好白名单校验。我之前遇到过一个线上Bug就是有人在order by后面直接用了前端传的字段结果被拼了奇怪的内容进去还好是内部系统不然后果很严重。第二MyBatis插件原理。MyBatis允许通过Interceptor拦截四大核心组件Executor、StatementHandler、ParameterHandler、ResultSetHandler。分页插件PageHelper就是拦截Executor改写SQL自动拼接limit。我建议大家可以自己实现一个简单的慢SQL拦截插件打印执行时间超过阈值的SQL这样“插件原理”就不再是死知识而是你已经动手实践过的东西。7.3 分布式事务两阶段提交、TCC与最终一致分布式事务是高级岗必考也是很多人复习时最头疼的。核心问题在于微服务架构下一个业务操作可能涉及多个服务多个数据库本地事务的ACID已经不够用了。两阶段提交是刚性事务有协调者统一协调准备阶段所有参与者都成功后进入提交阶段任何一个失败就全部回滚缺点是同步阻塞、协调者单点。TCC是一种补偿型方案Try阶段锁定资源Confirm阶段提交Cancel阶段回滚。它适合对一致性要求较高的场景但需要大量业务代码去实现三个方法。最终一致性方案里最常用的是本地消息表加消息队列本地事务先写业务表和消息表然后通过MQ把消息发出去消费者处理完后回调确认。如果MQ发送失败定时任务扫描消息表重新投递。这套方案虽然有一定的延迟但对大多数电商下单、积分发放这类场景是够用的。我面试时的经验是不要只说方案名字最好结合你自己做的项目讲一遍当时的选型过程比如为什么不用2PC而用消息最终一致是因为目标系统不要求实时一致性但要求高可用选型的理由比方案本身更重要。8. 算法与工具高频手撕题和一整套排查命令8.1 排序算法与TopK问题的两种解法Java面试手撕算法难度一般在中低档但排序算法是基础中的基础。冒泡排序、选择排序、插入排序、快速排序、归并排序至少能手写快排和归并。手写快排要注意边界条件比如left和right相遇时的交换逻辑还有递归退出的条件。面试官很看重代码风格变量命名清晰、边界处理得当、时间复杂度能说清楚这三点比背模板重要。TopK问题是实际场景中出现频率最高的题目。“从10亿个数中找出最大的100个”有两种主流解法全局排序后取前K个复杂度O(nlogn)用大小为K的最小堆遍历一遍堆顶就是当前第K大的数复杂度O(nlogK)。面试时一般要求说第二种同时可以把Java里的PriorityQueue直接用上。另一个常考的是手写一个LRU缓存用LinkedHashMap的accessOrder加上removeEldestEntry就能实现但面试官更想看到你用HashMap加双向链表自己实现这个我自己练过几次从设计思路到代码实现大概需要十分钟值得提前准备。8.2 Linux排查命令和Java线上问题定位流程工具类题目Linux命令是绕不开的。高频命令包括top看CPU和内存、free看内存、df看磁盘、jstack看线程栈、jstat看GC、jmap看堆。线上问题排查的完整套路我常用的先top看哪个进程CPU高然后top -Hp看进程中哪个线程高再jstack导出线程栈搜索对应线程号定位到具体代码行。如果是内存问题jstat -gcutil先看GC频率再用jmap -dump:formatb,fileheap.hprof导出堆转储用MAT分析Shallow Heap和Retained Heap找出大对象。这里有一个很实用的小技巧jstack输出的线程号是十六进制而top -Hp输出的是十进制换算时可以用printf %x\n快速转换。我第一次排查线上死循环问题时就是因为线程号没转换对对着线程栈找了半天才反应过来后来越用越顺手。这些命令不需要背我建议你打开一个测试环境故意写一个内存泄漏的程序按上面的流程完整走一遍一次之后就不会忘了。8.3 Linux面试题补充端口查看与日志分析除了Java层面的命令Linux本身的基础题目也会考。查看端口占用用netstat -tlnp或ss -tlnp查看进程用ps -ef实时查看日志用tail -f按关键字过滤用grep。有一次面试官问我“线上日志刷得飞快你怎么定位问题”我给出的思路是先grep ERROR统计错误频率再用awk按时间维度聚合并找出异常时间段最后用tail -n 500看看异常前后的上下文。这套组合拳把grep、awk、tail串起来用比单个命令背熟更能体现实战能力大家准备时一定要多练这种组合场景。9. 高频问题速查与避坑清单这段内容是我这几年面试和带人过程中积累出来的经验速查可以直接当复习清单用。我按“题目、答题要点、常见错误”三个维度整理了一张表你可以对着每一项自查看哪些能流畅回答哪些还需要补。题目核心回答要点常见错误HashMap和Hashtable区别HashMap线程不安全、允许null键、JDK8引入红黑树只说“一个线程安全一个不安全”ArrayList和LinkedList区别数组和双向链表随机访问vs插入删除忽略LinkedList也有get方法但复杂度是O(n)什么是CAS比较并交换乐观锁底层依赖CPU指令没说ABA问题及版本号解法Spring事务失效场景方法内部调用、异常被吞、访问权限非public只说“没加Transactional注解”Redis为什么快纯内存、单线程、IO多路复用忽略高效数据结构带来的贡献消息队列怎么保证不丢消息生产者端确认、Broker持久化、消费者端手动ack只答“消息持久化就行”微服务接口幂等怎么设计唯一ID加状态机、Redis token、数据库唯一索引没有提到“同一请求重复提交”的场景JVM频繁Full GC怎么排查jstat看GC频率、jmap导出堆分析一开始就盲目加堆内存避坑清单这边也要专门说几条。第一面试中不要只讲结论不讲场景面试官问“Redis为什么快”你答完单线程和IO多路复用后可以补一句“所以在高并发场景下我们通常用Redis做热点数据缓存而不是做复杂计算”这样显得有业务视角。第二别把“读过源码”挂在嘴边一旦被追问“HashMap的putVal方法里treeifyBin的触发条件是什么”答不上来反而扣分。第三项目经验的准备比刷题更重要面试官通常会先让你讲一个项目你再从项目里引出技术难点这套路线比干巴巴背题自然得多。10. 面试准备节奏与自我检测方法关于怎么安排学习节奏我最后多说两句。如果你的面试时间还有两周我建议按周拆分前三天把Java基础和并发整块过完第四天到第六天攻克JVM和数据库第七天到第九天集中看Redis、消息队列和Spring第十天到第十二天做算法和系统设计题最后两天做模拟面试。每天两小时左右早上背理论晚上动手写代码验证。自我检测的方法很简单拿一个具体场景来自问自答。比如“现在有一个秒杀系统库存只有100件但瞬时并发有10万你怎么设计”你顺着这个场景往下拆先上CDN和Nginx做流量拦截再用Redis加分布式锁控制超卖最后用MQ做削峰填谷。拆的过程中你会发现缓存、锁、消息队列、数据库事务这些知识点全被带出来了。这种以场景驱动的方式比按目录记忆要牢固得多面试时遇到类似问题也会更有底气。