ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java面试翻车现场:HashMap、线程池、JVM深度拆解

Java面试翻车现场:HashMap、线程池、JVM深度拆解 “严肃面试官 vs 搞笑水货程序员谢飞机本名王大瓜——互联网大厂 Java 面试实录与技术拆解”光看这个标题你可能觉得是个段子但我在现场的感觉是这简直就是一场喜剧外壳下的技术解剖课。谢飞机简历上写着“精通 Java、熟练使用 SSH 框架、熟悉分布式高并发”结果连 HashMap 的扩容流程都说成“数据太多就换个更大的桶装”而他对面的面试官全程面无表情一个问题一个问题往深处扎愣是把这场面试问成了一场大型翻车现场加补课现场。这篇文章就把那场面试的完整过程写给你看。每一个谢飞机说错的地方我都会以面试官视角做技术拆解讲清楚正确原理、常见误区以及面试官到底想考察什么。不管你是准备跳槽的 Java 开发者还是刚开始学并发和 JVM 的新手都能从这堆“笑话”里捞出真东西。我会尽量把话说得像坐在你对面一样直白不绕弯子。1. 面试开场当严肃面试官遇上“气氛组”选手1.1 谢飞机其人其事谢飞机本名王大瓜是朋友介绍过来面试的。简历刚递到我手上我就觉得这人有意思——工作经历写了三年项目名称叫“某电商中台”结果技能列表里赫然写着“精通酒桌文化”“熟练使用 CtrlC / CtrlV”“能连续加班不猝死”。我本来想直接把这简历扔进回收站但朋友说他“人很真诚就是基础差点”我想行吧看看能差到什么程度。他进来的时候更离谱抱着一台贴满贴纸的笔记本进门第一句话是“老师我这电脑有点卡您这儿有 Wi-Fi 吗我先连个热点把我面试用的 PPT 打开。”我说不用 PPT直接聊。他愣了一下又从包里掏出一张皱巴巴的纸上面手写着几个关键词HashMap、线程池、JVM、Spring、Redis。合着这就是他的“复习提纲”。面试官示意他坐下语气平平地说“先做个自我介绍吧。”谢飞机清了清嗓子“我叫王大瓜外号谢飞机因为打游戏的时候一激动就爱在椅子上起飞。我搞 Java 三年了主攻业务开发平时爱好是研究中间件尤其对 Redis 和消息队列有点心得……”他说到“研究中间件”的时候自己都笑场了面试官没笑只是低头在评分表上写了点什么。我心里想这人至少不装很多候选人一上来就背八股文这个谢飞机倒是走真诚搞笑路线。但技术面试终归要看技术自我介绍结束后真正的考验才开始。1.2 面试官的开场提问“我看你简历写了熟悉集合框架那我问你HashMap 的底层结构是什么”谢飞机眼睛一亮明显是复习过这道题“底层是数组加链表JDK 8 以后还有红黑树”面试官点点头脸色没变追了一句“那为什么要用红黑树链表它不香吗”谢飞机愣了两秒挠头“因为……链表太长会变成长虫长虫爬得慢所以换成红黑树红黑树是平衡树找得快。”他说完自己觉得不太对又补了一句“反正就是树能快点面试题上都这么写的。”面试官沉默了三秒继续说“行那你讲讲哈希冲突是怎么解决的。如果我现在往 HashMap 里放一个 key它的下标是怎么算出来的”谢飞机“就……调 hashCode然后呢然后取模嘛模数组长度。冲突了就链在后面。这个我会”面试官“那为什么要先用高 16 位异或低 16 位直接拿 hashCode 去取模可以吗”谢飞机明显没复习到这一层张着嘴想了半晌“高十六位……低十六位……哦我知道了这是为了……为了让散列更均匀对吧嗯……反正大概就是更均匀具体怎么个均匀法我记不太清了。”面试官没给下文扭头在纸上记了一笔。我能看得出来他觉得这人有点底子但全是死记硬背一旦往下追问就漏风。2. 第一轮硬核HashMap 底层拆解从“换个更大的桶”说起2.1 谢飞机的扩容理论面试官接着问“那你再说说 HashMap 的扩容机制。”谢飞机明显只背过个关键词开始自由发挥“扩容就是数组装不下了嘛装不下了就换个更大的桶把旧桶里的东西都倒到新桶里。嗯……就是类似搬家把旧家的家具全搬到新家搬的时候还挺费劲所以扩容很慢很费性能。”他说“很慢很费性能”的时候还挺得意觉得大方向对了。面试官却抓到了一个更细的点“搬的时候需要重新计算每个节点的下标吗还是直接把原来的位置搬过去”谢飞机“那肯定要重新算啊哈希值又没变但是数组长度变了嘛……不对哈希值没变的话那下标应该是一样的啊等等……”他绕进死胡同了。这正是无数候选人栽过的地方认为 HashMap 扩容就是把元素原封不动拷贝到新数组。实际上扩容会对每个元素重新计算下标这也是为什么在并发环境下旧版本 HashMap 扩容可能形成环状链表进而导致死循环。2.2 技术拆解HashMap 扩容的真实逻辑我把这里说透。HashMap 的默认初始容量是 16负载因子是 0.75。什么时候扩容当元素个数超过容量 × 负载因子 16 × 0.75 12的时候数组会扩大到原来的两倍也就是 32。扩容最关键的点在于新下标 原下标 旧数组长度或者不变。为什么因为数组长度是 2 的 n 次幂所以 HashMap 计算下标用的是(n - 1) hash而不是取模。扩容后 n 变成 2n那二进制上相当于多出来一位这一位是 0 还是 1直接决定了元素留在原位置还是加上旧数组长度的偏移。// JDK 8 中的 resize() 相关代码片段 if ((e.hash oldCap) 0) { // 高位为 0留在原位置 loTail.next e; } else { // 高位为 1迁移到 原位置 oldCap hiTail.next e; }这就是为什么你常能看到源码注释里出现lo和hi两个链表一个用于留在低位区一个用于进入高位区。谢飞机说“重新算下标”算蒙对了方向但他不知道 JDK 8 对这条链路做了优化不再用indexFor重新取模而是直接按位与判断速度更快、更巧妙。面试官在这道题上真正想看的不是你会不会背“扩容两倍”而是你有没有理解“为什么容量总是 2 的 n 次幂”以及“扩容时如何保证元素均匀分布”。这两个底层逻辑大多数候选人说不上来。2.3 技术拆解为什么 8 就要转红黑树面试官显然不打算放过谢飞机接着问“那你有印象链表长度是几的时候转红黑树吗”谢飞机眼睛一亮这个他真背过“是 8链表长度超过 8 就转红黑树然后树的长度低于 6 就转回链表”面试官接着问“那为什么是 8 不是 9为什么降级是 6 不是 7中间为什么差一个数”谢飞机又卡住了。他大概知道 8 和 6却不知道原因是啥。这个问题其实考察的是数学概率与工程取舍。链表转红黑树的阈值定为 8来源是泊松分布。在理想随机哈希码下某个桶位链表长度达到 8 的概率约为千万分之六属于小概率事件。如果真出现了长度 8 的链表说明要么是哈希函数被恶意构造了大量冲突 key要么是数据分布极度不均匀此时再用链表线性查找效率确实无法接受所以切换成红黑树把最坏时间复杂度从 O(n) 降到 O(log n)。那为什么树退化成链表要用 6 而不是 7是为了避免频繁震荡。如果阈值是 7链表长度在 7、8 之间浮动时会反复触发树化和反树化系统永远在转换过程中消耗资源。阈值 8 和 6 之间留了缓冲保证了稳定性。这就好比空调设定 26 度启动、27 度停止中间留一个回差压缩机才不会一会儿开一会儿关。谢飞机听了这段话表情像发现了新大陆“哦——原来是为了防止空调一开一关损耗寿命”面试官第一次没有反驳他反而顿了一下。3. 第二轮火力并发编程线程池、锁升级与 CAS 的连环追问3.1 “线程池就是线程的泳池”因为 HashMap 聊出了点节奏面试官进入 Java 并发主题第一个问题就是“线程池的核心参数有哪些平时线上你一般怎么配”谢飞机一听“核心参数”立刻来劲了开始掰手指“核心线程数最大线程数队列大小……还有那个……那个空闲时间还有……聊胜于无那个拒绝策略”他边说边数七个参数凑不齐最后补了一句“反正就是线程的泳池嘛要多少线程就从池子里捞用完了再放回去当线程用完的时候就排队等。”“线程池是泳池”这个说法面试官听了倒是难得嘴角动了一下“你这比喻挺生动那你告诉我如果核心线程已经全在忙新任务提交过来会先排队还是先开新线程”谢飞机自信满满“先开新线程不然任务要等很慢的”面试官“你确定吗”谢飞机“确定”这是无数候选人都会踩的坑。线程池的核心逻辑恰恰是先填满核心线程再往阻塞队列里排队列满了才创建新线程到最大线程数最后才触发拒绝策略。谢飞机的顺序完全反了。3.2 技术拆解线程池七大参数与执行流程线程池的核心参数其实是七个corePoolSize核心线程数、maximumPoolSize最大线程数、keepAliveTime非核心线程空闲存活时间、unit时间单位、workQueue阻塞队列、threadFactory线程工厂、handler拒绝策略。它的完整执行流程我理一遍提交任务后如果当前线程数小于核心线程数直接创建核心线程执行任务。如果核心线程数已满把任务放入阻塞队列排队。如果阻塞队列也满了并且当前线程数还没达到最大线程数创建新的非核心线程执行任务。如果线程数已经达到最大线程数队列也满了走拒绝策略。拒绝策略有四种AbortPolicy直接抛异常这是默认策略CallerRunsPolicy谁提交谁执行相当于把任务踢回给提交方DiscardPolicy静默丢弃DiscardOldestPolicy丢弃队列中最老的任务。线上配置线程池最简单的方法不是照搬公式而是看任务类型。如果是 CPU 密集型任务线程数一般设为CPU 核数 1如果是 IO 密集型任务可以设大一点比如2 × CPU 核数 1因为 IO 操作时线程在等待多配一点线程能让 CPU 更忙。但更准确的做法是压测先按经验值配再渐进加压调整。面试官后来跟我复盘时说这个问题考察的就是候选人有没有真正用线程池跑过业务。背参数容易但“核心线程满了先排队而不是先开线程”这个执行顺序没写过代码的人很难意识过来。3.3 ACAS 是什么谢飞机答成“卡死”面试官继续加码“你用线程池的时候知道RejectedExecutionException什么时候抛吗”谢飞机说“不懂但我知道这个异常英文意思是‘拒绝执行异常’字面意思就是线程池不想干这活了。”面试官没搭理这个冷笑话又问“行那你说说什么是 CAS。”谢飞机想了想脸色突然认真起来“CAS 就是 Compare And Swap比较并交换这个东西在并发里用来做乐观锁改数据之前先比较一下现在值是不是我以为的值是的话就改不是就不改防止覆盖别人改完的数据。”这一段居然基本全对我和面试官都愣了一下。面试官趁热打铁“那 CAS 会有什么问题”谢飞机“问题……就是如果一直改失败就一直死循环浪费 CPU。”他居然答对了自旋问题。面试官接着问“那 ABA 问题呢”谢飞机“ABA 是那个……那个……Abandon润滑油”3.4 技术拆解CAS 底层与 ABACAS 的核心依赖是一条 CPU 原子指令比如cmpxchg。Java 层面的AtomicInteger.compareAndSet最终会通过内部方法调用到 Unsafe 类再往下是操作系统级别的原子操作。加锁会阻塞、有上下文切换成本CAS 是自旋等待不阻塞轻量得多但代价是自旋会持续占用 CPU。CAS 有个著名 bugABA 问题。简单点说线程 A 读到值是 1线程 B 把值从 1 改成 2 再改回 1线程 A 再拿 CAS 去替换时发现还是 1就认为没人动过于是成功写入了。但中间其实发生过两次变化。在某些要求严格的场景下这会导致逻辑错误。解决办法是加版本号例如AtomicStampedReference每次修改带一个时间戳或版本号值没变但版本号变了就能发现“其实动过”。谢飞机把 ABA 听成了某种润滑油品牌这一题基本无了。3.5 技术拆解synchronized 锁升级锁也有成长体系面试官又问“那 synchronized 和 CAS 哪个更重synchronized 是怎么优化的”谢飞机说“synchronized 是重量级锁肯定重因为他是同步锁哎不对现在不叫重量级了……现在不是有锁升级吗就是锁可以升级跟打怪升级一样”“锁能升级”这概念他居然知道但他根本不知道升级过程。我把这条线捋清楚无锁 → 偏向锁 → 轻量级锁 → 重量级锁。JDK 6 之后对 synchronized 做了大量优化。偏向锁同一个线程反复进入同步块时锁会在对象头里记录线程 ID后续该线程进入不需要 CAS 或加锁操作性能极高。轻量级锁一旦有其他线程来竞争偏向锁会撤销升级为轻量级锁。轻量级锁通过 CAS 抢锁抢不到就自旋默认自旋次数在 JDK 8 之后由 JVM 自适应控制。重量级锁如果自旋失败或者线程竞争太激烈就升级为重量级锁此时才真正依赖操作系统层面的互斥量会涉及用户态与内核态切换开销最大。谢飞机听完之后感慨了一句“原来锁也会成长这跟人一样小时候没心没肺后来被社会毒打就变重了。”面试官这次开始有点憋不住笑了但还在硬撑。4. 第三轮深挖JVM 与垃圾回收水货眼看“玄学”4.1 “JVM 就是把 Java 代码变成电脑能跑的东西”面试官转而问 JVM“你线上排查过 OOM 吗知道 Java 虚拟机栈里存什么吗”谢飞机的回答颇具喜剧效果“JVM 嘛就是把 Java 代码变成电脑能跑的东西的虚拟机。栈里存……存蜡烛不是栈是先进后出存方法调用嗯存局部变量还有……那个临时量”面试官“本地方法栈和虚拟机栈的区别你知道吗”谢飞机“本地方法栈那不是给本地人用的吗……就是我们土办法写的代码用的栈”他自己说完都笑了。这里把 JVM 内存区域说清楚。4.2 技术拆解运行时数据区与方法区JVM 在运行时把内存划分为几块堆、虚拟机栈、本地方法栈、程序计数器、方法区。堆存放对象实例几乎所有对象都在这里分配。GC 大本营。虚拟机栈每个线程一根栈帧链条一个方法对应一个栈帧栈帧里放局部变量表、操作数栈、动态链接、方法出口。本地方法栈给 native 方法用。像Unsafe底层的 C 方法就有本地方法栈参与。程序计数器记录当前线程执行到哪一行字节码。线程切换后要能恢复现场靠的就是它。方法区存类元信息、常量、静态变量。JDK 8 以后方法区被“元空间”替代元空间用本地内存不再受堆内存上限约束。面试官特意追问了元空间是因为很多线上服务 OOM 就出在元空间上比如频繁生成动态类或者 CGLIB 代理太多会撑爆MaxMetaspaceSize。4.3 技术拆解怎么判断对象该死GC Roots 又是什么谢飞机对垃圾回收的理解停留在“没人用了就回收”。“那 JVM 怎么判断对象没人用”面试官问。谢飞机答“引用计数看有没有人引用它没人引用就回收了”面试官“那循环引用呢两个对象互相引用但都没人用它们了你回收吗”谢飞机“这……这两个人还在互相看看着看着就都不动了那不是内存泄露吗不会吧”这个问题的正解是主流的 HotSpot JVM 没有用引用计数法而是用可达性分析。从 GC Roots 出发沿着引用链往下找能到达的对象说明还被活着引用不能到达的对象才会被标记回收。GC Roots 包括虚拟机栈中引用的对象、方法区中静态属性引用的对象、常量引用的对象、本地方法栈中 JNI 引用的对象等。循环引用这两个对象如果 GC Roots 到达不了它们即使它们互相引用也会被判定为不可达然后被回收。所以“循环引用导致内存泄漏”在 Java 里靠可达性分析已经避开了。4.4 技术拆解CMS 与 G1以及 OOM 排查第一步谢飞机又主动问了一句“面试官你平时用哪个垃圾收集器CMS 还是 G1”面试官反问“你知道 G1 对比 CMS 的优点是什么吗”谢飞机“G1 就是……垃圾更少吧CMS 是那个……并发清除。”他能说出“并发清除”已经算超常发挥了。完整结论是这样CMS 采用标记清除算法和标记整理比没有内存碎片整理过程长时间运行会产生大量碎片G1 把堆划分为多个 Region通过维护每个 Region 的回收价值和回收成本优先回收收益最大的区域停顿可预测能更好地控制最大停顿时间。面试官最后问了个最实际的问题“线上出现 OOM你的第一反应是什么”谢飞机答“第一反应是重启。”这答案在运维角度没错但面试官想听的是“先 dump 堆快照再定位内存泄漏”。正常排查流程是先jps找到进程然后用jmap -dump:formatb,fileheap.hprof pid导出堆快照再把 hprof 文件导入 MAT 或 VisualVM 分析看哪个对象撑爆了堆。如果是线程爆了则用jstack看线程状态。面试官最后评价这一段时说“他连重启都对但他只懂重启。线上问题不能全靠重启解决你重启之后根本不知道刚才到底发生了什么。”5. 第四轮实战Spring 循环依赖与 Redis 三兄弟气氛最高潮5.1 “三级缓存是电脑、硬盘和网盘”面试官最后转向框架和中间件“Spring 怎么解决循环依赖的说说三级缓存。”谢飞机眼睛发直“循环依赖就是 A 要注入 BB 又要注入 A这俩互相要就跟互相等对方先说话一样谁都不先开口就僵住了。”他这比喻居然还挺准确实是 Bean 创建陷入“你先要我我也要你”的僵局。面试官“那 Spring 用什么解决的”谢飞机“三级缓存第一级是内存第二级是硬盘第三级是网盘数据一多就往上放……”他说到一半自己也觉得不对补了一句“不对缓存三级……是那个……L1 L2 L3Cache”全场第一笑声就这么出现了不过谢飞机是红着脸笑的。5.2 技术拆解三级缓存到底“缓存”了什么真实的三级缓存是三个 Map一级缓存singletonObjects存放完全创建好的单例 Bean。二级缓存earlySingletonObjects存放早期暴露的半成品 Bean已经实例化但还没完成属性填充。三级缓存singletonFactories存放单例 Bean 的工厂对象能在需要时提前返回一个早期引用。为什么必须三级而不是两级核心在于AOP 代理对象需要提前暴露。如果没有任何切面代理二级缓存就够用了。但 Spring 里很多 Bean 会被 AOP 生成代理对象代理对象是在 Bean 实例化之后、属性填充之后、初始化阶段才通过AbstractAutoProxyCreator生成的。如果只靠二级缓存循环依赖发生的时候把原始对象暴露给 B 了后续再也无法替换成代理对象就出问题。有了第三级工厂缓存可以在提前暴露的时候就已经介入判断是否需要生成代理对象保证注入到 B 里的是符合 AOP 语义的最终对象。谢飞机听我讲完三级缓存像听天书一样最后怯怯地问了一句“那这三级缓存能手动关掉吗”面试官说“可以关但你做好心理准备项目一启动就报BeanCurrentlyInCreationException到时候你可不止谢飞机你直接谢地。”5.3 技术拆解缓存穿透、击穿、雪崩别再张冠李戴面试官又问“Redis 缓存穿透、击穿、雪崩分别是什么意思怎么解决”谢飞机这次倒是来劲了“穿透就是流量太猛了把缓存打穿了击穿就是打穿了……雪崩那就是像雪一样崩了太多数据同时过期全崩了。”他三个概念全混在一起面试官让他分别说说怎么应对他突然沉默然后掏出了他的纸条纸条上写满了“布隆过滤器”“互斥锁”“逻辑过期”但没有一个他能完整解释。这里我把三兄弟彻底分开缓存穿透客户端查一个根本不存在的数据缓存和数据库都没有请求直接打到数据库恶意刷接口能把数据库打瘫痪。解决思路布隆过滤器把所有可能存在的数据 key 提前写入位图查询前先看 key 是否在位图里不在就直接返回根本不去查库另外也可以缓存空值给空值设一个很短的过期时间。缓存击穿某个热点 key 在过期的一瞬间大量并发请求发现缓存没有就一起打到数据库。解决思路互斥锁让只有一个线程去查数据库并重建缓存其他线程等待或者用逻辑过期不设置真实过期时间而是把过期时间作为一个业务字段放在 value 里后台异步刷新。缓存雪崩大量 key 在同一段时间集中过期或者 Redis 直接宕机导致请求全部砸向数据库。解决思路给过期时间加随机值避免同一时刻集体失效使用多级缓存做兜底Redis 高可用架构保证缓存层不挂。谢飞机听完直拍大腿“原来这三个不是一回事我面之前都背混了。”面试官冷冷地接了一句“面完之后再背也行。”5.4 现场追问分布式锁与 Redisson最后一个问题来自 Redis 的分布式锁。面试官问“单体项目用 synchronized 就够了分布式项目呢”谢飞机答“那肯定用 RedissonRedis 锁‘tryLock’还有那个看门狗续期”他忍不住补了一句“这个我真熟因为我们项目里就抢过红包。”能说出 Redisson 和看门狗说明谢飞机至少听说过分布式锁。但他不知道底层是 Lua 脚本加过期时间也不知道看门狗默认续期是 30 秒更不知道 Redis 主从模式下锁可能丢。面试官最后点评“分布式锁你有概念但没有深度回去把 Redisson 的源码当小说看一遍印象就深了。”6. 全场复盘哪些错误可以原谅哪些是硬伤6.1 谢飞机的“歪打正着”面试结束后我和面试官复盘了半个多小时。客观来说谢飞机不是纯水货他有几个地方展现了不错的直觉。比如他猜“HashMap 扩容要重新计算下标”、他能说出“CAS 是乐观锁”、他甚至还知道“锁可以升级”说明他平时没少刷面经也有一定的技术嗅觉。问题在于所有理解都停留在“名词大概意思”层面只要往下追问两三层就全部坍塌。这类候选人很典型做业务能干活但不求甚解出了问题第一反应是百度或者重启。项目调优经验也基本靠同事带。6.2 必须补的底子我给谢飞机列了一份“打回重练”清单这里也分享给你集合底层重点看 HashMap 的 put、resize、get 全流程以及 ConcurrentHashMap 的 JDK 8 实现是怎么用 CAS synchronized 锁桶实现并发安全的。JVM内存结构、对象创建过程、GC Roots、可达性分析、常见 GC 收集器、OOM 排查工具和方法。并发synchronized 锁升级过程、ReentrantLock 与 synchronized 的差别、CAS 原理和 ABA、ThreadLocal 原理与内存泄漏问题。SpringBean 生命周期、循环依赖三级缓存、事务失效场景、BeanFactory 与 ApplicationContext 的关系。MySQL索引结构为什么用 B 树、最左前缀原则、慢查询优化、事务隔离级别与 MVCC。Redis五种数据结构底层编码、持久化机制、缓存穿透/击穿/雪崩解决方案、分布式锁的正确实现。这六块是 Java 后端面试的“基础六件套”看起来多但每一块只要肯花时间读源码、画流程、写 demo两三个月能补扎实。我的建议是不要只看面经背结论。面经是结果源码才是原因。问自己一个问题HashMap 为什么链表转红黑树阈值选 8如果你只能说“因为源码里写 8”那你也只是谢飞机。6.3 面试官真正的评判标准面试结束后我问面试官会给什么等级。他说“C但这个人我要了。”我很意外他说“你听他说了一整场虽然一堆东西不会但他态度松弛、不懂就说而且聊到哈希分布和锁升级的时候他眼睛是亮的这种人对技术有好奇感教得出来。比起那种背了一百道题但眼神空洞的候选人我更愿意带一个能聊的人。”这让我挺有触动。技术面试看起来是在问知识点实际上是在测两件事一是你现有知识的地基牢不牢二是你有没有持续吸收新东西的能力。谢飞机在地基上漏水但吸收能力在线所以面试官愿意赌一把。7. 面试高频问题速查与备考建议7.1 高频问题与正确回答方向我把当天出现的题目按“谢飞机式错误答案”和“应该有的回答方向”整理成一张速查表方便你自检。题目谢飞机式错误答案正确回答方向HashMap 扩容换个更大的桶原样搬过去容量变两倍用(e.hash oldCap)判断元素留原位还是迁移到原位置oldCap为什么 8 转红黑树因为是树会快泊松分布概率 最坏时间复杂度优化 8 和 6 之间留缓冲避免频繁树化线程池执行顺序核心线程满了就开新线程核心线程 → 阻塞队列 → 最大线程数 → 拒绝策略CAS 的缺点不知道自旋占用 CPU、只能保证一个变量原子性、存在 ABA 问题synchronized 优化是重量级锁偏向锁 → 轻量级锁自旋 → 重量级锁的升级过程循环引用能否被 GC 回收不能回收泄漏可达性分析从 GC Roots 出发不可达即回收循环引用不影响OOM 排查重启jpsjmap dump MAT 分析堆快照Spring 循环依赖三级缓存是内存/硬盘/网盘三级缓存是singletonObjects、earlySingletonObjects、singletonFactories第二三级解决 AOP 代理提前暴露问题缓存穿透/击穿/雪崩三者全混穿透用布隆过滤器/空值缓存击穿用互斥锁/逻辑过期雪崩用随机过期时间/多级缓存分布式锁只知道 Redisson加锁本质是设置过期 key底层 Lua 脚本保证原子性看门狗续期主从切换可能丢锁7.2 MySQL 与索引的备忘录虽然当天没聊到 MySQL但准备大厂 Java 面试离不开它这里顺带提一句。索引选 B 树而不是 B 树或者红黑树根本原因是 B 树的非叶子节点不存数据只在叶子节点存数据因此非叶子节点能存更多索引项树更矮、磁盘 IO 更少。而且 B 树叶子节点之间有链表范围查询只要扫链表就行不需要中序遍历整棵树。面试考索引多半会接着问最左前缀原则、联合索引怎么建、覆盖索引和回表这些需要配合EXPLAIN的输出实际分析一遍才记得牢。慢 SQL 优化的顺序也别搞反先打开EXPLAIN看type、key、rows、Extra判断是否全表扫描、是否用了文件排序、有没有回表再考虑加索引、改写 SQL、分页优化、或者引入缓存。7.3 面试心态与表达训练我见过太多面试者一进会议室就开始紧张明明知道的问题也说不上来。谢飞机的面试表达其实有值得学习的地方他说话不端着快速回应哪怕错了也先把思路抛出来。这种表达方式在面试官眼里比沉默 30 秒然后背出一段标准答案要加分不少。当然光有松弛不够更建议你从今天开始做三件事每周深度读一个源码类问题从问题出发看到实现细节比如“ConcurrentHashMap 为什么读不用加锁”“Spring 事务为什么会失效”读完用自己的话写成一篇笔记。准备一套“项目故事线”不要泛泛说“我做了电商系统”要说清楚背景、任务、动作、结果重点突出遇到的技术难点和你当时的排查路径。找个朋友做模拟面试专挑你不熟悉的知识点往下追问问到你答不出来为止。这个过程很痛苦但比背 100 道题有效得多。写在最后谢飞机后来真被录用了。第一个月他在工位上天天抱着源码敲敲打打有一次半夜我在群里看到他说“原来 ConcurrentHashMap 的扩容是别人帮我搬数据我说我怎么越搬越少。”面试官在群里回了一句“你现在才懂我面你那天就知道你迟早会懂。”我常想起这场面试。它看起来像一场喜剧一个“搞笑水货程序员”在严肃面试官面前不断翻车但实际上它把技术面试最真实的一面摊开在桌面上面试官问的不是答案而是候选人对技术的态度。谢飞机缺的是基础但不缺好奇心这一点让面试官愿意给他一张入场券。如果你正处于准备跳槽的阶段试着别把自己当成“背题机器”而是把自己当成一个还在成长的技术人。基础不牢没关系补就是了但如果没有那股“想知道底层到底怎么跑”的劲头面试官很难在你身上看到未来。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进