Java面试必备:从基础到微服务全面梳理
面试Java岗位你到底在怕什么是八股文背得不够熟还是项目经验被面试官一句话问穿真相是大多数Java开发者面挂的原因不是技术不够深而是对底层逻辑的认知停留在“会用”层面。面试官问“HashMap为什么线程不安全”你答“因为put的时候可能死循环”——OK那你告诉我JDK 1.7和1.8分别是怎么死循环的扩容时链表头插法和尾插法的区别导致了什么问题一旦追到这种粒度七成人直接哑火。本文不打算罗列知识点清单那不是深度文该干的事。我将以面试官追问的链条为骨架把从Java基础到微服务架构中真正值得你死磕的“尖刀问题”逐一拆解。每个节点只会给你最锋利的那把刀而不是一麻袋钝刀。基础篇那些你以为自己会的“简单问题”面试官最爱从“String是值传递还是引用传递”这种问题开刀然后一路刺向JVM内存模型。实际上围绕String、Integer、Object三者的内存布局就能筛掉60%的候选人。请听好String s abc和String s new String(abc)分别创建了几个对象答案不是1和2那么简单。关键在于字符串常量池在JDK 7之后被移到了堆中并且new String(abc)至少会创建两个对象一个在堆上的String实例一个在常量池中的char[]。但如果你直接写a b c编译器会优化为abc只创建1个对象——这种编译期常量折叠是JVM的杀手锏。再往下追问Integer a 127; Integer b 127; a b结果是什么Integer c 128; Integer d 128; c d呢前者true后者false。关键在IntegerCache的缓存范围是-128到127超过这个范围每次都会new新对象。面试官接着问那为什么Long也有缓存但范围是-128到127Double为什么没有答案是整数在业务中高频使用的范围是可预期的而浮点数的比较本身就不推荐使用。这个设计哲学暴露了Java对语言易用性与性能的折中。真正能把你钉在墙上的问题其实是Object o new Object()在内存中占多少字节你以为就是对象头实例数据错了。在64位系统上开启指针压缩时普通对象占16字节12字节对象头4字节对齐而数组对象额外多4字节长度。如果类里有引用类型字段开启压缩后引用只占4字节否则占8字节。面试官甚至可能问为什么对象头里除了Mark Word还有Klass Pointer因为Mark Word存放锁信息、GC标记等运行时数据而Klass Pointer指向方法区的类元信息。没有这个指针JVM就不知道这个对象到底是哪个类的实例。集合篇HashMap源码就是你的照妖镜HashMap是面试中唯一一个能让你在5分钟内被问到怀疑人生的类。我要你掌握的是当两个线程同时执行put操作且触发了扩容时JDK 1.7的头插法会导致环形链表。具体流程线程A在扩容时先复制了部分数据线程B抢到CPU完成整个扩容此时A继续使用旧的next指针导致链表成环——下次get(key)时无限循环CPU飙到100%。JDK 1.8改用尾插法从根本上避免了环形链表但HashMap依然不是线程安全的因为put时判断size超过threshold触发的resize存在可见性问题。再问一个你绝对答不完整的问题为什么HashMap的负载因子默认是0.75这是一个时间和空间的折中。负载因子越高如1.0空间利用率高但链长增加查询时间变长负载因子越低如0.5空间浪费大但哈希冲突少。0.75这个值来自泊松分布的理论计算当负载因子为0.75时链表长度达到8的概率已经极低约0.00000006这就是为什么链表长度超过8会转红黑树——因为超过8的概率几乎可以忽略但为了防止极端恶意哈希攻击还是做了兜底。ConcurrentHashMap 1.8为什么抛弃了分段锁因为有同学还在背老版本。1.8直接使用synchronized CAS 红黑树锁粒度从Segment降级为Node即数组的每个桶。只有真正发生哈希冲突的桶才需要加锁而且读操作完全不用加锁——volatile保证数组引用的可见性而Node的val和next用volatile修饰。这才叫高并发设计用最少的锁覆盖最关键的路径。多线程与并发别让synchronized和volatile成为你的软肋面试多线程时80%的人能背出“synchronized是重量级锁JDK 1.6优化后引入了偏向锁、轻量级锁”。但你再问偏向锁在什么情况下会被撤销撤销过程需要STW吗答不上了吧。简单说当有其他线程尝试竞争偏向锁时偏向锁会升级成轻量级锁。撤销过程需要等待全局安全点SafePoint此时所有线程都暂停STW然后检查持有偏向锁的线程是否存活——如果不存活直接重置为无锁状态如果存活则升级为轻量级锁。这就是为什么高并发场景下偏向锁性能反而更差因为撤销的STW开销可能超过锁自旋。关于volatile很多人只知道“保证可见性不保证原子性”。但你有没有想过volatile到底是如何保证可见性的答案在JMM中volatile写操作会在汇编层插入一个lock前缀指令该指令会触发两件事1强制将处理器缓存写回主内存2写回操作会使其他CPU的缓存行失效通过缓存一致性协议MESI。所以volatile的可见性本质上不是靠JVM而是靠硬件层面的缓存一致性协议。再放一个狠问题ThreadLocal的内存泄漏到底怎么回事核心原因是ThreadLocalMap中的Entry继承了WeakReference但其Key是弱引用Value是强引用。如果ThreadLocal对象Key被GC回收但Thread还在运行那么Entry中的Value就会一直存在且因为Key为null而永远无法被访问到。这就是内存泄漏的根源。解决方案很简单用完ThreadLocal后调用remove()方法手动清除Value引用。JVM篇面试官最喜欢的“调优现场”“你的系统CPU飙升到100%怎么排查”——这是JVM高频题。但面试官想听的不是“先top -H看哪个线程再jstack看堆栈”而是你如何区分是业务死循环还是GC线程频繁。如果是GC线程你需要用jstat -gcutil PID 1000 10观察GC频率如果Young GC每100ms甚至更短就发生一次且Old区不断增长那就是内存泄露导致的对象晋升太快。你甚至应该知道CPU高不一定是坏事如果全是用户业务线程在高效运算那是正常的但如果是GC线程占了50%以上那就要做Heap Dump分析。再问一个穿透力极强的问题JVM的垃圾收集器怎么选型的别只背口诀“响应优先用CMS吞吐优先用Parallel”。实际上在JDK 8中-XX:UseConcMarkSweepGC之后会默认激活ParNew作为新生代收集器而ParNew的线程数默认与CPU核心数相同这在服务器上可能导致竞争。更关键的是CMS的“浮动垃圾”问题在并发标记阶段用户线程还在产生新垃圾这些垃圾必须在下一次FULL GC时才能被回收。如果老年代预留空间不足就会发生Concurrent Mode Failure退化为Serial Old单线程收集STW时间飙升。因此生产环境中通常把CMSInitiatingOccupancyFraction设为68%-75%提前触发CMS。有一个99%面试官说不清楚的问题G1的Region是怎么划分的G1把堆分成多个大小相等的Region默认约2048个每个Region大小在1M-32M之间通过-XX:G1HeapRegionSize设定。G1的垃圾回收不仅是分代更是混合回收Mixed GC它并不是回收整个新生代老年代而是根据每个Region的存活对象占比和回收收益选择一批Region来清理。这就是“停顿时间可预测”的核心G1可以设定最大STW时间-XX:MaxGCPauseMillis然后在这个时间窗口内选择收益最高回收空间/时间的Region进行回收。这本质上是一个成本收益模型而非固定的全堆扫描。Spring篇IOC和AOP的“骨头”在哪里面试Spring的时候很多人能流畅讲出IOC是依赖注入AOP是面向切面。但一旦面试官问“Spring是如何解决循环依赖的为什么构造函数注入的循环依赖无法解决”大半人就卡壳了。答案核心在三级缓存singletonObjects一级、earlySingletonObjects二级、singletonFactories三级。A需要BB需要A。创建A时先实例化A对象但属性还没填充将A的工厂函数ObjectFactory放入三级缓存然后A开始给属性填充发现需要B于是去创建B。B实例化后需要填充A属性此时从三级缓存中找到A的工厂函数调用getEarlyBeanReference获得A的早期引用可能经过AOP代理放入二级缓存。B创建完成A拿到B的引用继续初始化。最后A完成初始化将最终引用放入一级缓存。为什么构造器注入无法解决循环依赖因为构造器在实例化阶段就必须传入参数此时对象还没创建出来无法放入三级缓存——也就是说Spring在实例化A时发现需要B的构造参数但B还没创建也无从谈早期引用直接报错。AOP的底层是动态代理。Spring AOP默认使用JDK动态代理基于接口如果目标类没有实现接口则使用CGLIB基于子类。但有一个坑CGLIB通过比子类方式增强this调用方法时无法触发增强因为调用的是目标对象本身的方法而非代理对象的方法。这就是为什么Spring官方建议如果使用CGLIB代理最好不要在同一个类的方法内部调用另一个被增强的方法否则AOP不生效。解决办法是自己注入代理对象比如通过ApplicationContextAware获取。微服务篇别再只背服务注册与发现的八股了微服务面试现在更看重架构决策背后的取舍。比如“为什么要用Netflix Eureka而不用Zookeeper做服务注册中心”Eureka遵循AP原则可用性分区容忍性Zookeeper遵循CP原则一致性分区容忍性。在微服务场景中服务发现对一致性的要求没那么高即使注册信息有一两秒的延迟也能接受但如果Zookeeper发生leader选举整个注册中心不可用服务将完全瘫痪。Eureka的设计是自我保护模式当心跳丢失超过85%时Eureka不会立即摘除服务而是保护现有注册信息宁可放过也不杀错。这才是分布式系统设计的精髓选择适合场景的牺牲。再问Spring Cloud Gateway和Zuul 1.x的区别到底在哪很多人只回答“Gateway基于WebFlux异步非阻塞Zuul 1.x基于Servlet同步阻塞”。但我想听的是WebFlux的底层是Netty使用Reactor编程模型线程模型是Event-Loop一个连接对应一个线程压测下资源占用远低于Zuul。而Zuul 1.x本质是每个请求一个线程在高并发下线程池膨胀上下文切换开销巨大极易OOM。所以Gateway更适合网关场景而Zuul 2.x虽然也改成异步但Netflix已经放弃维护。分布式事务是微服务面试的终极大魔王。别再说“用Seata AT模式解决一切”。面试官想听的是Seata AT模式本质是两阶段提交2PC的优化第一阶段执行SQL并记录undo log第二阶段提交或回滚。但2PC最大的问题是资源锁定在prepare阶段数据库行记录被排他锁锁住直到commit或rollback才释放。高并发下会严重降低吞吐量。真正的生产方案往往是混合使用核心链路用TCCTry-Confirm-Cancel非核心链路用最大努力通知本地消息表。TCC的难点在于Cancel和Confirm可能空回滚、幂等性处理——比如订单服务Try时冻结了库存如果Try失败网络异常返回超时Cancel需要识别出这笔Try根本没发生从而执行空回滚。总结面试不是背八股而是展示你的“底层思考”Java面试的本质是看你能否在表面的技术名词背后理解设计者的权衡与取舍。为什么HashMap用红黑树而不是AVL树因为红黑树的旋转次数更少插入删除性能更好。为什么Spring用三级缓存而不是二级因为二级缓存无法解决AOP代理对象的提前创建问题。为什么Eureka选择AP因为服务发现的核心场景中短暂的不一致优于不可用。当你下次面试再被问到“请说说你对xxx的理解”时请你以“这个问题本质上是一个XXX权衡”开头然后从数据结构、JVM、设计模式、分布式理论四个维度去拆解。面试官听到你这种回答会忍不住在心里给你打上“此人具有架构洞察力”的标签。最后送你一句我自己的座右铭不要只做Java的熟练工要做Java的“解剖者”。每一次追问到底都是一次技艺的跃迁。你准备好了吗