尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java面试核心知识点全解析:从基础到原理涵盖JVM并发数据库

Java面试核心知识点全解析:从基础到原理涵盖JVM并发数据库 1. Java核心基础基本功扎实不扎实问这几个问题就知道1.1 面向对象与数据类型重写、重载、Integer缓存这类送分题别再丢分先说重载和重写。很多候选人能说出“重写是子类重写父类方法重载是方法名相同参数不同”但追问两句就露馅。重载发生在编译期本质是编译器根据参数列表、参数类型决定调用哪个方法所以也叫编译时多态重写则发生在运行期由JVM根据对象的实际类型动态分派所以叫运行时多态。这两个概念在JVM层面也很值得展开。方法重写的本质是虚方法表vtable的分派机制JVM通过invokevirtual指令查找实际类型对应的方法。而重载可能在编译阶段直接确定调用目标有些场景下JVM会做“方法内联”优化。面试时能讲到这一层基本就赢过九成背概念的人。重写还有一个高频考点重写方法的访问权限不能比父类更严格返回值可以是父类返回类型的子类型协变返回类型抛出的异常范围不能比父类更宽。为什么允许协变返回类型因为子类在语义上满足“is-a”关系调用方按父类方法签名接收返回值时子类返回的更具体类型天然兼容这也是Java泛型、集合框架里经常出现的模式。再来看 与 equals。基础题但很多人理解得不够彻底。 在比较基本数据类型时比较的是数值在比较引用类型时比较的是内存地址。equals 是Object类的方法默认实现等价于但String、Integer这些类重写后比较的是内容。这里真正的考点是重写equals时必须重写hashCode。道理很简单以HashMap为例put时先算hash定位桶桶内再用equals判断是否相同。如果你只重写equals不重写hashCode两个逻辑上相等的对象会被分到不同桶里HashMap里就会出现重复数据。这个坑我在真实项目里见过不止一次面试官问概率极高。配套的还有一个经典题Integer i1 100, i2 100i1 i2 返回什么如果换成 Integer i3 200, i4 200 呢答案是前者true、后者false。原因是IntegerCache默认缓存了[-128, 127]区间的对象自动装箱时直接返回缓存中的同一个对象。这个缓存区间的上界可以通过JVM参数 -XX:AutoBoxCacheMax 调整。所以用包装类型比较值的时候永远推荐用equals或者直接比较基本类型值不要依赖。顺带说一个2026年面试里越来越常见的新特性题Java 17和Java 21的Record、sealed class、switch模式匹配。Record可以理解成一行代码搞定不可变数据类的equals、hashCode、toString和构造方法配合注解处理器很多项目的Lombok依赖都能替换掉。记录类型定义时组件字段是final的适合做DTO、事件对象这类场景。能主动讲出新特性在实际项目里怎么用的候选人观感会好很多。1.2 String与集合天天用的代码反而最容易被问倒String不可变性是Java面试的必考题它的价值远远超出“背答案”的范畴。String类内部用一个final修饰的byte数组存储字符串内容所有修改操作都返回新对象原对象不变。这个设计的直接好处有三个一是字符串常量池可以放心复用同一个字面量字符串在内存中只有一份JVM做intern操作时不用担心被修改二是String对象天然线程安全作为HashMap的key时就算在多线程环境下也不会被意外改写三是哈希值可以在第一次计算后缓存下来这也是HashMap喜欢用String做key的原因之一。经典的new String(abc)创建了几个对象答案要看场景。如果字符串常量池里还没有abc那么JVM会在常量池中创建一个字面量对象同时new关键字在堆中创建一个新对象一共两个如果常量池里已经有abc那只有堆中一个。这道题的对手法在于引导候选人讲清楚常量池和堆的关系以及intern()方法的作用。StringBuilder和StringBuffer的区别就不赘述了核心就一句话StringBuffer的关键方法加了synchronized关键字保证线程安全但牺牲了性能StringBuilder是线程不安全的但性能更高。现代Java开发里单线程环境下的字符串拼接基本都是StringBuilder编译器对字符串的号拼接也会自动优化成StringBuilder.append。随带提一笔JDK 9之后字符串存储从char[]换成了byte[]配合COMPACT_STRINGS特性可以按需使用Latin-1或UTF-16编码这也是为什么源码里看到的是byte数组。集合框架这部分我面试必问HashMap。JDK 1.8版本之后HashMap的底层是数组加链表加红黑树默认初始容量16加载因子0.75当链表长度达到8且数组长度达到64时链表转红黑树当红黑树节点数降到6时转回链表。为什么阈值是8源码注释里给了答案泊松分布下负载因子0.75时链表中节点数到达8的概率是千万分之六这个阈值兼顾了空间和时间成本。HashMap的扩容机制也特别容易考。扩容时元素需要重新计算桶位JDK 1.7用的是头插法并发扩容时可能形成环形链表导致get死循环JDK 1.8改成尾插法解决了环形链表问题但HashMap在并发写时仍然不安全可能丢失数据。所以并发场景必须用ConcurrentHashMap。ConcurrentHashMap在JDK 1.8后放弃了分段锁改用CAS加synchronized锁住数组的首节点粒度更细读操作完全无锁这也是它性能好的原因。ArrayList和LinkedList这对兄弟也总被拿来对比。ArrayList底层是Object数组随机访问O(1)LinkedList底层是双向链表头尾插入删除快但中间插入也同样需要遍历找到位置复杂度O(n)。真实开发里我建议除了一些特殊的队列场景外无脑用ArrayList因为它内存占用更友好数据连续存储对CPU缓存友好遍历性能明显优于LinkedList。LinkedList频繁创建节点对象对GC压力更大实际项目中用它的场景远比想象中少。还有一个容易被问的细节是字符编码。开发中常见的乱码问题基本都是编码不一致导致的比如文件是UTF-8编码代码里却用GBK去读。处理原则很简单读写必须指定同一套字符集不要依赖系统默认编码。String.getBytes()不传参数时用的是JVM默认编码换台机器可能行为就变了线上出过太多这种事故。优先用UTF-8所有IO操作显式声明StandardCharsets.UTF_8。1.3 异常与常用库函数睡着也能答的题要有自己的思考异常体系考察的是代码设计的边界意识。受检异常checked exception必须显式处理或向上抛出比如IOException、SQLException非受检异常RuntimeException及其子类不需要强制处理比如NullPointerException、IllegalArgumentExceptionError则代表JVM层面的严重问题如OutOfMemoryError、StackOverflowError程序通常无法恢复也不应该去捕获。很多框架和项目现在反而很克制地使用受检异常因为过度使用会导致代码里全是try-catch或throws声明可读性急剧下降。Spring的Dao层异常就全部包装成非受检的DataAccessException让业务代码可以更灵活地决定在哪个层级处理异常。面试官真正想看的不是你能不能背出Exception的继承树而是你在设计接口时如何约定异常边界。finally与return的执行顺序也值得认真研究一下。try中执行到return语句时会先计算return后面的表达式结果再跳转到finally执行最后把刚才的结果返回所以finally里对返回变量的修改不会影响已经算好的返回值。但有一种例外如果finally块里也写了return它会直接覆盖try中的return这种写法极度危险团队规范里应该直接禁止。常用库函数方面网上经常搜到“algorithm java”“常用库函数algorithm java”这类关键词其实Java标准库没有一个统一的algorithm包范围是散的java.util.Arrays提供了sort、binarySearch、copyOf、fill这些数组操作java.util.Collections提供了排序、反转、洗牌、不可变集合封装java.lang.Math提供了数学计算。实际算法题里Arrays.sort用的是双轴快速排序基本类型和TimSort对象类型这个底层细节也能体现你读源码的深度。Comparator和Comparable的对比也顺手复习一下。Comparable是“类自己跟自己比”实现compareTo就能指定自然排序Comparator是“外部比较器”通过lambda或匿名类传入sort方法适合定义多种灵活的排序规则。Java 8之后Comparator还提供了链式写法Comparator.comparing(User::getAge).thenComparing(User::getName)在业务排序里非常常用面试时可以主动展示这一手。2. JVM与内存管理背书答不出亮点能讲原理才值钱2.1 内存区域与对象分配画张图就能讲清楚的东西JVM内存区域划分是面试必问但我建议别停留在背名字的阶段把每个区域的职责和可能抛出的异常说清楚才叫合格。堆Heap是所有线程共享的区域对象实例基本都在这里分配。堆内部可以进一步划分为新生代Eden区、两个Survivor区S0和S1和老年代默认比例大约是8:1:1。新生代对象存活率低适合用复制算法老年代对象存活率高适合用标记-清除或标记-整理算法。堆空间不足时抛出OutOfMemoryError: Java heap space。虚拟机栈VM Stack是线程私有的每个方法调用都会压入一个栈帧栈帧里有局部变量表、操作数栈、动态链接、方法出口等结构。局部变量表里存放基本数据类型、对象引用和returnAddress64位的long和double占用两个局部变量槽。栈深度不够时会抛StackOverflowError。方法区在JDK 8之后被元空间Metaspace取代核心区别是元空间使用本地内存而不是JVM堆内存默认情况下不会OOM但可以设置-XX:MaxMetaspaceSize限制。方法区里存的是类元信息、运行时常量池、静态变量等。程序计数器是唯一不会OOM的区域它记录当前线程执行的字节码行号用于分支、循环、跳转、异常恢复等场景。对象创建流程是第一章的延伸类加载检查到分配内存到零值初始化到设置对象头再到执行构造方法。这里值得展开的是内存分配方式堆内存规整时用指针碰撞不规整时用空闲列表。并发分配内存有两个解决思路一个是CAS加失败重试一个是TLABThread Local Allocation Buffer——每个线程预先在Eden区划一小块私有内存分配对象先在自己私有的TLAB里分配满了才去堆上申请新的。大部分Java对象分配是在TLAB中完成的这也是为什么new对象在高并发场景下性能尚且可以接受的原因。2.2 垃圾回收从根搜索到三色标记聊聊原理别只背算法名判断对象是否存活有两种方案。引用计数法实现简单每个对象维护一个计数器被引用加一、引用失效减一C的智能指针就是这么做的但它解决不了循环引用问题Java没有采用。Java用的是可达性分析从一组GC Roots出发通过引用链遍历凡是不可达的对象判定为可回收。GC Roots包括虚拟机栈中引用的对象、静态属性引用的对象、常量引用的对象、JNI引用的对象。三种基础回收算法要放在一起对比。标记-清除算法先标记可回收对象再统一清除缺点是会产生大量不连续的内存碎片后续分配大对象时可能要提前触发FG。标记-复制算法把内存分成两块每次只用一块存活对象复制到另一块后整块清理没有碎片但浪费了一半空间。标记-整理算法让存活对象向一端移动然后清理边界以外的内存适合老年代这种对象存活率高的场景。新生代因为“朝生夕死”的特点天然适合复制算法Eden和Survivor的比例设计就是为了把复制成本降到最低。GC算法名称背得很溜但面试官更想听你说清楚CMS和G1的设计思路。CMSConcurrent Mark Sweep主打低停顿标记阶段和清除阶段尽量和用户线程并发执行但CMS有两个硬伤一是使用标记-清除算法会产生碎片老年代碎片多了之后不得不退化成Serial Old的Full GC二是CMS的并发阶段会和用户线程抢CPUCPU核数少的时候吞吐量反而降低。G1把堆划分成一个个大小相同的Region维护一个优先级列表优先回收垃圾最多的Region从而让停顿时间可预测。G1既用复制算法解决碎片问题又用并发标记和SATB快照式保证正确性是JDK 9之后的默认垃圾收集器。如果要展示深度可以聊聊三色标记和并发标记漏标问题。在并发标记阶段为了不暂停业务线程GC线程用“灰色”表示已访问但正在扫描的对象“白色”表示未被访问的对象“黑色”表示已访问且其引用也全部访问过的对象。并发标记最大的风险是“漏标”黑色对象新引用了某个白色对象同时这个白色对象原有的引用路径被切断那么它就会被错误回收。CMS通过增量更新解决G1通过SATB快照解决。能讲到这个层面面试官基本会默默给你加分。2.3 类加载与静态链接误区Java到底是静态链接还是动态链接网上有个流传很广的错误说法叫“Java是静态链接的”看到这个热搜词的时候我愣了一下这明显是把概念搞混了。Java类默认是动态链接的一个类编译后引用其他类时在字节码层面只是符号引用真正执行到那条指令时JVM通过解析符号引用为直接引用把对应的类加载进来。这个“用到才加载、运行时才解析”的机制就是动态链接的经典表现。静态链接更贴切的例子是C/C在编译期把用到的库函数代码直接复制进可执行文件。所以如果不幸在面试里被问到这个题请你放心大胆地说“Java本质上是动态链接的”顺着类加载和常量池解析往下讲面试官会明白你是真的读过资料。类加载过程分为加载、验证、准备、解析、初始化五个阶段。“加载”是找到class文件字节流并生成Class对象“验证”是检查格式和安全防止恶意字节码包括了元数据验证、字节码验证等“准备”是给类静态变量分配内存并初始化为零值不是赋代码里写的值那个在初始化阶段才做“解析”是把常量池内的符号引用替换为直接引用这里既有静态解析也有动态解析比如方法调用指令中的虚方法就需要运行时才确定“初始化”才真正执行类构造器clinit方法。双亲委派模型这个点必须讲透。三层类加载器从下到上是应用类加载器、扩展类加载器JDK 9后被平台类加载器替代、启动类加载器。加载一个类时子加载器先不自己加载而是逐层向上委托父加载器能加载就父加载器说了算父加载器加载不了再向下传。这样做最大的意义是安全你就算自己写一个java.lang.String即便能编译通过运行期也会被启动类加载器加载的JDK版本String截胡你那个类根本不会加载。同时类库的统一性也保证了同一个类在全JVM范围内只有一份。双亲委派也不是万能的JDK的SPI机制Service Provider Interface就遇到了麻烦。以JDBC的DriverManager为例它是启动类加载器加载的rt.jar里的核心类但它需要调用各数据库厂商实现的驱动类这些驱动类放在应用的classpath里由应用类加载器加载。按照双亲委派DriverManager根本加载不到第三方的mysql-connector-java。于是JDK引入了线程上下文类加载器让父加载器“反向”委托给子加载器去加载。这就是所谓的“破坏双亲委派”理解了它才算真正理解了类加载机制的全貌。3. 并发编程这里才真正区分“会用”和“懂原理”3.1 volatile与synchronized从可见性到锁升级volatile这个词被问烂了但能讲透的人真不多。它保证两件事可见性和有序性。如果多个线程操作同一个变量volatile修饰的变量每次读取都会从主内存中拿最新值每次写入也会立即回写到主内存避免了线程工作内存中“缓存过期”的问题。底层依托的是JMMJava内存模型以及处理器层面的缓存一致性协议例如MESI协议配合内存屏障读屏障、写屏障实现。但volatile有个致命短板不保证原子性。经典的count操作即使count加了volatile并发执行还是会丢值。原因很简单count在字节码层面是“读-改-写”三步三步之间线程随时可能切换。volatile只保证你读的时候是最新值、写的时候其他线程看得见但读取和写入不是一个不可分割的原子操作。这个题几乎每次面试都会出现答案是“volatile不保证原子性要原子操作用AtomicInteger或加锁”才对。synchronized是Java内置锁面试含量极高。早期synchronized是一个重量级实现直接依赖操作系统的Monitor锁线程阻塞和唤醒涉及用户态和内核态切换开销非常夸张。JDK 1.6之后引入了锁升级无锁到偏向锁到轻量级锁到重量级锁。偏向锁解决“单线程访问同步块”的场景在对象头记录线程ID不需要真的CAS一旦有第二个线程争抢撤销偏向锁并升级到轻量级锁轻量级锁通过自旋CAS抢锁抢不到就膨胀成重量级锁线程挂起等待。不过注意JDK 15已经默认禁用了偏向锁JEP 374给出的原因是维护成本太高、收益已经微乎其微。面试时把这段演进讲出来既能展示你了解历史又能说明你关注新版本变化。synchronized优化还有锁消除和锁粗化JIT编译器发现某个对象不可能被多线程共享时会直接去掉锁循环里反复加锁的代码会被合并成一次加锁这些都是编译器层面的优化思路。CAS比较并交换也是必考。CAS利用Unsafe类的compareAndSwap方法在硬件层面完成“比较-更新”的原子操作失败就重试。它避免了锁带来的线程阻塞但引入了ABA问题一个值从A变成B又变成ACAS无法感知中间变化。Java里可以用AtomicStampedReference带版本号解决实际开发中ABA的触发条件相当苛刻但面试题就是喜欢拿来考。3.2 Java并发工具锁、ThreadLocal、线程池ReentrantLock和synchronized的区别可以列一张表来记后者是隐式的锁的获取和释放由JVM自动管理前者需要手动lock和unlock且必须放在finally里后者不可中断前者的lockInterruptibly可以响应中断后者是非公平的前者可以选择公平模式后者只有一个等待队列前者可以有多个Condition实现精细化的等待通知。ReentrantLock基于AQS实现AQS内部维护一个volatile的state状态和CLH线程等待队列独占模式下state为1表示锁被占用重入时state递增。ThreadLocal是并发场景里被误解最多的工具。它的原理是每个Thread内部维护一个ThreadLocalMap键是ThreadLocal的弱引用值是真实的业务数据。所谓“线程隔离”其实就是各线程访问自己的Map。这里有个著名的坑ThreadLocalMap的key是WeakReferenceThreadLocal对象失去了外部强引用后会被GC回收但value是强引用还留在Map里导致内存泄漏。尤其在线程池中线程长时间存活累积的value会越来越多。所以正规用法是用完立即调用remove()用try-finally包裹。线程池是并发面试的重头戏。ThreadPoolExecutor的七个核心参数必须张口就来corePoolSize核心线程数、maximumPoolSize最大线程数、keepAliveTime空闲线程存活时间、unit时间单位、workQueue工作队列、threadFactory线程工厂、handler拒绝策略。执行流程是来一个任务当前线程数小于corePoolSize就直接开新线程大于等于corePoolSize就进队列排队队列满了再检查是否小于maximumPoolSize小于就开临时线程大于等于就触发拒绝策略。关于核心参数怎么设置给一个实用经验。CPU密集型任务的线程数接近CPU核数加一IO密集型任务因为等待IO期间CPU空闲可以设置为核心数的两倍甚至更高。实际生产里我是这样做的先用公式算初值再压测调整。线程池跑在线上的时候核心线程数的调整是可以动态的setCorePoolSize方法支持运行时修改。拒绝策略有四种AbortPolicy直接抛异常拒绝提交、CallerRunsPolicy让提交任务的线程自己执行、DiscardPolicy和DiscardOldestPolicy直接丢弃。业务上我不建议用Discard因为任务丢了没有任何反馈自定义拒绝策略里打日志加消息队列补偿才是稳妥方案。4. 数据库与SQL实战MySQL面试题无非这三个方向4.1 索引与性能优化explain都不看的简历我是不太信的索引这部分想答出彩要讲清楚“为什么是B树”。B树的非叶子节点只存键值和子节点指针不存数据所以一个16KB的页能存储更多索引项树的高度更矮。三层的B树大概能支撑千万级数据的查找树矮意味着磁盘IO少。B树的叶子节点通过双向链表串联天然支持范围查询和排序而B树每个节点都带数据不管是范围查询还是遍历都要多次回溯。哈希索引适合等值查询但无法支撑范围查询和排序所以InnoDB的默认索引结构是B树。索引失效是高频题我把常见场景整理一下联合索引不满足最左前缀where条件中对索引列使用了函数或计算比如WHERE DATE(create_time) 2026-01-01索引列参与计算会导致优化器放弃索引模糊查询左侧通配like %xxx无法用B树有序特性隐式类型转换比如对varchar类型的列写WHERE num 123MySQL会做类型转换索引也会失效用or连接时两侧条件不全有索引范围查询右侧的列可能失效。实际排查中用EXPLAIN查看type、key、rows字段最直接从all到range再到ref逐步做优化。这里插一个非常实用的建议建索引不是越多越好。每个二级索引都是一份独立的B树写操作需要同步维护多棵树的更新插入和更新性能会明显下降。我在项目中见过一张表建了十几个索引结果导入数据时慢到怀疑人生。索引的取舍原则是优先覆盖查询频率最高的SQL联合索引设计时把区分度高的列放前面查询条件里等值判断的列排在范围判断的列之前。4.2 事务、隔离级别与MVCC事务的ACID四个特性要结合InnoDB的底层结构来讲。原子性靠undo log保证事务一旦回滚通过undo log恢复数据到事务开始前的状态持久性靠redo log保证事务提交时先把redo log刷盘即使数据库崩溃重启后通过redo log重放已经提交的事务隔离性靠锁和MVCC机制一致性则是前三者协同工作的最终结果。隔离级别有四个档次。读未提交READ UNCOMMITTED会出现脏读读已提交READ COMMITTED解决了脏读但会出现不可重复读可重复读REPEATABLE READ解决了不可重复读但在更高的隔离级别下仍可能出现幻读串行化SERIALIZABLE通过强制事务串行执行解决了所有问题但性能极差。MySQL默认隔离级别是可重复读要记住InnoDB在可重复读级别下通过间隙锁和MVCC在很大程度上解决了幻读问题。MVCC是被问得最多但答得最差的一个点。无锁的快照读依赖的是undo log版本链和ReadView。每一行数据除了业务字段外还有隐藏的trx_id最后修改事务ID和roll_pointer回滚指针指向undo log中的历史版本。事务执行快照读时生成ReadView里面记录了当前活跃事务ID列表、最小活跃ID、最大ID等信息判断一个版本是否可见本质上就是看trx_id是否在当前事务的快照范围内。可重复读级别下事务第一次快照读时生成一次ReadView整个事务期间复用同一份读已提交级别下每次快照读都会生成新的ReadView正因为这个差异两种隔离级别在同一个事务内看到的结果不同。死锁的排查与解决也要会。死锁发生的四个必要条件互斥、持有并等待、不可剥夺、循环等待。MySQL最常见的死锁场景是事务A先更新表X再更新表Y事务B先更新表Y再更新表X顺序相反导致互相等待。排查时执行SHOW ENGINE INNODB STATUS关注LATEST DETECTED DEADLOCK段落能看到最近一次死锁的SQL语句和持有锁的情况再查information_schema.innodb_trx和innodb_locks表定位未提交的事务。预防手段包括多表操作始终按固定顺序访问、事务尽量短小、合理设计索引避免间隙锁扩大锁范围。4.3 分库分表与行级权限业务功能背后都在考什么分库分表考察的是数据量增长后的架构设计。垂直拆分把表按业务域拆开比如订单库、用户库、商品库分离核心是降低单库的压力水平拆分则把同一张表的数据按某种规则分到多个库多个表规则常见的是哈希取模和范围分片。哈希取模写分布均匀但扩容要迁移数据范围分片扩容方便但可能热点集中。实际落地时通常先垂直拆分再对数据量最大的几张表做水平拆分。分片后的全局主键怎么生成也是必考。数据库自增ID已经不好用了常见的方案有雪花算法Snowflake64位二进制1位符号位加41位时间戳加10位机器ID加12位序列号单机每秒可生成约409.6万个ID趋势递增不依赖数据库美团Leaf的号段模式每次从数据库取一批ID客户端内存中分配性能和持久性都不错。面试时能把雪花算法的位结构、时钟回拨问题的处理思路讲清楚就是明显的加分项。行级权限在Java开发里是很多业务系统的刚需尤其是多租户SaaS、企业管理系统。场景很简单用户A登录后只能看到自己创建的单据部门经理能看到整个部门的管理员能看到全部。实现思路是不要在每个查询SQL里手动拼权限条件那样迟早会漏而是通过MyBatis的拦截器统一处理。拦截StatementHandler的prepare方法解析SQL的AST利用JSqlParser这样的库找到where子句自动拼上dept_id #{currentUser.deptId}。实现时要做几个防坑设计一是必须通过注解标记哪些Mapper方法需要自动加权限条件避免所有查询都被误加二是拦截器只处理查询语句insert、update、delete要放行三是拼条件时要注意SQL本身是否已经带了相同字段的条件否则会SQL语法错误或权限条件互相覆盖。这块考察的其实不只是MyBatis插件机制还有你对SQL语法、AST解析的理解项目里如果在简历上写了权限控制一定要把这个方案吃透。4.4 常见SQL面试题排序、分组、行号与三表关联SQL题来几道最常见的。第一道查每个部门工资最高的员工。MySQL 8.0之后推荐用窗口函数ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC)先计算行号再取行号为1的记录。要注意的是如果允许并列RANK()和DENSE_RANK()的区别要讲清楚RANK会在并列后跳号DENSE_RANK不跳号。如果面试官不让用窗口函数就用关联子查询找出每个部门的最高工资再关联员工表取对应记录。第二道高频题查询连续N天登录的用户或者叫连续签到。核心技巧是用日期减去行号把用户每天登录的日期排序后减去行号得到一个日期如果用户连续登录这个日期是相同的。具体是DATE_SUB(login_date, INTERVAL row_number() OVER (PARTITION BY user_id ORDER BY login_date) DAY)然后按user_id和这个计算出来的日期分组count超过N就是连续登录N天的用户。这道题考察的是窗口函数和分组思路平时写业务SQL不涉及但面试很爱考。第三道是行级权限相关SQL比如一张表有author_id要求用户只能看到自己或本部门的数据。切换到JDBC代码就是动态拼接SQL但直接拼字符串有注入风险正确做法是MyBatis的动态SQL加where标签权限条件中的值用#{}绑定参数。如果涉及非本部门的数据过滤还可以用IN子查询先查出部门成员ID再作为过滤条件。这类题本质上考察的是“写安全的SQL”的能力参数化查询和预编译是底线任何把用户输入直接拼进SQL的行为都是面试一票否决项。5. 框架与中间件高频题Spring、MyBatis、Redis、Kafka5.1 Spring核心IOC循环依赖与AOP动态代理Spring全家桶是Java后端面试的大头。IOC容器的核心是BeanFactoryApplicationContext是其子接口功能更完善。Bean的生命周期常考版流程是实例化对象、属性填充、处理Aware接口比如BeanNameAware、ApplicationContextAware、BeanPostProcessor的before方法、执行init-method或InitializingBean、BeanPostProcessor的after方法、使用Bean、销毁时执行destroy-method。Spring Boot应用中大量使用了自动配置和条件装配这块建议顺着Configuration和Bean注解讲。循环依赖能问倒一大批人。Spring容器中解决构造器循环依赖无能为力但解决setter或字段注入的循环依赖靠的是三级缓存第一级singletonObjects存放完整的单例Bean第二级earlySingletonObjects存放提前暴露的半成品Bean第三级singletonFactories存放一个ObjectFactory用来生成提前暴露的代理对象。流程简化版A创建时需要注入BB创建时需要注入A此时A提前把自己暴露到三级缓存中B注入到未完成的AA最终完成创建并填充缓存B也就能正常完成。AOP代理的情况下如果A被切面代理三级缓存里的ObjectFactory会提前生成代理对象保证注入的也是代理。AOP的底层是动态代理。基于接口的默认用JDK动态代理基于类的用CGLIB。JDK动态代理要求目标类必须实现接口通过java.lang.reflect.Proxy生成一个实现了同样接口的代理类在InvocationHandler里统一处理CGLIB通过ASM生成目标类的子类重写被代理的方法所以不需要接口。Spring Boot 2.x默认开启proxyTargetClasstrue即优先用CGLIB理由是现代ASM生成字节码的性能已经不输反射。5.2 MyBatis一级缓存、二级缓存与动态SQLMyBatis几乎所有项目都在用但很多候选人只写了“会使用”原理一问就慌。先说缓存。一级缓存是SqlSession级别的默认开启同一SqlSession内两次执行同样的查询不会重复查数据库但执行增删改操作后一级缓存会被清空避免脏数据。二级缓存是namespace级别的需要配置缓存粒度是Mapper接口但多表操作时容易出现脏读——因为另一张表的更新不会触发这个namespace缓存的失效。所以真实项目里我基本不推荐开二级缓存分布式环境下缓存一致性问题更难解决。#{}和${}的区别是送分题。#{}是预编译方式MyBatis生成PreparedStatement占位符传入参数不会被拼接进SQL能有效防止SQL注入${}是直接拼接字符串有注入风险只能在表名、列名等无法使用占位符的场景使用而且必须对传入值做白名单校验。再深一层的考点是Mapper接口绑定。MyBatis启动时会为每个Mapper接口生成代理对象调用接口方法时通过方法名和接口全限定名找到对应的MappedStatement执行SQL并映射结果。动态SQL的核心标签if、where、set、foreach、choose底层通过OGNL表达式判断条件拼接SQL时用where自动去掉多余的AND用set去掉多余的逗号。这个细节值得记住自己手写拼接SQL时永远容易踩。顺带提一下MyBatis插件机制。MyBatis允许拦截四大核心对象Executor执行器、StatementHandler语句处理器、ParameterHandler参数处理、ResultSetHandler结果集处理基于JDK动态代理实现。前面讲的行级权限方案就是拦截StatementHandler实现的。能说清插件机制和四大对象职责的候选人面试评价会高一个档次。5.3 Redis缓存穿透、击穿、雪崩与分布式锁Redis三兄弟是面试必问题先把概念分清。缓存穿透是查询一个根本不存在的数据缓存中没有、数据库也没有每次请求都打到数据库攻击者用不存在的ID刷接口就能拖垮数据库。常用解法是缓存空值比如缓存key对应空对象TTL设短一点或者布隆过滤器把所有可能存在的主键加载进过滤器不存在的直接拦截。布隆过滤器有误判率但胜在内存占用极小适用于海量数据场景。缓存击穿是一个热点key在过期瞬间大量并发请求同时打到数据库。解决办法互斥锁只有一个线程能去查库并重建缓存其他线程等待后直接读缓存或者逻辑过期value里存一个过期时间戳发现逻辑过期后异步线程去更新但读取线程返回旧值牺牲了一点点强一致性。缓存雪崩则是一大批key在同一时刻过期或者Redis本身宕机导致大量请求压到数据库。key过期时间加随机值打散过期时间Redis用主从加哨兵或集群保证高可用这都属于常规手段。Redis分布式锁是另一个大热门。基于SET命令的SET key value NX PX 30000NX表示不存在才设置PX设置过期时间value里存一个唯一请求IDUUID或雪花ID释放锁时必须用Lua脚本比较value再DEL防止误删其他线程的锁。生产环境更推荐直接使用Redisson库它提供了看门狗机制自动续期默认每10秒续期一次避免业务执行超过锁过期时间后锁被提前释放的问题。面试常追问Redis主从切换时锁丢失怎么办这引出了RedLock算法但业界对RedLock争议很大最基本的结论是一致性和可用性要求特别高的场景直接选ZooKeeper或etcd实现分布式锁而不是纠结Redis。5.4 Kafka与消息队列可靠性与顺序性才是考察重点消息队列的题考察核心就两个词可靠性和顺序性。先讲不丢消息。生产端要设置acksall表示分区所有副本都写入成功才确认同时打开重试设置retries大于0避免网络抖动导致的偶发失败。Broker端核心是副本机制topic副本数至少3min.insync.replicas2保证至少两个副本同步。消费端最容易丢消息的是自动提交offset在业务还没处理完的时候offset已经提交消费端重启后就会跳过未处理的几条消息。方案是改成手动提交并保证在业务逻辑执行成功后再手动提交offset。顺序性问题的解法思路是Kafka只保证单个分区内的顺序全局顺序需要牺牲吞吐量使用单分区。现实中大多数场景是“某个用户的消息必须有序”那生产者在发送时按用户ID做key哈希同一个key进同一个分区即可。消费者的瓶颈在于多线程并发处理订单消息时同一个用户的多个消息可能被不同线程并发处理导致顺序错乱解决方式是按业务ID路由到内存队列每个队列配单线程处理。Kafka性能为何如此出众细节也值得掌握。一是顺序写磁盘Kafka的日志是追加写虽然用了磁盘但顺序写性能接近内存写二是Page Cache操作系统缓存热点数据读写尽量命中内核页缓存三是零拷贝消费消息时数据从磁盘读到PageCache后直接通过sendfile发送到网卡socket没有经过用户态缓冲区减少了多次上下文切换和数据拷贝。Zero Copy这个点尤其能体现你对性能和OS底层的理解深度。中间件这章还经常穿插Spring Boot自动配置原理。核心是EnableAutoConfiguration注解它通过ImportSelector机制加载META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中注册的配置类再配合ConditionalOnClass、ConditionalOnProperty等条件注解实现按需装配。所以引入一个starter依赖后符合条件时自动创建对应的Bean这就是“自动配置”的本质。定时任务方面XXL-Job在分布式定时任务领域出镜率很高它的分片广播、调度中心和执行器分离的特点都有可能在面试中出现。6. 算法、系统设计与面试表达最后一公里的这几分6.1 手写排序冒泡、快排、堆排序怎么讲出一种脱俗感算法题在Java面试中比例不低尤其校招。冒泡排序虽然简单但很多人写不出带优化的版本。标准写法是双重循环外层控制轮数内层相邻比较交换优化点在于增加一个标志位如果某一轮循环没有发生任何交换说明序列已经有序直接终止后续循环。复杂度最好O(n)、最坏和平均O(n^2)稳定。平时刷算法网站看到“冒泡排序Java”这类关键词的同学大概率是刚开始准备面试先把模板写熟再思考优化。快排是最常被要求手写的排序算法它把分治思想体现得淋漓尽致。思路是选一个基准值通过partition把数组分成左边小、右边大然后递归处理左右两部分。平均复杂度O(nlogn)但如果每次基准都选到最大或最小值退化到O(n^2)所以优化手段有三数取中、随机选基准、小区间改用插入排序。写代码的时候有一个典型边界坑partition中的边界条件搞错会导致死递归建议平时就写一套固定模板面试时照着自己的习惯快速输出。堆排序或TopK问题也是常客。找一亿个数里最大的K个数最优解不是全排序而是维护一个容量为K的小顶堆遍历数据如果当前元素比堆顶大就弹出堆顶并加入当前元素最终堆里的K个元素就是最大的K个。Java里直接用PriorityQueue实现默认是小顶堆。注意如果要最大K个数小顶堆恰好是对的选择因为堆顶就是当前K个候选中最小的新元素比堆顶大才有资格进入。手写题答完后可以顺带说一句PriorityQueue是基于数组实现的二叉堆扩容时类似ArrayList的倍增策略。6.2 分布式锁选型从Redis到ZooKeeper的对比分布式锁实际开发乃至面试中都很常见这里给个横向对比表方案实现难度性能可靠性典型使用场景数据库唯一索引低差一般依赖事务提交回滚低频、对一致性要求不极端的业务Redis SET NX EX中高存在主从切换丢锁问题大多数互联网业务配合Redisson看门狗ZooKeeper临时顺序节点高中高基于ZAB协议对一致性敏感、核心金融类服务etcd租约加续期高中高raft共识云原生环境k8s生态配套数据库分布式锁的原理是利用唯一索引的特性插入一条记录代表获取锁删除记录代表释放锁。它最大的问题是性能差和没有自动过期机制如果客户端宕机需要额外任务清理超时记录。Redis方案性能最好但主从架构中如果Master宕机时锁还没来得及同步到Slave新的Master上锁就丢了极端情况下两个客户端同时持有锁。ZooKeeper方案利用临时顺序节点加Watcher机制实现锁的公平性和自动释放客户端断开时临时节点自动消失不需要担心死锁但要引入一套ZooKeeper集群运维成本和延迟都高于Redis。选型结论也很直接90%的互联网业务场景Redisson就够了它的看门狗机制已经处理了业务超时问题但如果你在做资金、交易这类不能接受“两个人同时拿到锁”的系统老老实实用ZooKeeper或etcd。回答选型题的时候给出“成本-可用性-一致性”的取舍逻辑面试官会认为你真的做过架构决策而不是在背题。6.3 面试描述三个真话要比一个漂亮话值钱我参与面试这几年最强烈的感受是背题痕迹太明显了。候选人说出的话和网上搜到的按原文一字不差但稍微换个场景就不会应变。准备的时候可以用“背景、方案、取舍、结果、反思”作为故事线把每一个项目经历串起来。比如行级权限这个需求讲清楚为什么要做最初方案是什么遇到什么问题漏加条件、SQL拼接攻击面大最后怎么改进上线后有没有再出状况。这种讲述方式比单纯说“我用了MyBatis拦截器”要有说服力得多。还有两个小建议。第一被问到不会的问题时不要装懂坦诚说“这部分我平时用得少但我理解它大概是…”会比硬编一个答案好很多面试官也是技术人看得出诚实和应变。第二线上排查过的故障是稀缺加分项比如Full GC导致接口超时、缓存穿透把数据库打爆、Kafka消费延迟翻了几十倍。细节是你真正处理过后才能讲出来的如果只是看过文章别硬安在自己身上追问两个问题就会露馅。写在最后我自己的体会是Java面试越来越务实了。2026年这个时间点光会背答案的候选人已经很难过关面试官更愿意聊系统设计的取舍、线上故障的排坑、版本演进背后的动机。准备面试最好的方式永远是回到代码本身把常用的组件往深处再挖一层把亲手做过的坑记录下来。最后分享一个小技巧准备一套自己的“追问推演”。每道题背后至少要提前演练两个可能的追问方向。比如准备了HashMap就要顺手准备ConcurrentHashMap、红黑树退化、扩容死循环、为什么加载因子是0.75。面试时最加分的不是你背得多熟而是你能在原本的问题上主动延伸把面试官想问的下一句提前说出来。
返回列表