Java 核心与 JVM - 架构师面试题库

侧重Java并发、JVM调优、类加载机制、内存模型等深水区,考察候选人对Java底层原理的理解和生产环境问题排查能力。


一、JVM 内存模型与垃圾回收(1-25题)

1. 🔵 请描述JVM的内存结构。堆、栈、方法区、直接内存各自存储什么?JDK8之后方法区有什么变化?

答:JVM内存结构:

  • 堆(Heap):对象实例和数组,GC主要管理区域。分为新生代(Eden+S0+S1)和老年代。
  • 虚拟机栈(VM Stack):每个线程私有,每个方法调用创建一个栈帧(局部变量表、操作数栈、动态链接、返回地址)。
  • 本地方法栈:Native方法使用。
  • 程序计数器:当前线程执行的字节码行号,线程私有。
  • 方法区:类信息、常量、静态变量。JDK8之前用永久代(PermGen)实现,大小固定易OOM。JDK8改为元空间(Metaspace),使用本地内存,默认不限大小(可通过MaxMetaspaceSize限制)。
  • 直接内存:NIO的DirectByteBuffer分配,不受GC直接管理,通过Cleaner机制回收。Netty大量使用。

2. 🔴 请详细描述G1垃圾回收器的工作原理。它是如何实现可预测的停顿时间的?

答:G1将堆划分为大小相等的Region(默认2048个,每个1-32MB)。每个Region可以是Eden、Survivor、Old或Humongous(大对象,超过Region一半)。核心机制:

  • Remembered Set(RSet):每个Region维护一个RSet,记录其他Region中指向本Region的引用,避免全堆扫描。
  • Collection Set(CSet):每次GC选择回收价值最高的Region集合(垃圾最多的优先,即Garbage First)。
  • 可预测停顿:用户设置目标停顿时间(-XX:MaxGCPauseMillis,默认200ms),G1根据历史数据预测每个Region的回收时间,选择在目标时间内能回收的Region。
  • Mixed GC:同时回收新生代和部分老年代Region。
  • 并发标记:三色标记法+SATB(Snapshot-At-The-Beginning)写屏障处理并发标记期间的引用变化。
    调优关键:Region大小、目标停顿时间、InitiatingHeapOccupancyPercent(触发并发标记的堆占用比例,默认45%)。

3. 🔵 ZGC和Shenandoah的设计目标是什么?它们是如何实现亚毫秒级停顿的?

答:ZGC(JDK11+)设计目标:停顿时间不超过1ms,不随堆大小增长。核心技术:

  • 染色指针(Colored Pointers):在64位指针中使用4个bit标记对象状态(Marked0、Marked1、Remapped、Finalizable),无需额外数据结构。
  • 读屏障(Load Barrier):读取引用时检查指针颜色,如果需要则自愈(修正指针指向新地址)。
  • 并发整理:几乎所有阶段都是并发的,只有初始标记和再标记有短暂STW(<1ms)。
    Shenandoah(RedHat,JDK12+):类似目标,使用Brooks Pointer(转发指针)实现并发整理,每个对象头部额外一个指针指向自己(未移动时)或新位置(移动后)。读写屏障都需要检查转发指针。
    选型:JDK17+推荐ZGC(成熟度更高),大堆(>4GB)场景优势明显。

4. 🔴 生产环境中遇到Full GC频繁,你的排查思路是什么?请描述完整的排查和调优过程。

答:排查步骤:

  1. 确认现象:GC日志分析(-Xlog:gc*),关注Full GC频率、耗时、回收前后堆大小。工具:GCViewer、GCEasy。
  2. 定位原因
    • 老年代不足:对象晋升过快(Survivor太小或年龄阈值太低)、大对象直接进老年代。
    • 内存泄漏:每次Full GC后老年代占用持续增长。用MAT分析堆dump(jmap -dump或OOM时自动dump)。
    • Metaspace不足:动态生成类过多(如大量使用反射、CGLIB代理)。
    • System.gc()调用:代码或框架显式调用(-XX:+DisableExplicitGC禁用)。
  3. 调优方案
    • 增大堆/调整新生代老年代比例。
    • 切换GC算法(CMS→G1→ZGC)。
    • 修复内存泄漏(常见:静态集合、未关闭的连接、ThreadLocal未清理、监听器未注销)。
    • 对象池化减少GC压力。

5. 🔵 什么是JVM的安全点(Safepoint)和安全区域(Safe Region)?它们对GC停顿有什么影响?

答:安全点:JVM能够暂停线程进行GC的位置。不是任意位置都能暂停(如正在修改对象引用的中间状态)。安全点位置:方法调用、循环回边、异常跳转等。线程到达安全点后主动挂起。问题:如果某个线程长时间不到达安全点(如可数循环int i=0;i<大数;i++),会导致其他线程等待,延长STW时间。JDK10的Loop Strip Mining优化了这个问题。安全区域:线程处于Sleep或Blocked状态时,不能主动走到安全点,但这些状态下引用关系不会变化,标记为安全区域,GC可以直接进行。线程离开安全区域前需要检查GC是否完成。

6. 🔴 什么是JIT编译器?C1和C2编译器有什么区别?Graal编译器有什么优势?

答:JIT(Just-In-Time):将热点代码编译为本地机器码,提高执行效率。

  • C1(Client Compiler):编译速度快,优化程度低。适合启动速度要求高的场景。优化:方法内联、常量折叠、空值检查消除。
  • C2(Server Compiler):编译速度慢,优化程度高。适合长期运行的服务端应用。优化:逃逸分析、标量替换、锁消除、循环展开。
  • 分层编译(Tiered Compilation,JDK8默认):先用C1快速编译,热点方法再用C2深度优化。5个层级:解释执行→C1无profiling→C1有限profiling→C1完整profiling→C2。
  • Graal编译器:用Java写的JIT编译器(替代C2),更易扩展和维护。GraalVM的核心组件。支持AOT编译(Native Image),启动时间毫秒级,内存占用小,适合Serverless/云原生场景。

7. 🔵 什么是逃逸分析?它能带来哪些优化?在什么情况下逃逸分析会失效?

答:逃逸分析判断对象的作用域是否超出方法或线程。三种逃逸状态:不逃逸、方法逃逸、线程逃逸。优化:

  • 栈上分配:不逃逸的对象在栈上分配,方法结束自动回收,无需GC。(HotSpot实际通过标量替换实现)
  • 标量替换:将对象拆解为基本类型变量,直接在栈上使用。
  • 锁消除:不逃逸的对象上的同步操作可以消除(如StringBuffer在方法内使用)。
    失效情况:1)对象被赋值给成员变量或静态变量;2)对象作为方法参数传递(可能逃逸);3)对象存入集合;4)JIT编译器无法确定是否逃逸时保守处理。-XX:+DoEscapeAnalysis开启(JDK8默认开启)。

8. 🔴 请描述CMS垃圾回收器的工作流程。它有哪些已知问题?为什么JDK14移除了CMS?

答:CMS(Concurrent Mark Sweep)流程:

  1. 初始标记(STW):标记GC Roots直接引用的对象,速度快。
  2. 并发标记:从GC Roots遍历整个对象图,与用户线程并发执行。
  3. 重新标记(STW):修正并发标记期间变化的引用(增量更新),比初始标记慢。
  4. 并发清除:清除未标记对象,与用户线程并发。
    问题:1)CPU敏感,并发阶段占用CPU资源;2)浮动垃圾(并发清除阶段产生的新垃圾需要下次GC处理);3)内存碎片(标记-清除不整理),导致大对象分配失败触发Full GC(Serial Old,单线程,停顿时间长);4)Concurrent Mode Failure(并发GC来不及回收,退化为Serial Old)。JDK14移除原因:G1全面优于CMS,维护成本高。

9. 🔵 什么是类加载机制?双亲委派模型的原理是什么?什么场景下需要打破双亲委派?

答:类加载过程:加载→验证→准备→解析→初始化。双亲委派:类加载请求先委托给父加载器,父加载器无法加载时才自己加载。层次:Bootstrap ClassLoader(rt.jar)→Extension ClassLoader(ext目录)→Application ClassLoader(classpath)→自定义ClassLoader。目的:保证核心类库安全(如java.lang.String只能由Bootstrap加载)。打破双亲委派的场景:

  • SPI机制:JDBC的DriverManager在Bootstrap ClassLoader中,但需要加载classpath下的驱动实现。通过Thread.currentThread().getContextClassLoader()获取应用类加载器。
  • 热部署:Tomcat每个Web应用有独立的WebAppClassLoader,优先加载自己的类(打破委派)。
  • 模块化:OSGi的网状类加载模型。
  • Java 9 Module System:模块间的可见性控制。

10. 🔴 什么是Java Agent?如何利用Instrumentation API实现字节码增强?在APM系统中如何应用?

答:Java Agent:通过-javaagent参数在JVM启动时或运行时(Attach API)加载的特殊jar包。核心接口:Instrumentation,提供addTransformer方法注册ClassFileTransformer,在类加载时修改字节码。实现方式:

  • premain:JVM启动时加载,public static void premain(String args, Instrumentation inst)
  • agentmain:运行时通过Attach API动态加载。
    字节码增强工具:ASM(底层,性能好)、Byte Buddy(高级API,易用)、Javassist(基于源码级API)。APM应用:SkyWalking/Pinpoint用Java Agent自动埋点,在方法入口和出口插入计时代码,记录调用链、响应时间、异常信息。无需修改业务代码。关键:Agent不能影响应用性能(异步上报、采样)。

11. 🔵 HashMap的底层实现原理是什么?JDK8做了哪些优化?为什么线程不安全?

答:JDK8的HashMap:数组+链表+红黑树。put过程:1)计算hash(高16位异或低16位,减少碰撞);2)(n-1) & hash定位桶;3)桶为空直接放入;4)桶不为空,链表遍历,key相同则覆盖,不同则尾插(JDK7头插,多线程下会形成环导致死循环);5)链表长度≥8且数组长度≥64时转红黑树(<6时退化为链表)。扩容:容量翻倍,rehash时利用hash & oldCap判断元素在原位置还是原位置+oldCap(巧妙避免重新计算hash)。线程不安全:1)并发put可能丢失数据(两个线程同时写入同一个桶);2)并发扩容可能导致数据丢失或死循环(JDK7)。并发场景用ConcurrentHashMap。

12. 🔴 ConcurrentHashMap在JDK7和JDK8中的实现有什么区别?JDK8是如何实现高并发的?

答:

  • JDK7:Segment数组(默认16个)+ HashEntry数组 + 链表。每个Segment是一个ReentrantLock,锁粒度为Segment级别。并发度=Segment数量。
  • JDK8:Node数组 + 链表/红黑树。锁粒度细化到桶级别(synchronized锁住链表头节点)。核心操作:
    • put:CAS写入空桶;非空桶synchronized锁住头节点后遍历插入。
    • size:baseCount + CounterCell数组(类似LongAdder的分段计数),避免全局锁。
    • 扩容:多线程协助扩容(transferIndex分配迁移任务),每个线程负责一段桶的迁移。ForwardingNode标记已迁移的桶。
    • get:无锁,volatile保证可见性。Node的val和next都是volatile。
      JDK8的优势:锁粒度更细、红黑树优化长链表、多线程扩容。

13. 🔵 什么是ThreadLocal?它的实现原理是什么?为什么会导致内存泄漏?如何避免?

答:ThreadLocal为每个线程提供独立的变量副本。实现:每个Thread对象有一个ThreadLocalMap(自定义HashMap),key是ThreadLocal对象(弱引用),value是存储的值。get/set操作都是操作当前线程的ThreadLocalMap。内存泄漏原因:ThreadLocalMap的key是弱引用,GC后key变为null,但value是强引用不会被回收,形成key=null但value存在的”脏Entry”。如果线程长期存活(如线程池),这些value永远不会被回收。避免:1)使用完后调用remove();2)ThreadLocalMap在get/set时会清理key=null的Entry(启发式清理),但不能完全依赖。最佳实践:try-finally中remove,或使用TransmittableThreadLocal(阿里开源,支持线程池场景的值传递)。

14. 🔴 请描述Java的SPI机制。ServiceLoader的实现原理是什么?它有什么局限性?Dubbo的SPI做了哪些增强?

答:SPI(Service Provider Interface):在META-INF/services/目录下放置接口全限定名的文件,文件内容为实现类全限定名。ServiceLoader.load()读取文件并实例化。原理:使用线程上下文类加载器加载实现类(打破双亲委派)。局限性:1)一次性加载所有实现类,不能按需加载;2)没有IOC和AOP能力;3)不支持依赖注入。Dubbo SPI增强:1)按需加载(key-value形式,如dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol);2)@Adaptive注解实现自适应扩展(运行时根据URL参数选择实现);3)@Activate注解实现条件激活;4)Wrapper机制实现AOP(装饰器模式);5)依赖注入(setter注入其他扩展点)。

15. 🔵 什么是Java的强引用、软引用、弱引用、虚引用?各自的应用场景是什么?

答:

  • 强引用Object obj = new Object(),GC不会回收,即使OOM也不回收。
  • 软引用(SoftReference):内存不足时才回收。适合缓存(如图片缓存),OOM前会被清理。
  • 弱引用(WeakReference):下次GC时一定回收。ThreadLocalMap的key、WeakHashMap。适合非必需的辅助数据。
  • 虚引用(PhantomReference):无法通过虚引用获取对象,唯一作用是在对象被回收时收到通知(通过ReferenceQueue)。DirectByteBuffer的Cleaner就是虚引用的应用,用于释放堆外内存。
    引用队列(ReferenceQueue):软/弱/虚引用关联的对象被回收后,引用对象会被加入队列,可以做清理工作。

16. 🔴 什么是Java的内存屏障?在不同CPU架构下有什么区别?JVM是如何抽象内存屏障的?

答:内存屏障是CPU指令,用于控制指令重排序和内存可见性。JVM定义4种屏障:

  • LoadLoad:Load1; LoadLoad; Load2 → Load1在Load2之前完成。
  • StoreStore:Store1; StoreStore; Store2 → Store1在Store2之前对其他处理器可见。
  • LoadStore:Load1; LoadStore; Store2 → Load1在Store2之前完成。
  • StoreLoad:Store1; StoreLoad; Load2 → Store1对所有处理器可见后才执行Load2。最强也最昂贵。
    CPU架构差异:x86是强内存模型(TSO),只需要StoreLoad屏障(lock前缀指令);ARM/RISC-V是弱内存模型,需要更多屏障。JVM通过在字节码层面插入屏障指令,由JIT编译器根据目标CPU架构生成对应的机器指令。volatile写 = StoreStore + StoreLoad,volatile读 = LoadLoad + LoadStore。

17. 🔵 什么是Java的锁升级过程?偏向锁、轻量级锁、重量级锁的原理是什么?

答:JDK6引入锁升级优化synchronized性能。对象头的Mark Word存储锁状态:

  • 无锁:对象hashCode、GC年龄、锁标志01。
  • 偏向锁:第一个获取锁的线程ID写入Mark Word,后续该线程进入同步块无需CAS。适合单线程重复获取锁。JDK15默认关闭偏向锁(-XX:-UseBiasedLocking)。
  • 轻量级锁:有竞争时升级。在栈帧中创建Lock Record,CAS将Mark Word替换为指向Lock Record的指针。适合交替执行(无实际竞争)。
  • 重量级锁:CAS自旋失败后升级。Mark Word指向Monitor对象(ObjectMonitor),未获取锁的线程进入EntryList阻塞(park)。
    升级过程不可逆(偏向→轻量→重量)。自适应自旋:JVM根据历史数据动态调整自旋次数。

18. 🔴 什么是JVM的Safepoint偏差问题?在生产环境中如何诊断和解决?

答:Safepoint偏差:某些线程长时间不到达安全点,导致其他已到达安全点的线程等待,延长STW时间。常见原因:1)可数循环(int循环变量,JIT不会在循环体内插入安全点检查);2)大数组复制(System.arraycopy是native方法,执行期间不检查安全点)。诊断:-XX:+PrintSafepointStatistics查看安全点统计,关注”spin”和”block”时间。-XX:+SafepointTimeout -XX:SafepointTimeoutDelay=2000设置超时告警。解决:1)将int循环变量改为long(long循环JIT会插入安全点);2)大数组分批复制;3)JDK10的Loop Strip Mining自动在循环中插入安全点。

19. 🔵 什么是Java NIO?和BIO有什么区别?Netty为什么比原生NIO更好用?

答:BIO(Blocking IO):一个连接一个线程,read/write阻塞。适合连接数少的场景。NIO(Non-blocking IO):Selector多路复用,一个线程管理多个Channel。核心组件:Channel(双向通道)、Buffer(缓冲区)、Selector(多路复用器)。底层:Linux的epoll,macOS的kqueue。Netty优于原生NIO:1)API简洁(原生NIO的Selector使用复杂,bug多如epoll空轮询);2)线程模型清晰(Reactor模式,Boss线程接收连接,Worker线程处理IO);3)内存管理(PooledByteBufAllocator,jemalloc算法的内存池);4)编解码框架(LengthFieldBasedFrameDecoder解决粘包拆包);5)丰富的协议支持(HTTP/2、WebSocket、SSL)。

20. 🔴 Netty的内存管理机制是怎样的?PooledByteBufAllocator是如何实现高效内存分配的?

答:Netty的内存管理借鉴jemalloc算法:

  • Arena:每个线程绑定一个Arena(减少锁竞争),Arena管理多个Chunk。
  • Chunk:默认16MB,用完全二叉树管理(类似伙伴系统)。
  • Page:默认8KB,Chunk由2048个Page组成。
  • SubPage:小于8KB的分配,将Page细分为等大的SubPage(如16B、32B…4KB)。
    分配策略:Tiny(<512B)→Small(512B-8KB)→Normal(8KB-16MB)→Huge(>16MB)。ThreadLocal缓存(PoolThreadCache):每个线程缓存最近释放的内存块,下次分配时优先从缓存取(无锁)。堆外内存(DirectByteBuf):减少一次内存拷贝(零拷贝),但分配和释放比堆内慢。引用计数:ReferenceCounted接口,retain()+1,release()-1,为0时回收。内存泄漏检测:ResourceLeakDetector,采样检测未释放的ByteBuf。

21. 🔵 什么是Java的CompletableFuture?它和Future有什么区别?如何实现异步编排?

答:Future的局限:只能阻塞等待(get())或轮询(isDone()),不能回调,不能组合。CompletableFuture(JDK8)增强:

  • 回调:thenApply(转换结果)、thenAccept(消费结果)、thenRun(不关心结果)。
  • 组合:thenCompose(串行,类似flatMap)、thenCombine(并行,合并两个结果)。
  • 多任务:allOf(等待所有完成)、anyOf(任一完成)。
  • 异常处理:exceptionally(异常时的默认值)、handle(无论成功失败都处理)。
  • 异步执行:默认用ForkJoinPool.commonPool(),可指定自定义线程池(推荐,避免commonPool被阻塞任务占满)。
    实际应用:并行调用多个微服务接口后合并结果。注意:不要在CompletableFuture中使用阻塞操作(会占用ForkJoinPool线程),JDK21可用虚拟线程替代。

22. 🔴 什么是Java的Stream API?它的惰性求值是如何实现的?parallel stream有什么陷阱?

答:Stream API的惰性求值:中间操作(filter、map、flatMap)不会立即执行,只是记录操作管道。终端操作(collect、forEach、reduce)触发时才遍历数据源执行所有操作。实现:每个中间操作返回一个新的Stream对象,内部维护一个操作链(Sink链),终端操作时从后向前构建Sink链,然后从前向后执行。短路操作(findFirst、limit)可以提前终止。parallel stream陷阱:1)使用ForkJoinPool.commonPool(),阻塞操作会影响其他parallel stream;2)数据源不适合并行(LinkedList拆分O(n),ArrayList拆分O(1));3)有状态操作(sorted、distinct)需要额外同步;4)线程安全问题(收集到非线程安全的集合);5)小数据量并行反而更慢(线程切换开销)。

23. 🔵 什么是Java的动态代理?JDK动态代理和CGLIB代理有什么区别?Spring AOP用的是哪种?

答:JDK动态代理:基于接口,运行时生成实现目标接口的代理类。核心:Proxy.newProxyInstance() + InvocationHandler。限制:目标类必须实现接口。CGLIB代理:基于继承,运行时生成目标类的子类。核心:Enhancer + MethodInterceptor。使用ASM字节码生成。限制:不能代理final类和final方法。性能对比:JDK代理在JDK8+性能已接近CGLIB,且不需要额外依赖。Spring AOP选择:目标类实现了接口用JDK代理,否则用CGLIB。Spring Boot 2.x默认使用CGLIB(spring.aop.proxy-target-class=true)。注意:CGLIB代理的self-invocation问题(内部方法调用不经过代理,AOP不生效)。

24. 🔴 什么是Java的模块化系统(JPMS)?它解决了什么问题?对现有项目迁移有什么影响?

答:JPMS(Java Platform Module System,JDK9):在包之上增加模块层,通过module-info.java声明模块的导出(exports)、依赖(requires)、服务提供(provides/uses)。解决的问题:1)JAR Hell(类路径冲突,同一个类在多个JAR中);2)封装性(包级别的访问控制不够,内部API如sun.misc.Unsafe被滥用);3)JRE瘦身(jlink只打包需要的模块)。迁移影响:1)反射访问非导出包需要–add-opens;2)内部API(如sun.misc.Unsafe)需要迁移到公开API(VarHandle);3)Split Package问题(同一个包在多个模块中)。实际中大多数项目仍使用classpath模式(unnamed module),模块化采用率不高。

25. 🔵 什么是GraalVM Native Image?它的优势和局限是什么?在云原生场景下有什么应用?

答:GraalVM Native Image:AOT(Ahead-of-Time)编译,将Java应用编译为独立的本地可执行文件。优势:1)启动时间从秒级降到毫秒级(无需JVM启动和类加载);2)内存占用大幅减少(无JIT编译器、无类元数据);3)即时达到峰值性能(无预热)。局限:1)不支持动态类加载、反射需要配置(reflect-config.json);2)编译时间长(分钟级);3)峰值性能可能不如JIT(缺少运行时profiling优化);4)调试困难。云原生应用:Quarkus、Micronaut、Spring Native(Spring Boot 3.x支持)。适合Serverless/FaaS场景(冷启动敏感)、CLI工具、Sidecar。不适合长期运行的高吞吐服务(JIT优化更好)。


二、Java 并发编程深度(26-55题)

26. 🔵 synchronized和ReentrantLock有什么区别?在什么场景下选择哪个?

答:区别:1)synchronized是JVM内置关键字,ReentrantLock是API级别;2)ReentrantLock支持公平锁、可中断锁(lockInterruptibly)、超时锁(tryLock)、多条件变量(Condition);3)synchronized自动释放锁(异常时也会),ReentrantLock需要手动unlock(必须在finally中);4)synchronized有锁升级优化(偏向→轻量→重量)。选择:简单同步用synchronized(代码简洁、不易出错);需要高级功能(公平锁、超时、多条件)用ReentrantLock。JDK6之后synchronized性能已接近ReentrantLock。注意:JDK21虚拟线程场景下,synchronized会pin住载体线程,应改用ReentrantLock。

27. 🔴 请描述Java线程池的核心参数和工作流程。如何合理配置线程池参数?

答:核心参数:corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(非核心线程空闲存活时间)、workQueue(任务队列)、threadFactory(线程工厂)、handler(拒绝策略)。工作流程:1)线程数<core,创建新线程;2)线程数≥core,任务入队列;3)队列满且线程数<max,创建非核心线程;4)队列满且线程数≥max,执行拒绝策略。参数配置:

  • CPU密集型:core = CPU核心数 + 1(+1是为了CPU空闲时有备用线程)。
  • IO密集型:core = CPU核心数 × 2,或用公式 core = CPU核心数 × (1 + IO等待时间/CPU计算时间)。
  • 队列选择:有界队列(ArrayBlockingQueue)防止OOM,无界队列(LinkedBlockingQueue)可能导致任务堆积。
  • 动态调整:美团的动态线程池方案,通过配置中心实时调整参数。

28. 🔵 什么是Java的Happens-Before规则?请列举所有规则并解释其含义。

答:Happens-Before定义了操作间的可见性和有序性保证:

  1. 程序顺序规则:同一线程中,前面的操作happens-before后面的操作。
  2. Monitor锁规则:unlock happens-before后续的lock。
  3. volatile规则:volatile写happens-before后续的volatile读。
  4. 线程启动规则:Thread.start() happens-before线程中的任何操作。
  5. 线程终止规则:线程中的任何操作happens-before Thread.join()返回。
  6. 线程中断规则:interrupt()调用happens-before被中断线程检测到中断。
  7. 对象终结规则:构造函数完成happens-before finalize()开始。
  8. 传递性:A happens-before B,B happens-before C,则A happens-before C。
    注意:happens-before不等于时间上的先后,而是可见性保证。两个没有happens-before关系的操作可能被重排序。

29. 🔴 什么是Java的StampedLock?它和ReentrantReadWriteLock有什么区别?乐观读是如何实现的?

答:StampedLock(JDK8)三种模式:写锁、悲观读锁、乐观读。与ReentrantReadWriteLock区别:1)支持乐观读(无锁,性能最高);2)不可重入(避免死锁但使用需谨慎);3)不支持Condition。乐观读实现:tryOptimisticRead()返回一个stamp(版本号),读取数据后调用validate(stamp)检查期间是否有写操作。如果validate返回false,降级为悲观读锁重新读取。底层:stamp是一个long值,写锁时stamp的第8位(WBIT)置1,乐观读时检查WBIT是否变化。使用模式:

1
2
3
4
5
6
long stamp = lock.tryOptimisticRead();
// 读取共享变量
if (!lock.validate(stamp)) {
stamp = lock.readLock(); // 降级为悲观读
try { /* 重新读取 */ } finally { lock.unlockRead(stamp); }
}

适合读多写极少的场景(如配置读取)。

30. 🔵 什么是Java的Phaser?它和CyclicBarrier、CountDownLatch有什么区别?

答:Phaser(JDK7)是更灵活的同步屏障:

  • CountDownLatch:一次性,计数到0后不能重用。适合”等待N个任务完成”。
  • CyclicBarrier:可重用,所有线程到达屏障后同时继续。适合”N个线程互相等待”。参与者数量固定。
  • Phaser:可重用,支持动态注册/注销参与者,支持多阶段(phase)同步。每个阶段所有参与者到达后进入下一阶段。支持层级结构(Tiered Phaser)减少同步开销。
    Phaser的高级用法:1)onAdvance()回调,在阶段切换时执行自定义逻辑;2)arriveAndDeregister()动态减少参与者;3)register()动态增加参与者。适合分阶段并行计算(如遗传算法的每代进化)。

31. 🔴 什么是Java的ForkJoinPool?它的工作窃取算法实现细节是什么?和普通线程池有什么区别?

答:ForkJoinPool专为分治任务设计。与ThreadPoolExecutor区别:1)每个工作线程有自己的双端队列(WorkQueue),而非共享队列;2)工作窃取:空闲线程从其他线程队列尾部偷任务;3)任务类型:RecursiveTask(有返回值)、RecursiveAction(无返回值)。实现细节:WorkQueue数组,偶数槽位存放外部提交的任务(共享队列),奇数槽位存放工作线程的本地队列。本地任务LIFO(栈顶取,利用缓存局部性),窃取FIFO(队尾取,偷大任务)。signalWork()唤醒或创建工作线程。scan()随机选择起始位置扫描其他队列。关键参数:parallelism(并行度,默认CPU核心数)、asyncMode(true时FIFO,适合事件处理)。

32. 🔵 什么是Java的原子类?LongAdder为什么比AtomicLong快?Striped64的实现原理是什么?

答:AtomicLong:单个volatile long + CAS。高竞争时大量CAS失败重试,性能下降。LongAdder:分段计数,最终求和。Striped64实现:

  • base:无竞争时直接CAS更新base。
  • Cell数组:有竞争时,每个线程映射到一个Cell(通过线程probe值hash),CAS更新对应Cell。
  • sum():base + 所有Cell的值。
    Cell用@Contended避免伪共享。扩容:Cell数组初始2,竞争激烈时翻倍(最大为CPU核心数)。线程映射:Thread.threadLocalRandomProbe,CAS失败时rehash换一个Cell。LongAccumulator是LongAdder的泛化版本,支持自定义累加函数。适用场景:高并发计数(如QPS统计),不适合需要精确实时值的场景(sum()不是原子操作)。

33. 🔴 什么是Java的Exchanger?它的实现原理是什么?有什么实际应用场景?

答:Exchanger用于两个线程交换数据。线程A调用exchange(dataA)阻塞,线程B调用exchange(dataB)阻塞,两者匹配后交换数据并继续。实现原理:基于Slot(槽位)。单槽位:第一个线程将数据放入槽位并等待,第二个线程取出数据放入自己的数据,唤醒第一个线程。多槽位(arena):高并发时扩展为多个槽位,减少竞争。应用场景:1)双缓冲技术:一个线程填充缓冲区,另一个线程消费缓冲区,填满/消费完后交换;2)遗传算法中的基因交叉;3)管道模式中的数据传递。实际使用较少,大多数场景用BlockingQueue更直观。

34. 🔵 什么是Java的Condition接口?它和Object的wait/notify有什么区别?如何实现生产者-消费者模式?

答:Condition是Lock的伴生对象,提供await()/signal()/signalAll()。与wait/notify区别:1)一个Lock可以创建多个Condition(如ArrayBlockingQueue的notEmpty和notFull),而synchronized只有一个等待队列;2)Condition支持超时等待、不可中断等待;3)Condition必须在Lock.lock()之后使用。生产者-消费者:

1
2
3
4
5
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 生产者:lock → while(full) notFull.await() → put → notEmpty.signal() → unlock
// 消费者:lock → while(empty) notEmpty.await() → take → notFull.signal() → unlock

ArrayBlockingQueue就是这样实现的。注意:await()必须在while循环中(防止虚假唤醒)。

35. 🔴 什么是Java的StructuredConcurrency(JDK21预览)?它解决了什么问题?

答:结构化并发将并发任务组织为树形结构,子任务的生命周期绑定到父任务。解决的问题:1)任务泄漏(子任务在父任务结束后仍在运行);2)错误传播(一个子任务失败,其他子任务不知道);3)取消传播(取消父任务时子任务不会自动取消)。核心API:StructuredTaskScope,两种策略:ShutdownOnFailure(任一子任务失败则取消其他)、ShutdownOnSuccess(任一子任务成功则取消其他)。配合虚拟线程使用效果最佳。类比:try-with-resources管理资源生命周期,StructuredConcurrency管理任务生命周期。Go的errgroup是类似概念。

36. 🔵 什么是Java的ScopedValue(JDK21预览)?它和ThreadLocal有什么区别?

答:ScopedValue是ThreadLocal的替代方案,专为虚拟线程设计。区别:1)不可变(一旦绑定不能修改,避免了ThreadLocal的可变状态问题);2)有界生命周期(通过ScopedValue.where().run()限定作用域,作用域结束自动清理,无内存泄漏风险);3)自动继承(子虚拟线程自动继承父线程的ScopedValue,无需TransmittableThreadLocal);4)性能更好(不需要HashMap查找,基于栈帧)。使用模式:ScopedValue.where(KEY, value).run(() -> { ... })。适合请求上下文传递(如用户ID、TraceID)。

37. 🔴 请描述Java中常见的内存泄漏场景和排查方法。

答:常见泄漏场景:1)静态集合持有对象引用(如static Map不断put不remove);2)未关闭的资源(Connection、InputStream、Cursor);3)ThreadLocal未清理(线程池场景);4)监听器/回调未注销;5)内部类持有外部类引用(非静态内部类、匿名类);6)缓存无淘汰策略(应用Caffeine/Guava Cache设置maxSize和expireAfter);7)ClassLoader泄漏(热部署场景,旧ClassLoader无法被回收)。排查方法:1)jmap -histo查看对象数量和大小;2)jmap -dump导出堆快照;3)MAT(Memory Analyzer Tool)分析:Dominator Tree找大对象、Leak Suspects自动分析、Path to GC Roots找引用链;4)Arthas的heapdump和dashboard命令;5)JFR(Java Flight Recorder)持续监控。

38. 🔵 什么是Arthas?它能解决哪些生产环境问题?请列举常用命令。

答:Arthas是阿里开源的Java诊断工具,无需重启应用即可诊断。常用命令:

  • dashboard:实时查看线程、内存、GC信息。
  • thread:查看线程状态,thread -n 3查看CPU最高的3个线程,thread -b查看阻塞线程。
  • jad:反编译类,确认线上代码版本。
  • watch:观察方法入参、返回值、异常。watch com.example.Service method "{params,returnObj}" -x 3
  • trace:方法调用链路耗时分析,定位慢方法。
  • stack:查看方法调用栈(谁调用了这个方法)。
  • sc/sm:搜索类/方法信息。
  • redefine/retransform:热替换class文件(紧急修复)。
  • profiler:火焰图,分析CPU热点。
  • heapdump:导出堆快照。
  • ognl:执行OGNL表达式,如查看静态变量值。

39. 🔴 什么是Java Flight Recorder(JFR)?它和JMX监控有什么区别?如何用JFR分析性能问题?

答:JFR是JVM内置的低开销事件记录框架(<2%性能影响)。与JMX区别:JMX是实时查询(pull模式),JFR是持续记录(push模式,事件写入环形缓冲区)。JFR记录的事件:GC、线程、锁竞争、IO、方法profiling、异常、类加载等。使用:1)启动时:-XX:StartFlightRecording=duration=60s,filename=recording.jfr;2)运行时:jcmd <pid> JFR.start。分析工具:JDK Mission Control(JMC),可视化分析JFR文件。常见分析场景:1)GC分析:GC暂停时间、频率、原因;2)热点方法:CPU采样找到耗时最多的方法;3)锁竞争:哪些锁竞争最激烈、等待时间最长;4)IO分析:文件IO和网络IO的延迟分布;5)内存分配:哪些方法分配了最多的对象(TLAB外分配)。JDK11+免费使用。

40. 🔵 什么是Java的序列化?为什么不推荐使用Java原生序列化?有哪些替代方案?

答:Java原生序列化(Serializable)问题:1)安全漏洞(反序列化攻击,如Apache Commons Collections的gadget chain);2)性能差(反射、类描述信息冗余);3)序列化后体积大;4)跨语言不兼容。替代方案:

  • JSON:Jackson/Gson,可读性好,跨语言。缺点:体积较大,不支持复杂类型。
  • Protobuf:Google,二进制格式,体积小性能好,强类型(.proto文件定义)。gRPC默认使用。
  • Kryo:Java专用,性能极好(比Java序列化快10倍+),Spark默认使用。缺点:不跨语言。
  • Hessian:Dubbo默认序列化,跨语言,性能适中。
  • Avro:Hadoop生态,支持Schema演进,适合大数据场景。
  • MessagePack:类似JSON的二进制格式,体积小。
    选型:内部Java服务用Kryo/Protobuf,跨语言用Protobuf/JSON,大数据用Avro。

41. 🔴 什么是Java的Record类型(JDK16)和Sealed类(JDK17)?它们在领域建模中有什么应用?

答:Record:不可变数据载体,自动生成构造器、getter、equals、hashCode、toString。record Point(int x, int y) {}。适合DTO、值对象、事件。限制:不能继承其他类(隐式继承Record)、字段不可变。Sealed类:限制哪些类可以继承/实现。sealed interface Shape permits Circle, Rectangle {}。子类必须是final、sealed或non-sealed。应用:1)代数数据类型(ADT):sealed interface Result<T> permits Success, Failure {};2)配合模式匹配(JDK21):switch(shape) { case Circle c -> ...; case Rectangle r -> ...; },编译器检查穷举性。领域建模:Record做值对象(Value Object),Sealed做领域事件/命令的类型安全建模。

42. 🔵 什么是Java的模式匹配(Pattern Matching)?JDK21的switch模式匹配有哪些增强?

答:模式匹配演进:

  • JDK16:instanceof模式匹配,if (obj instanceof String s) { s.length(); },无需显式强转。
  • JDK21:switch模式匹配(正式版),支持类型模式、守卫条件、null处理。
1
2
3
4
5
6
switch (obj) {
case Integer i when i > 0 -> "positive: " + i;
case String s -> "string: " + s;
case null -> "null";
default -> "other";
}

配合Sealed类实现穷举检查(无需default)。Record模式:case Point(int x, int y) when x > 0 -> ...,解构Record的组件。实际应用:替代访问者模式(Visitor Pattern),代码更简洁。

43. 🔴 什么是Project Valhalla?Value Types和Primitive Classes会对Java产生什么影响?

答:Project Valhalla目标:消除Java中基本类型和对象类型的鸿沟。核心概念:

  • Value Types(值类型):类似struct,没有对象头(无identity),可以内联存储(不需要指针间接访问)。减少内存占用和GC压力。
  • Primitive Classes:值类型的具体实现,primitive class Complex { double re; double im; }
  • 泛型特化List<int>直接存储int值,而非Integer对象(消除装箱)。
    影响:1)数组扁平化(Complex[]连续存储,而非指针数组);2)消除大量装箱/拆箱;3)缓存友好(数据局部性好);4)减少GC压力(值类型在栈上或内联在对象中)。预计对数值计算、金融系统、游戏引擎等性能敏感场景影响巨大。目前仍在开发中。

44. 🔵 什么是Java的函数式接口?@FunctionalInterface注解的作用是什么?常用的函数式接口有哪些?

答:函数式接口:只有一个抽象方法的接口(可以有default方法和static方法)。@FunctionalInterface:编译时检查,确保接口只有一个抽象方法。常用函数式接口:

  • Function<T,R>:T→R,转换。compose/andThen组合。
  • Predicate:T→boolean,判断。and/or/negate组合。
  • Consumer:T→void,消费。andThen链式。
  • Supplier:()→T,生产。延迟计算。
  • BiFunction<T,U,R>:(T,U)→R,双参数转换。
  • UnaryOperator:T→T,一元操作(Function的特例)。
  • BinaryOperator:(T,T)→T,二元操作(BiFunction的特例)。
    实际应用:Stream API、CompletableFuture、Optional的方法参数都是函数式接口。方法引用(::)是Lambda的简写。

45. 🔴 什么是Java的响应式编程?Reactor和RxJava的核心概念是什么?背压如何实现?

答:响应式编程基于异步数据流,核心概念:Publisher(发布者)→Subscriber(订阅者),通过Subscription控制流量。Reactor核心类型:Mono(0或1个元素)、Flux(0到N个元素)。操作符:map、flatMap、filter、zip、merge、concat等。背压实现:Reactive Streams规范的Subscription.request(n),订阅者告诉发布者自己能处理n个元素。策略:buffer(缓冲)、drop(丢弃最新)、latest(只保留最新一个)、error(报错)。与RxJava区别:Reactor是Spring WebFlux的基础,与Spring生态集成更好;RxJava更成熟,Android生态常用。注意:响应式编程学习曲线陡峭,调试困难(堆栈信息不直观),不适合所有场景。JDK21的虚拟线程在很多场景下是更简单的替代方案。

46. 🔵 什么是Spring Boot的自动配置原理?@EnableAutoConfiguration是如何工作的?

答:自动配置流程:1)@SpringBootApplication包含@EnableAutoConfiguration;2)@EnableAutoConfiguration通过@Import(AutoConfigurationImportSelector.class)导入配置;3)AutoConfigurationImportSelector读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 3.x)或META-INF/spring.factories(2.x)中的自动配置类列表;4)每个自动配置类通过@Conditional注解条件化生效(如@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty);5)用户自定义的Bean优先级高于自动配置的Bean(@ConditionalOnMissingBean保证)。调试:spring.boot.autoconfigure.exclude排除特定配置,–debug启动查看自动配置报告(ConditionEvaluationReport)。

47. 🔴 什么是Spring的事务传播机制?REQUIRED和REQUIRES_NEW有什么区别?嵌套事务如何实现?

答:7种传播机制:REQUIRED(默认,有事务加入,没有新建)、REQUIRES_NEW(总是新建,挂起当前事务)、NESTED(嵌套事务,用Savepoint实现)、SUPPORTS(有事务加入,没有非事务执行)、NOT_SUPPORTED(非事务执行,挂起当前事务)、MANDATORY(必须在事务中,否则异常)、NEVER(不能在事务中,否则异常)。REQUIRED vs REQUIRES_NEW:REQUIRED共享同一个物理事务,内部方法异常会导致整个事务回滚;REQUIRES_NEW创建独立事务,内部事务回滚不影响外部事务。NESTED:基于JDBC Savepoint,内部事务回滚到Savepoint,外部事务可以选择继续或回滚。注意:同一个类中方法调用不经过代理,事务注解不生效(self-invocation问题),需要通过AopContext.currentProxy()或注入自身解决。

48. 🔵 什么是Spring的循环依赖?三级缓存是如何解决循环依赖的?

答:循环依赖:A依赖B,B依赖A。Spring通过三级缓存解决setter注入的循环依赖:

  • 一级缓存(singletonObjects):完全初始化的Bean。
  • 二级缓存(earlySingletonObjects):提前暴露的Bean(已实例化但未初始化)。
  • 三级缓存(singletonFactories):Bean工厂(ObjectFactory),用于生成早期引用。
    流程:1)创建A,实例化后将ObjectFactory放入三级缓存;2)A填充属性发现依赖B,创建B;3)B填充属性发现依赖A,从三级缓存获取A的ObjectFactory,生成早期引用放入二级缓存;4)B初始化完成放入一级缓存;5)A继续初始化完成放入一级缓存。为什么需要三级缓存而不是二级?因为AOP代理需要在ObjectFactory中决定是否创建代理对象。构造器注入的循环依赖无法解决(实例化阶段就需要依赖)。Spring Boot 2.6+默认禁止循环依赖。

49. 🔴 什么是Spring WebFlux?它和Spring MVC有什么区别?在什么场景下应该使用WebFlux?

答:WebFlux是Spring的响应式Web框架,基于Reactor。与MVC区别:1)MVC基于Servlet(阻塞IO,一个请求一个线程),WebFlux基于Netty/Undertow(非阻塞IO,少量线程处理大量请求);2)MVC用命令式编程,WebFlux用响应式编程(Mono/Flux);3)MVC支持所有Servlet容器,WebFlux支持Netty/Undertow/Servlet 3.1+。适用场景:1)高并发IO密集型(如API网关、代理服务);2)流式数据处理(SSE、WebSocket);3)微服务间的非阻塞调用。不适合:1)CPU密集型;2)阻塞IO(如JDBC,需要用R2DBC替代);3)团队不熟悉响应式编程。JDK21虚拟线程+Spring MVC在很多场景下是更简单的替代方案。

50. 🔵 什么是Spring Cloud的核心组件?请描述一个完整的微服务架构中各组件的作用。

答:核心组件:

  • 服务注册发现:Nacos/Eureka/Consul。服务启动时注册,消费者从注册中心获取服务列表。
  • 负载均衡:Spring Cloud LoadBalancer(替代Ribbon)。客户端负载均衡,支持轮询、随机等策略。
  • 服务调用:OpenFeign(声明式HTTP客户端)、gRPC。
  • 熔断降级:Sentinel/Resilience4j(替代Hystrix)。
  • API网关:Spring Cloud Gateway(替代Zuul)。路由、限流、认证、日志。
  • 配置中心:Nacos/Apollo/Spring Cloud Config。
  • 分布式事务:Seata(AT/TCC/Saga模式)。
  • 消息驱动:Spring Cloud Stream(抽象MQ,支持Kafka/RocketMQ/RabbitMQ)。
  • 链路追踪:Micrometer Tracing(替代Sleuth)+ Zipkin/Jaeger。
  • 分布式任务调度:XXL-JOB/ElasticJob。

51. 🔴 什么是Spring的IOC容器?BeanFactory和ApplicationContext有什么区别?Bean的生命周期是怎样的?

答:IOC(控制反转):对象的创建和依赖关系由容器管理,而非代码中硬编码。BeanFactory:最基础的容器,懒加载。ApplicationContext:BeanFactory的扩展,增加了国际化、事件发布、AOP、资源加载等功能,默认预加载所有单例Bean。Bean生命周期:1)实例化(构造器/工厂方法);2)属性填充(依赖注入);3)Aware接口回调(BeanNameAware、ApplicationContextAware等);4)BeanPostProcessor.postProcessBeforeInitialization;5)InitializingBean.afterPropertiesSet / @PostConstruct / init-method;6)BeanPostProcessor.postProcessAfterInitialization(AOP代理在此创建);7)使用;8)DisposableBean.destroy / @PreDestroy / destroy-method。BeanPostProcessor是Spring扩展的核心机制,AOP、事务、异步等都通过它实现。

52. 🔵 什么是Java的Optional?如何正确使用它避免NullPointerException?有哪些反模式?

答:Optional(JDK8):显式表达值可能不存在。正确用法:1)方法返回值(替代返回null);2)链式操作:optional.map(User::getName).orElse("unknown");3)条件执行:optional.ifPresent(user -> ...)。反模式:1)Optional作为方法参数(增加调用方复杂度);2)Optional作为字段(不可序列化);3)optional.get()不检查(和null一样危险);4)optional.isPresent() + optional.get()(应该用orElse/map替代);5)Optional.of(nullableValue)(应该用ofNullable)。最佳实践:orElse(默认值)、orElseGet(延迟计算默认值)、orElseThrow(抛异常)、map/flatMap(转换)。JDK9增加:ifPresentOrElse、or、stream。

53. 🔴 什么是Java的泛型擦除?它会导致什么问题?如何通过TypeToken等技术绕过擦除?

答:泛型擦除:编译后泛型信息被擦除,List<String>List<Integer>在运行时都是List。导致的问题:1)不能new T()new T[];2)不能instanceof List<String>;3)不能重载void method(List<String>)void method(List<Integer>);4)桥方法(Bridge Method):泛型继承时编译器生成的类型转换方法。绕过方式:1)TypeToken/TypeReference:通过匿名子类保留泛型信息(new TypeReference<List<String>>(){},泛型信息存储在类的Signature属性中);2)Class参数传递:<T> T create(Class<T> clazz);3)反射获取:Method.getGenericReturnType()、Field.getGenericType()可以获取声明时的泛型信息。Jackson/Gson的反序列化都依赖TypeToken机制。

54. 🔵 什么是Java的注解处理器(Annotation Processor)?Lombok是如何实现的?

答:注解处理器在编译期处理注解,生成代码或修改AST。标准API:javax.annotation.processing.AbstractProcessor,通过process()方法处理注解,可以生成新的Java源文件(Filer.createSourceFile)。Lombok的实现不同于标准注解处理器:它直接修改AST(抽象语法树),在编译期将@Data等注解转化为getter/setter/toString等方法。这依赖于javac的内部API(com.sun.tools.javac),不是标准行为,所以需要特殊的编译器插件支持。其他应用:MapStruct(对象映射代码生成)、Dagger(依赖注入代码生成)、AutoValue(不可变类生成)。优势:编译期生成代码,无运行时开销(对比反射)。

55. 🔴 什么是Java的Foreign Function & Memory API(JDK22)?它如何替代JNI?

答:FFM API(Project Panama)提供安全高效的方式调用本地代码和管理堆外内存。替代JNI的优势:1)纯Java API,无需编写C/C++胶水代码;2)类型安全,编译期检查;3)性能接近JNI(某些场景更好,避免了JNI的开销如句柄管理);4)内存安全(Arena管理生命周期,自动释放)。核心组件:Linker(调用本地函数)、SymbolLookup(查找本地符号)、FunctionDescriptor(描述函数签名)、MemorySegment(堆外内存段)、Arena(内存生命周期管理)。应用场景:调用OpenSSL、SQLite等C库,高性能IO(直接操作堆外内存),替代Unsafe的堆外内存操作。jextract工具可以从C头文件自动生成Java绑定代码。


三、Java 性能调优与生产实践(56-80题)

56. 🔵 生产环境中CPU使用率100%,你的排查思路是什么?

答:排查步骤:1)top -Hp <pid>找到CPU最高的线程ID;2)printf '%x' <tid>转为16进制;3)jstack <pid> | grep <hex_tid> -A 30查看线程堆栈;4)分析堆栈:死循环、正则回溯(ReDoS)、频繁GC(GC线程占CPU)、锁自旋。或者用Arthas:thread -n 3直接查看CPU最高的3个线程堆栈。常见原因:1)死循环或无限递归;2)正则表达式灾难性回溯;3)频繁Full GC(用jstat -gcutil确认);4)大量线程自旋等待锁;5)序列化/反序列化大对象;6)加密/压缩操作。JFR的CPU采样可以生成火焰图,直观定位热点方法。

57. 🔴 生产环境中出现OOM,你的排查思路是什么?不同类型的OOM分别是什么原因?

答:OOM类型和原因:

  • Java heap space:堆内存不足。原因:内存泄漏、大对象、堆设置太小。
  • Metaspace:元空间不足。原因:动态生成类过多(反射、CGLIB、Groovy脚本)。
  • GC overhead limit exceeded:GC时间占比过高(>98%时间在GC,回收<2%内存)。
  • Direct buffer memory:堆外内存不足。原因:NIO DirectByteBuffer分配过多。
  • unable to create new native thread:线程数超限。原因:线程泄漏或ulimit限制。
  • Requested array size exceeds VM limit:数组大小超过Integer.MAX_VALUE-8。
    排查:1)-XX:+HeapDumpOnOutOfMemoryError自动dump;2)MAT分析堆dump;3)Arthas的heapdump和dashboard;4)jstat -gcutil监控GC。

58. 🔵 什么是JVM调优?请描述一个完整的JVM调优过程。

答:调优目标:降低GC停顿时间、提高吞吐量、减少Full GC频率。过程:

  1. 基线测量:开启GC日志(-Xlog:gc*),用GCViewer/GCEasy分析当前GC情况。
  2. 确定目标:停顿时间<200ms?吞吐量>99%?
  3. 选择GC算法:JDK8默认Parallel GC(吞吐量优先),JDK17+推荐G1或ZGC。
  4. 堆大小调整:-Xms=-Xmx(避免动态扩缩容),一般为物理内存的50-70%。新生代:-Xmn或-XX:NewRatio。
  5. GC参数调优:G1的MaxGCPauseMillis、InitiatingHeapOccupancyPercent;ZGC基本不需要调优。
  6. 验证效果:压测对比调优前后的GC日志、P99延迟、吞吐量。
  7. 持续监控:Prometheus+Grafana监控GC指标,告警异常。
    经验:大多数情况下选对GC算法+合理堆大小就够了,过度调优收益递减。

59. 🔴 什么是Java的零拷贝(Zero Copy)?在Netty和Kafka中是如何实现的?

答:零拷贝减少数据在内核空间和用户空间之间的拷贝次数。传统IO:磁盘→内核缓冲区→用户缓冲区→Socket缓冲区→网卡,4次拷贝+4次上下文切换。

  • mmap:磁盘→内核缓冲区(用户空间映射同一块内存)→Socket缓冲区→网卡。减少一次拷贝。Java的MappedByteBuffer(FileChannel.map)。RocketMQ用mmap读写CommitLog。
  • sendfile:磁盘→内核缓冲区→网卡(DMA gather copy,只传递描述符到Socket缓冲区)。减少到2次拷贝+2次上下文切换。Java的FileChannel.transferTo()。Kafka用sendfile发送消息给消费者。
  • Netty的零拷贝:应用层面的优化。1)CompositeByteBuf合并多个Buffer的逻辑视图,避免内存拷贝;2)slice()创建Buffer的视图;3)FileRegion封装transferTo实现文件传输零拷贝;4)堆外内存(DirectByteBuf)避免JVM堆到native内存的拷贝。

60. 🔵 什么是Java的类加载器泄漏?在Tomcat热部署场景下如何发生?如何排查和解决?

答:类加载器泄漏:Web应用重新部署后,旧的WebAppClassLoader无法被GC回收,导致Metaspace持续增长最终OOM。原因:旧ClassLoader被某个GC Root引用链持有。常见泄漏源:

  1. ThreadLocal:线程池中的线程持有旧ClassLoader加载的类的ThreadLocal值。
  2. JDBC驱动:DriverManager持有旧ClassLoader注册的Driver引用。
  3. JMX MBean:注册的MBean引用了旧ClassLoader的类。
  4. 日志框架:Log4j/Logback的静态引用。
  5. shutdown hook:Runtime.addShutdownHook注册的线程引用旧类。
    排查:MAT分析堆dump,查找WebAppClassLoader的GC Root引用链(Path to GC Roots → exclude weak/soft references)。解决:1)应用停止时清理ThreadLocal、注销Driver、注销MBean;2)Tomcat的内存泄漏检测(MemoryLeakTrackingListener);3)使用Spring Boot内嵌容器避免热部署。

61. 🔴 请描述一次你在生产环境中排查Java性能问题的完整过程。从发现问题到最终解决。

答:(参考答案,考察候选人的实战经验)典型案例:某服务P99延迟从50ms飙升到2s。

  1. 发现:监控告警P99延迟超阈值,Grafana确认是某个服务的问题。
  2. 初步排查:dashboard查看CPU、内存、GC。发现Young GC频率从每秒1次变为每秒10次,每次耗时从10ms变为50ms。
  3. 深入分析:jstat -gcutil发现Eden区秒满。jmap -histo发现某个DTO对象数量异常多(百万级)。
  4. 定位代码:Arthas的profiler start生成火焰图,发现某个接口在循环中创建大量临时对象。代码review发现:一个批量查询接口没有分页,一次查询返回了10万条记录,每条记录转换为DTO时创建了多个中间对象。
  5. 解决:1)接口增加分页限制(最大1000条);2)DTO转换使用MapStruct替代手动new(减少中间对象);3)大批量场景改为流式处理。
  6. 验证:压测确认P99恢复到50ms,Young GC频率恢复正常。
  7. 复盘:增加接口返回数据量的监控告警,代码review增加批量接口的分页检查。

62. 🔵 什么是Java的对象内存布局?对象头包含哪些信息?如何计算一个对象占用的内存大小?

答:HotSpot对象内存布局:对象头(Header)+ 实例数据(Instance Data)+ 对齐填充(Padding)。

  • 对象头:Mark Word(8字节,存储hashCode、GC年龄、锁状态、线程ID等)+ Klass Pointer(类型指针,开启压缩指针时4字节,否则8字节)。数组对象额外4字节存储长度。
  • 实例数据:字段按类型大小排列(long/double 8字节、int/float 4字节、short/char 2字节、byte/boolean 1字节、引用4/8字节)。JVM会重排字段顺序以减少填充(-XX:FieldsAllocationStyle)。
  • 对齐填充:对象大小必须是8字节的倍数。
    计算工具:JOL(Java Object Layout),ClassLayout.parseInstance(obj).toPrintable()。示例:一个只有一个int字段的对象 = 12字节头 + 4字节int = 16字节。空对象 = 12字节头 + 4字节填充 = 16字节。压缩指针(-XX:+UseCompressedOops,堆<32GB时默认开启)将引用从8字节压缩为4字节。

63. 🔴 什么是TLAB(Thread Local Allocation Buffer)?它如何提高对象分配效率?

答:TLAB是Eden区中每个线程私有的一小块内存区域。对象分配时优先在TLAB中分配(指针碰撞,bump the pointer),无需加锁(因为是线程私有的)。TLAB满了之后申请新的TLAB或在Eden区共享区域分配(需要CAS)。大对象(超过TLAB剩余空间)直接在Eden共享区域或老年代分配。

  • 大小:默认为Eden的1%,JVM根据线程分配速率动态调整(-XX:+ResizeTLAB,默认开启)。
  • 浪费处理:TLAB剩余空间不足以分配新对象时,如果剩余空间<TLAB的refill_waste比例(默认64字节),则废弃当前TLAB申请新的;否则在共享区域分配。
  • 监控:-XX:+PrintTLAB查看TLAB分配统计。
    TLAB是JVM对象分配快速路径的关键优化,使得大部分对象分配只需要一次指针加法操作。

64. 🔵 什么是Java的字符串常量池?String.intern()的作用是什么?JDK7之后有什么变化?

答:字符串常量池存储字符串字面量和intern()的字符串引用。JDK7之前在永久代(PermGen),JDK7+移到堆中(避免PermGen OOM)。String.intern():如果常量池中已有equals的字符串,返回池中引用;否则将当前字符串的引用加入池中(JDK7+不复制字符串,直接存引用)。应用场景:大量重复字符串的去重(如XML解析、日志处理),减少内存占用。注意:1)intern()有性能开销(需要查找哈希表),不适合高频调用;2)常量池大小可通过-XX:StringTableSize调整(默认60013,质数,影响哈希分布);3)JDK9的Compact Strings:Latin1字符用byte[](1字节/字符)替代char[](2字节/字符),减少约一半内存。G1的String Deduplication(-XX:+UseStringDeduplication)可以自动去重。

65. 🔴 什么是Java的直接内存(Direct Memory)?如何监控和管理?堆外内存泄漏如何排查?

答:直接内存通过ByteBuffer.allocateDirect()或Unsafe.allocateMemory()分配,不受GC直接管理。优势:1)减少一次内存拷贝(IO操作时不需要从堆拷贝到native内存);2)不受GC停顿影响。管理:DirectByteBuffer通过Cleaner(虚引用)机制在GC时释放堆外内存。-XX:MaxDirectMemorySize限制最大直接内存(默认等于-Xmx)。监控:1)JMX的BufferPoolMXBean;2)NMT(Native Memory Tracking):-XX:NativeMemoryTracking=summary,jcmd VM.native_memory summary。泄漏排查:1)NMT对比两个时间点的内存快照(jcmd VM.native_memory baseline → jcmd VM.native_memory summary.diff);2)Netty的ResourceLeakDetector检测ByteBuf泄漏;3)gperftools/jemalloc的内存profiling。常见原因:ByteBuf未release、MappedByteBuffer未unmap。

66. 🔵 什么是Java的SPI在JDBC中的应用?DriverManager是如何自动发现数据库驱动的?

答:JDBC 4.0+利用SPI自动发现驱动。流程:1)数据库驱动JAR包中包含META-INF/services/java.sql.Driver文件,内容为驱动类全限定名(如com.mysql.cj.jdbc.Driver);2)DriverManager静态初始化时,通过ServiceLoader.load(Driver.class)加载所有驱动;3)DriverManager.getConnection()时遍历已注册的驱动,调用每个驱动的connect()方法,能处理该URL的驱动返回Connection。这就是为什么JDK6+不需要显式Class.forName(“com.mysql.jdbc.Driver”)。注意:DriverManager在Bootstrap ClassLoader中,但驱动类在Application ClassLoader中,所以SPI通过Thread.getContextClassLoader()打破双亲委派来加载驱动类。

67. 🔴 什么是Java的内存模型中的指令重排序?它会导致什么问题?DCL(双重检查锁)为什么需要volatile?

答:指令重排序:编译器和CPU为了优化性能,在不改变单线程语义的前提下调整指令执行顺序。三种重排序:1)编译器重排序(javac/JIT);2)指令级并行重排序(CPU流水线);3)内存系统重排序(Store Buffer/Invalidate Queue)。DCL问题:

1
2
3
4
5
6
7
if (instance == null) {           // 第一次检查
synchronized (lock) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题在这里
}
}
}

new Singleton()分三步:1)分配内存;2)初始化对象;3)将引用赋值给instance。步骤2和3可能被重排序,导致其他线程看到未初始化的对象(instance不为null但字段还是默认值)。volatile禁止这种重排序(通过StoreStore屏障保证初始化在赋值之前完成)。JDK5+的volatile语义才能正确保证DCL。

68. 🔵 什么是Java的WeakHashMap?它的实现原理是什么?有什么实际应用场景?

答:WeakHashMap的key是弱引用(WeakReference),当key没有其他强引用时,GC会回收key,对应的Entry会被清理。实现:Entry继承WeakReference,key被回收后Entry进入ReferenceQueue。每次get/put/size操作时调用expungeStaleEntries(),遍历ReferenceQueue清理过期Entry。应用场景:1)缓存:以对象为key的缓存,对象不再使用时缓存自动清理(如ClassLoader→Class的映射);2)元数据关联:为对象附加额外信息,对象被回收时信息自动清理;3)ThreadLocalMap的key就是弱引用(类似思路)。注意:value不是弱引用,如果value强引用了key,key永远不会被回收(间接强引用)。解决:value也用WeakReference包装,或在expunge时主动清理。

69. 🔴 什么是Java的Unsafe类?它提供了哪些能力?为什么说它是”不安全”的?JDK9+有什么替代方案?

答:Unsafe提供了绕过JVM安全检查的底层操作:

  1. 内存操作:allocateMemory/freeMemory(堆外内存)、getInt/putInt(直接内存读写)。
  2. CAS操作:compareAndSwapInt/Long/Object,AtomicXxx类的基础。
  3. 线程调度:park/unpark,LockSupport的基础。
  4. 对象操作:allocateInstance(不调用构造器创建对象)、objectFieldOffset(获取字段偏移量)。
  5. 内存屏障:loadFence/storeFence/fullFence。
    “不安全”原因:1)可以直接操作内存,绕过GC和类型检查;2)可能导致JVM崩溃(段错误);3)不同JVM实现可能不兼容。JDK9+替代:VarHandle(替代CAS和内存屏障)、MethodHandles(替代反射)、Foreign Memory API(替代堆外内存操作)。但Unsafe短期内不会被移除,太多框架依赖它(Netty、Kafka、Disruptor等)。

70. 🔵 什么是Java的注解(Annotation)?运行时注解和编译时注解有什么区别?如何自定义注解?

答:注解是元数据,不直接影响代码逻辑,但可以被编译器、工具或运行时读取。保留策略(@Retention):

  • SOURCE:只在源码中,编译后丢弃。如@Override、@SuppressWarnings、Lombok的@Data。
  • CLASS:保留到class文件,运行时不可见(默认)。如@NonNull(用于静态分析工具)。
  • RUNTIME:运行时可通过反射读取。如@Component、@Transactional、@RequestMapping。
    编译时注解通过Annotation Processor处理(如Lombok、MapStruct),生成代码无运行时开销。运行时注解通过反射处理(如Spring),灵活但有性能开销。自定义注解:
1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int value() default 100; // QPS限制
String key() default ""; // 限流key
}

配合AOP拦截器实现限流逻辑。@Repeatable(JDK8)支持同一位置多次使用同一注解。

71. 🔴 什么是Java的类型擦除对反射的影响?如何在运行时获取泛型的实际类型参数?

答:虽然泛型在运行时被擦除,但某些位置的泛型信息会保留在class文件的Signature属性中:1)类/接口声明的泛型参数(class MyList extends ArrayList<String>);2)字段声明的泛型类型;3)方法参数和返回值的泛型类型。获取方式:

1
2
3
4
5
6
7
8
9
10
11
12
// 获取父类泛型参数
Type type = getClass().getGenericSuperclass();
ParameterizedType pt = (ParameterizedType) type;
Type actualType = pt.getActualTypeArguments()[0]; // String.class

// 获取字段泛型
Field field = clazz.getDeclaredField("list");
ParameterizedType pt = (ParameterizedType) field.getGenericType();

// 获取方法参数泛型
Method method = clazz.getMethod("process", List.class);
Type[] paramTypes = method.getGenericParameterTypes();

Jackson的TypeReference、Gson的TypeToken、Spring的ResolvableType都利用了这个机制。核心技巧:通过创建匿名子类(new TypeReference<List<String>>(){}),泛型信息保留在子类的Signature中。

72. 🔵 什么是Java的异常处理最佳实践?Checked Exception和Unchecked Exception的设计哲学是什么?

答:设计哲学:Checked Exception(编译时检查)用于可恢复的异常(如IOException、SQLException),强制调用方处理。Unchecked Exception(RuntimeException)用于编程错误(如NullPointerException、IllegalArgumentException),不应该被catch而应该修复代码。最佳实践:

  1. 不要catch Exception/Throwable:太宽泛,会掩盖真正的问题。
  2. 不要用异常控制流程:异常创建堆栈信息开销大(fillInStackTrace)。
  3. 自定义业务异常:继承RuntimeException,包含错误码和消息。
  4. 异常转译:底层异常转为上层有意义的异常(如DAOException→ServiceException)。
  5. 不要忽略异常:至少记录日志。
  6. finally中不要return:会吞掉try中的异常。
  7. try-with-resources:自动关闭资源(实现AutoCloseable)。
  8. 考虑异常性能:高频路径避免抛异常,用Optional或错误码替代。预创建异常对象(覆盖fillInStackTrace)可以消除堆栈开销。

73. 🔴 什么是Java的类加载过程中的”准备”和”解析”阶段?静态变量在哪个阶段赋值?

答:类加载五个阶段:加载→验证→准备→解析→初始化。

  • 准备阶段:为类的静态变量分配内存并设置零值(int=0, boolean=false, 引用=null)。注意:static int a = 10在准备阶段a=0,在初始化阶段才赋值为10。但static final int a = 10(编译期常量)在准备阶段直接赋值为10(ConstantValue属性)。
  • 解析阶段:将常量池中的符号引用(类名、方法名、字段名的字符串表示)替换为直接引用(内存地址、偏移量)。解析可以是懒加载的(首次使用时解析)。四种解析:类/接口解析、字段解析、方法解析、接口方法解析。
  • 初始化阶段:执行<clinit>()方法(编译器收集所有静态变量赋值和static块合并而成)。JVM保证<clinit>()线程安全(加锁),这就是为什么静态内部类实现单例是线程安全的。

74. 🔵 什么是Java的方法调用指令?invokevirtual、invokeinterface、invokespecial、invokestatic、invokedynamic各自用于什么场景?

答:五种方法调用指令:

  • invokestatic:调用静态方法。编译期确定,最快。
  • invokespecial:调用构造器、私有方法、super方法。编译期确定。
  • invokevirtual:调用实例方法(虚方法)。运行时根据对象实际类型查找vtable(虚方法表)。
  • invokeinterface:调用接口方法。类似invokevirtual但需要在itable(接口方法表)中查找,比invokevirtual慢(接口方法在不同实现类中的偏移量不固定)。
  • invokedynamic:JDK7引入,Lambda表达式、字符串拼接(JDK9+)使用。运行时通过BootstrapMethod动态绑定调用目标。Lambda的invokedynamic在首次调用时通过LambdaMetafactory生成实现类(而非匿名内部类),性能更好且避免类膨胀。

75. 🔴 什么是JVM的内联缓存(Inline Cache)?单态、多态、超多态调用对性能有什么影响?

答:虚方法调用需要查找vtable,开销较大。内联缓存优化:在调用点缓存上次调用的类型和方法地址。

  • 单态(Monomorphic):调用点只出现一种类型,直接跳转到缓存的方法地址,性能接近静态调用。JIT可以进一步内联该方法。
  • 多态(Polymorphic):调用点出现2-4种类型,使用多态内联缓存(PIC),检查类型后跳转。性能略有下降。
  • 超多态(Megamorphic):调用点出现>4种类型,退化为vtable查找,JIT无法内联。性能显著下降。
    实际影响:接口方法如果实现类很多(如List的各种实现),在热点循环中可能导致超多态调用。优化:1)减少接口实现类的多样性;2)将不同类型的处理分开(避免同一调用点出现多种类型);3)使用具体类型而非接口声明变量(在性能关键路径上)。

76. 🔵 什么是Java的ServiceLoader和模块化系统中的provides/uses?它们有什么区别?

答:ServiceLoader(JDK6):通过META-INF/services/目录下的配置文件发现服务实现。运行时通过反射实例化。问题:1)配置文件容易出错;2)一次性加载所有实现;3)没有编译期检查。模块化的provides/uses(JDK9):在module-info.java中声明:

1
2
3
4
5
module my.module {
uses com.example.MyService; // 声明使用的服务
provides com.example.MyService with // 声明提供的实现
com.example.MyServiceImpl;
}

优势:1)编译期检查(确保provides的类实现了接口);2)模块系统控制可见性;3)ServiceLoader仍然是加载机制,但配置从文件变为module-info。实际中,由于模块化采用率不高,大多数项目仍使用META-INF/services方式。Spring Boot的自动配置也是SPI思想的变种。

77. 🔴 什么是Java的协变返回类型和泛型的PECS原则?

答:协变返回类型(JDK5):子类重写方法时可以返回更具体的类型。如父类方法返回Object,子类可以返回String。编译器生成桥方法(Bridge Method)保证字节码兼容。PECS原则(Producer Extends, Consumer Super):

  • <? extends T>(上界通配符):生产者,只能读取(读出来的是T类型),不能写入(不知道具体类型)。如List<? extends Number>可以读取Number,但不能add。
  • <? super T>(下界通配符):消费者,只能写入T类型,读取只能得到Object。如List<? super Integer>可以add Integer,但get只能得到Object。
    应用:Collections.copy(List<? super T> dest, List<? extends T> src)。Comparator<? super T>(可以用父类的比较器比较子类)。Stream的collect方法签名大量使用PECS。

78. 🔵 什么是Java的枚举(Enum)?它的底层实现是什么?为什么说枚举是实现单例的最佳方式?

答:枚举底层:编译后是继承java.lang.Enum的final类,每个枚举常量是类的static final实例,在类加载的<clinit>()中初始化。枚举实现单例的优势:1)线程安全(类加载机制保证,JVM保证<clinit>()只执行一次);2)防止反射攻击(Constructor.newInstance()对枚举类型抛异常);3)防止反序列化破坏(枚举的反序列化通过valueOf()返回已有实例,不会创建新对象);4)代码简洁。Effective Java推荐的单例实现方式。枚举的高级用法:1)实现接口(策略模式);2)抽象方法(每个常量不同实现);3)EnumSet/EnumMap(基于位运算,性能极高)。

79. 🔴 什么是Java的MethodHandle?它和反射(Reflection)有什么区别?性能差异如何?

答:MethodHandle(JDK7)是对方法的类型安全引用,类似C的函数指针。与反射区别:

  1. 类型安全:MethodHandle有MethodType描述参数和返回值类型,编译期检查。反射的invoke参数是Object[]。
  2. 性能:MethodHandle可以被JIT内联优化(特别是invokeExact),性能接近直接调用。反射每次调用需要检查权限、装箱拆箱,性能差。
  3. 访问控制:MethodHandle在lookup时检查权限(一次),之后调用不再检查。反射每次调用都检查(除非setAccessible(true))。
  4. 功能:MethodHandle支持柯里化(insertArguments)、参数适配(asType)、异常处理(catchException)等组合操作。
    invokedynamic指令底层就是通过BootstrapMethod返回MethodHandle来实现动态绑定。Lambda表达式的实现就依赖MethodHandle。实际使用中,反射更常见(API更直观),MethodHandle主要用于框架底层和invokedynamic。

80. ⚫ 请设计一个Java应用的全链路性能优化方案,从JVM层、框架层、代码层、数据层分别阐述优化策略。

答:

  • JVM层:1)选择合适的GC(G1/ZGC);2)合理设置堆大小(-Xms=-Xmx,避免动态扩缩容);3)开启分层编译(默认);4)大页内存(-XX:+UseLargePages,减少TLB miss);5)NUMA感知(-XX:+UseNUMA)。
  • 框架层:1)连接池调优(HikariCP的minimumIdle、maximumPoolSize);2)线程池调优(根据业务类型配置,动态调整);3)序列化选择(Protobuf/Kryo替代JSON);4)HTTP客户端连接池复用(OkHttp/Apache HttpClient);5)Spring的懒加载(减少启动时间)。
  • 代码层:1)避免在循环中创建对象(对象复用、StringBuilder);2)集合初始化指定容量(避免扩容);3)使用基本类型替代包装类型(避免装箱);4)异步化非关键路径(CompletableFuture/消息队列);5)缓存计算结果(Caffeine);6)减少锁粒度(分段锁、读写锁、无锁数据结构)。
  • 数据层:1)SQL优化(索引、避免全表扫描、批量操作);2)读写分离;3)多级缓存(本地→Redis→DB);4)数据库连接池调优;5)慢SQL监控和告警。

四、Java 新特性与框架深度(81-120题)

81. 🔵 什么是Spring Boot 3.x的主要变化?从Spring Boot 2.x迁移需要注意什么?

答:主要变化:1)最低Java 17(利用Record、Sealed Class、Pattern Matching等新特性);2)Jakarta EE 9+(javax.*→jakarta.*命名空间迁移,影响所有Servlet、JPA、Validation等API);3)GraalVM Native Image支持(Spring AOT引擎,编译期处理Bean定义);4)Micrometer Observation API(统一可观测性,替代Sleuth);5)HTTP Interface Client(声明式HTTP客户端,类似Feign但原生支持)。迁移注意:1)javax→jakarta是最大的破坏性变更,需要全局替换;2)第三方库兼容性(确保依赖支持Jakarta EE);3)Spring Security配置方式变化(Lambda DSL);4)Actuator端点路径变化;5)Properties/YAML配置项变化。建议:先升级到Spring Boot 2.7(有兼容性提示),再迁移到3.x。

82. 🔴 什么是Spring的事件机制?ApplicationEvent和@EventListener有什么区别?如何实现异步事件?

答:Spring事件机制基于观察者模式。ApplicationEvent:自定义事件继承ApplicationEvent,通过ApplicationEventPublisher.publishEvent()发布。@EventListener(JDK4.2+):注解方式监听事件,无需实现ApplicationListener接口,更简洁。支持条件过滤(condition属性)、排序(@Order)。异步事件:1)@Async + @EventListener:方法级异步,需要@EnableAsync;2)自定义ApplicationEventMulticaster,设置TaskExecutor。注意事项:1)默认同步执行,发布者等待所有监听器执行完毕;2)事务事件:@TransactionalEventListener,在事务提交后/回滚后触发(phase = AFTER_COMMIT),避免事务未提交就处理事件导致数据不一致;3)异步事件中异常不会传播给发布者,需要单独处理。实际应用:订单创建后发送通知、用户注册后初始化数据等解耦场景。

83. 🔵 什么是Spring的BeanPostProcessor?它在Spring生态中有哪些重要应用?

答:BeanPostProcessor是Spring最核心的扩展机制,在Bean初始化前后执行自定义逻辑。两个方法:postProcessBeforeInitialization(初始化前)、postProcessAfterInitialization(初始化后)。重要应用:

  1. AOP代理:AbstractAutoProxyCreator在postProcessAfterInitialization中创建代理对象(JDK动态代理或CGLIB)。
  2. @Autowired注入:AutowiredAnnotationBeanPostProcessor处理@Autowired/@Value注入。
  3. @Async:AsyncAnnotationBeanPostProcessor创建异步代理。
  4. @Scheduled:ScheduledAnnotationBeanPostProcessor注册定时任务。
  5. @PostConstruct/@PreDestroy:CommonAnnotationBeanPostProcessor处理生命周期回调。
  6. 配置属性绑定:ConfigurationPropertiesBindingPostProcessor处理@ConfigurationProperties。
    执行顺序通过@Order或Ordered接口控制。BeanFactoryPostProcessor在Bean实例化之前执行(修改BeanDefinition),如PropertySourcesPlaceholderConfigurer处理${}占位符。

84. 🔴 什么是Spring的条件化配置?@Conditional的实现原理是什么?如何自定义Condition?

答:@Conditional根据条件决定是否注册Bean或配置类。实现原理:Spring在解析@Configuration类时,检查@Conditional注解指定的Condition实现类的matches()方法,返回true则注册,false则跳过。Spring Boot内置的条件注解:

  • @ConditionalOnClass/OnMissingClass:类路径中是否存在某个类。
  • @ConditionalOnBean/OnMissingBean:容器中是否存在某个Bean。
  • @ConditionalOnProperty:配置属性是否满足条件。
  • @ConditionalOnWebApplication:是否是Web应用。
  • @ConditionalOnExpression:SpEL表达式。
    自定义Condition:
1
2
3
4
5
6
7
8
9
public class OnLinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().getProperty("os.name").contains("Linux");
}
}
@Conditional(OnLinuxCondition.class)
@Bean
public MyService myService() { ... }

SpringBootCondition是Spring Boot的基础条件类,提供了日志记录和ConditionEvaluationReport支持。

85. 🔵 什么是MyBatis的一级缓存和二级缓存?它们各自有什么问题?

答:

  • 一级缓存(SqlSession级别):默认开启,同一个SqlSession中相同查询直接返回缓存结果。问题:1)Spring中每次请求通常是新的SqlSession(除非在同一个事务中),一级缓存命中率低;2)多SqlSession之间不共享,可能读到脏数据;3)任何增删改操作会清空整个一级缓存(不是精确失效)。
  • 二级缓存(Mapper级别):需要手动开启(<cache/>),跨SqlSession共享。问题:1)粒度太粗,同一个Mapper的任何写操作会清空该Mapper的所有缓存;2)多表关联查询时,关联表的更新不会触发缓存失效(脏读);3)分布式环境下需要序列化+共享存储(如Redis),配置复杂。
    实际建议:关闭MyBatis二级缓存,使用应用层缓存(Caffeine/Redis),更灵活可控。一级缓存在事务内有用,但要注意脏读风险。

86. 🔴 什么是MyBatis的插件机制?Interceptor是如何实现的?PageHelper分页插件的原理是什么?

答:MyBatis插件基于动态代理,可以拦截四大对象的方法:Executor(执行器)、StatementHandler(SQL处理)、ParameterHandler(参数处理)、ResultSetHandler(结果集处理)。实现:@Intercepts + @Signature注解指定拦截的类和方法,实现Interceptor接口的intercept()方法。MyBatis用JDK动态代理包装目标对象,多个插件形成代理链(责任链模式)。PageHelper原理:1)拦截Executor.query()方法;2)通过ThreadLocal获取分页参数(PageHelper.startPage()设置);3)改写原始SQL,添加COUNT查询(获取总数)和LIMIT/OFFSET(分页);4)将结果封装为Page对象(包含总数、当前页数据)。注意:PageHelper必须紧跟在查询方法之前调用(ThreadLocal在查询后自动清理),否则可能影响其他查询。

87. 🔵 什么是Spring Data JPA的N+1查询问题?如何解决?

答:N+1问题:查询N个实体时,每个实体的关联对象触发一次额外查询,总共1+N次SQL。例如:查询10个Order,每个Order的User触发一次查询,共11次SQL。原因:JPA的懒加载(FetchType.LAZY),访问关联对象时才触发查询。解决方案:

  1. JOIN FETCH:JPQL中SELECT o FROM Order o JOIN FETCH o.user,一次查询加载关联对象。
  2. @EntityGraph:声明式指定加载策略,@EntityGraph(attributePaths = {"user"})
  3. @BatchSize:Hibernate批量加载,@BatchSize(size = 100),将N次查询合并为N/100次IN查询。
  4. DTO投影:直接查询需要的字段,避免加载整个实体。SELECT new OrderDTO(o.id, u.name) FROM Order o JOIN o.user u
  5. 二级缓存:关联对象缓存后不再查询数据库。
    最佳实践:默认LAZY加载,需要时用JOIN FETCH或@EntityGraph按需加载。

88. 🔴 什么是Hibernate的Session和JPA的EntityManager?一级缓存(持久化上下文)是如何工作的?

答:Session/EntityManager管理实体的生命周期,维护一级缓存(持久化上下文,Persistence Context)。实体状态:

  • Transient(瞬时):new出来的对象,未与Session关联。
  • Persistent(持久):与Session关联,在一级缓存中。修改会自动同步到数据库(脏检查,Dirty Checking)。
  • Detached(游离):Session关闭后,对象脱离管理。修改不会自动同步。
  • Removed(删除):标记为删除,flush时执行DELETE。
    一级缓存工作:1)persist/save将对象放入缓存;2)find/get先查缓存,未命中再查数据库;3)flush时对比缓存中对象的当前状态和快照(Snapshot),生成UPDATE SQL(脏检查)。脏检查机制:Session维护每个实体的加载时快照,flush时逐字段比较。优化:@DynamicUpdate只更新变化的字段(而非所有字段)。注意:大批量操作时一级缓存可能导致OOM,需要定期flush+clear。

89. 🔵 什么是连接池?HikariCP为什么比其他连接池快?核心参数如何配置?

答:连接池复用数据库连接,避免频繁创建/销毁连接的开销。HikariCP快的原因:1)字节码级优化:用Javassist生成代理类(而非JDK动态代理),减少方法调用开销;2)FastList替代ArrayList:不做范围检查,remove从尾部开始搜索(Connection通常后获取先归还);3)ConcurrentBag:自定义的无锁集合,线程优先获取自己之前使用的连接(ThreadLocal缓存),减少竞争;4)精简代码:代码量只有其他连接池的1/10,减少bug和开销。核心参数:

  • maximumPoolSize:最大连接数,建议 = CPU核心数 × 2 + 磁盘数(PostgreSQL建议公式)。通常10-20足够。
  • minimumIdle:最小空闲连接数,建议等于maximumPoolSize(避免连接创建延迟)。
  • connectionTimeout:获取连接超时,默认30秒,建议3-5秒。
  • maxLifetime:连接最大存活时间,建议比数据库的wait_timeout小几分钟。
  • idleTimeout:空闲连接超时,minimumIdle < maximumPoolSize时生效。

90. 🔴 什么是Spring的@Transactional注解的常见陷阱?请列举至少5个。

答:常见陷阱:

  1. self-invocation:同一个类中方法A调用方法B,B的@Transactional不生效(不经过代理)。解决:注入自身或使用AopContext.currentProxy()。
  2. 非public方法:Spring AOP默认只代理public方法,protected/private上的@Transactional无效。
  3. 异常类型:默认只对RuntimeException和Error回滚,Checked Exception不回滚。需要@Transactional(rollbackFor = Exception.class)
  4. catch了异常:方法内catch异常后没有重新抛出,事务不会回滚(Spring感知不到异常)。
  5. 传播机制误用:REQUIRES_NEW中的异常被外层catch,外层事务继续执行,但内层已回滚。
  6. 多数据源:@Transactional默认使用primary数据源的事务管理器,多数据源需要指定transactionManager。
  7. 长事务:事务方法中包含RPC调用或大量计算,导致数据库连接长时间占用。
  8. 只读事务:@Transactional(readOnly = true)在某些数据库/驱动下不生效,需要确认。

91. 🔵 什么是Java的日志框架?SLF4J、Logback、Log4j2的关系是什么?如何选择?

答:SLF4J是日志门面(接口),Logback和Log4j2是日志实现。关系:SLF4J定义API,通过桥接器连接不同实现。Logback是SLF4J的原生实现(同一作者),Spring Boot默认使用。Log4j2是Apache的实现,性能更好(异步日志基于Disruptor)。选择:

  • Spring Boot项目:默认Logback即可,简单够用。
  • 高性能要求:Log4j2的AsyncLogger,吞吐量是Logback的10倍+。
  • 统一日志框架:用SLF4J门面 + 桥接器统一第三方库的日志输出(如jul-to-slf4j、log4j-over-slf4j)。
    最佳实践:1)使用参数化日志log.info("user {} login", userId)而非字符串拼接;2)合理设置日志级别(生产环境INFO,调试时DEBUG);3)异步日志减少IO阻塞;4)结构化日志(JSON格式)便于ELK收集分析;5)MDC传递TraceID实现链路追踪。

92. 🔴 什么是Java的响应式数据库访问?R2DBC和JDBC有什么区别?

答:R2DBC(Reactive Relational Database Connectivity)是响应式关系数据库驱动规范。与JDBC区别:1)JDBC是阻塞IO(一个查询占用一个线程直到结果返回),R2DBC是非阻塞IO(基于Reactive Streams,不阻塞线程);2)JDBC返回ResultSet,R2DBC返回Flux/Mono;3)R2DBC不支持JPA/Hibernate(它们深度依赖JDBC的阻塞模型)。支持的数据库:PostgreSQL、MySQL、MariaDB、H2、Oracle、SQL Server。Spring Data R2DBC提供Repository抽象。局限:1)生态不如JDBC成熟;2)不支持存储过程、批量操作等高级特性;3)调试困难(响应式堆栈);4)ORM支持有限。适用场景:WebFlux应用需要全链路非阻塞时使用。JDK21虚拟线程+JDBC在大多数场景下是更简单的替代方案。

93. 🔵 什么是Quarkus和Micronaut?它们和Spring Boot有什么区别?

答:Quarkus(RedHat)和Micronaut(OCI)是面向云原生的Java框架。与Spring Boot区别:

  1. 编译时处理:Quarkus/Micronaut在编译期完成依赖注入、AOP代理等工作(而非Spring的运行时反射),启动更快、内存更少。
  2. GraalVM Native Image:原生支持AOT编译,启动时间毫秒级。Spring Boot 3.x也支持但成熟度稍低。
  3. 启动时间:Native Image下Quarkus约10ms,Spring Boot约50-100ms(JVM模式下差距更大)。
  4. 内存占用:Native Image下Quarkus约10-30MB,Spring Boot约50-100MB。
  5. 生态:Spring Boot生态最丰富,社区最大。Quarkus/Micronaut生态在快速增长。
    选型:Serverless/FaaS场景(冷启动敏感)优先Quarkus/Micronaut;传统微服务Spring Boot仍是首选(生态、人才、稳定性)。

94. 🔴 什么是Java的Project Loom对现有框架的影响?Spring、Tomcat、JDBC等如何适配虚拟线程?

答:虚拟线程(JDK21)对框架的影响和适配:

  • Tomcat 10.1+:支持虚拟线程执行器,每个请求一个虚拟线程。配置:server.tomcat.threads.virtual=true(Spring Boot 3.2+)。
  • Spring Boot 3.2+spring.threads.virtual.enabled=true,自动将Tomcat、Jetty、定时任务、异步任务切换到虚拟线程。
  • JDBC:虚拟线程遇到JDBC阻塞IO时自动卸载载体线程,但synchronized会pin住载体线程。HikariCP 5.1+已修复内部synchronized为ReentrantLock。
  • 注意事项:1)synchronized→ReentrantLock(避免pin);2)ThreadLocal→ScopedValue(虚拟线程数量巨大,ThreadLocal内存开销大);3)不要池化虚拟线程(创建成本极低);4)CPU密集型任务不适合虚拟线程。
    影响:WebFlux的主要优势(非阻塞IO)被虚拟线程+传统阻塞IO替代,Spring MVC + 虚拟线程成为更简单的高并发方案。

95. 🔵 什么是Java的密封接口(Sealed Interface)在领域驱动设计中的应用?

答:密封接口在DDD中的应用:用Sealed Interface建模领域中有限的类型集合,编译器保证穷举性。

1
2
3
4
5
6
sealed interface OrderStatus permits Created, Paid, Shipped, Completed, Cancelled {}
record Created(LocalDateTime time) implements OrderStatus {}
record Paid(LocalDateTime time, BigDecimal amount) implements OrderStatus {}
record Shipped(LocalDateTime time, String trackingNo) implements OrderStatus {}
record Completed(LocalDateTime time) implements OrderStatus {}
record Cancelled(LocalDateTime time, String reason) implements OrderStatus {}

配合模式匹配实现状态机:

1
2
3
4
5
6
7
8
9
String describe(OrderStatus status) {
return switch (status) {
case Created c -> "订单创建于 " + c.time();
case Paid p -> "已支付 " + p.amount();
case Shipped s -> "运单号 " + s.trackingNo();
case Completed c -> "已完成";
case Cancelled c -> "取消原因: " + c.reason();
}; // 编译器检查穷举,新增状态必须处理
}

优势:1)类型安全,新增状态时编译器强制所有switch处理;2)替代枚举+策略模式,每个状态可以携带不同数据;3)替代访问者模式,代码更简洁。

96. 🔴 什么是Java的Vector API(JDK22孵化)?它如何利用SIMD指令提升计算性能?

答:Vector API允许Java代码显式使用SIMD(Single Instruction Multiple Data)指令,一条指令同时处理多个数据。传统方式:JIT的自动向量化(Auto-Vectorization)不可靠,复杂循环无法优化。Vector API:

1
2
3
4
5
6
7
8
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256; // 256位,8个float
void vectorAdd(float[] a, float[] b, float[] c) {
for (int i = 0; i < a.length; i += SPECIES.length()) {
var va = FloatVector.fromArray(SPECIES, a, i);
var vb = FloatVector.fromArray(SPECIES, b, i);
va.add(vb).intoArray(c, i);
}
}

一次处理8个float加法(256bit / 32bit = 8)。性能提升:数值计算、图像处理、机器学习推理等场景可提升4-16倍。底层映射到CPU的AVX/AVX-512/NEON指令。目前仍在孵化阶段,API可能变化。适用场景:科学计算、金融风控(大量浮点运算)、编解码、相似度计算。

97. 🔵 什么是Java的文本块(Text Block,JDK15)和字符串模板(String Template,JDK21预览)?

答:文本块:多行字符串字面量,用三引号"""包围。自动处理缩进(公共前导空白被移除)、换行。适合嵌入SQL、JSON、HTML。

1
2
3
4
5
6
String json = """
{
"name": "test",
"value": 42
}
""";

字符串模板(JDK21预览,JDK23移除预览重新设计中):类型安全的字符串插值。

1
2
3
4
String name = "World";
String msg = STR."Hello \{name}!"; // Hello World!
// 支持表达式
String info = STR."1 + 1 = \{1 + 1}"; // 1 + 1 = 2

与简单拼接的区别:模板处理器(StringTemplate.Processor)可以自定义处理逻辑,如SQL模板处理器自动防注入、JSON模板处理器自动转义。这是比String.format()更安全、更高效的方案。

98. 🔴 什么是Java的Gatherer API(JDK22预览)?它如何扩展Stream的中间操作能力?

答:Gatherer是Stream API的扩展点,允许自定义中间操作(之前只能自定义终端操作Collector)。解决的问题:某些操作(如滑动窗口、去重保留前N个、有状态的转换)用标准Stream操作难以表达。核心接口:Gatherer<T, A, R>,包含initializer(初始状态)、integrator(处理每个元素)、combiner(并行合并)、finisher(最终处理)。

1
2
3
4
// 滑动窗口示例
stream.gather(Gatherers.windowSliding(3))
.forEach(window -> System.out.println(window));
// [1,2,3], [2,3,4], [3,4,5]...

内置Gatherer:windowFixed(固定窗口)、windowSliding(滑动窗口)、fold(有状态折叠)、scan(前缀扫描)、mapConcurrent(并发映射,限制并发度)。Gatherer之于中间操作,如同Collector之于终端操作,大幅增强了Stream的表达能力。

99. 🔵 什么是Java的Class Data Sharing(CDS)和Application CDS?它如何加速启动?

答:CDS将类的元数据预处理后存储在共享归档文件(.jsa)中,JVM启动时直接内存映射加载,跳过类解析和验证。

  • 默认CDS:JDK12+默认开启,共享JDK核心类(约1200个类)。
  • Application CDS(AppCDS):将应用类也加入共享归档。步骤:1)-XX:DumpLoadedClassList=classes.lst记录加载的类;2)-Xshare:dump -XX:SharedClassListFile=classes.lst -XX:SharedArchiveFile=app.jsa生成归档;3)-Xshare:on -XX:SharedArchiveFile=app.jsa使用归档。
  • Dynamic CDS(JDK13+):运行时自动生成归档,无需手动步骤。-XX:ArchiveClassesAtExit=app.jsa
    效果:启动时间减少10-30%,内存减少(多个JVM进程共享同一份类元数据)。Spring Boot 3.x的AOT + CDS组合可以显著加速启动。

100. ⚫ 请设计一个高性能的Java微服务框架,要求:启动时间<1秒、内存<100MB、支持GraalVM Native Image、支持依赖注入和AOP。核心的编译时处理和运行时机制如何设计?

答:核心设计思路(参考Quarkus/Micronaut):

  • 编译时依赖注入:注解处理器(Annotation Processor)在编译期扫描@Inject/@Singleton等注解,生成Bean工厂代码(而非运行时反射创建)。每个Bean生成一个$BeanFactory类,包含创建逻辑和依赖关系。
  • 编译时AOP:注解处理器生成代理类(而非运行时CGLIB/JDK代理)。拦截器链在编译期确定,运行时直接调用。
  • 编译时配置处理:解析application.yml,生成类型安全的配置类,避免运行时反射绑定。
  • GraalVM兼容:1)消除反射(编译时生成所有代码);2)消除动态类加载;3)自动生成reflect-config.json(对于无法避免的反射);4)消除动态代理(编译时生成)。
  • 运行时机制:1)Bean容器只是一个HashMap<Class, Object>,启动时按依赖顺序调用工厂方法;2)无类路径扫描(编译时已确定所有Bean);3)懒加载(按需创建Bean)。
  • 启动优化:AppCDS + 编译时处理 + 最小化依赖。Native Image下启动<50ms。

五、补充高频深度题(101-120题)

101. 🔵 什么是Java的happens-before在实际并发编程中的应用?请举一个不使用synchronized/volatile也能保证可见性的例子。

答:利用happens-before的传递性。例如:线程A写入一个普通变量x,然后写入volatile变量v;线程B读取volatile变量v,然后读取普通变量x。由于volatile写happens-before volatile读(volatile规则),加上程序顺序规则和传递性,线程B能看到线程A对x的修改。这就是”借助volatile的可见性传播”。实际应用:ConcurrentHashMap的get()不加锁但能看到最新值,因为Node的val和next是volatile的。另一个例子:CountDownLatch.countDown() happens-before await()返回,所以countDown之前的所有写操作对await之后的代码可见,无需额外同步。

102. 🔴 什么是Java的对象逃逸和锁粗化/锁消除?JIT编译器如何优化synchronized?

答:JIT对synchronized的优化:

  • 锁消除(Lock Elision):逃逸分析发现锁对象不会逃逸出方法,直接消除锁。如方法内创建的StringBuffer,其synchronized操作会被消除。
  • 锁粗化(Lock Coarsening):连续多次对同一对象加锁解锁,合并为一次。如循环内的synchronized合并到循环外。
  • 自适应自旋(Adaptive Spinning):轻量级锁CAS失败后不立即阻塞,先自旋等待。JVM根据历史数据动态调整自旋次数(上次成功则多旋,上次失败则少旋或不旋)。
  • 偏向锁:无竞争时只需一次CAS,后续进入同步块只需检查线程ID。
    验证:-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining查看JIT优化决策。JMH的@CompilerControl可以控制JIT行为进行基准测试。

103. 🔵 什么是Java的弱一致性迭代器?ConcurrentHashMap的迭代器为什么不会抛ConcurrentModificationException?

答:Java集合的迭代器分两类:

  • 快速失败(fail-fast):ArrayList、HashMap等,迭代期间检测到结构修改(modCount变化)立即抛ConcurrentModificationException。
  • 弱一致性(weakly consistent):ConcurrentHashMap、CopyOnWriteArrayList等,迭代期间允许修改,迭代器反映创建时或之后的某个状态。
    ConcurrentHashMap迭代器原理:遍历Node数组和链表/红黑树,不加锁。由于Node的val和next是volatile的,能看到最新值。但不保证看到迭代开始后的所有修改(可能看到部分)。CopyOnWriteArrayList:迭代器持有创建时的数组快照引用,写操作复制新数组,互不影响。适合读多写少场景(如监听器列表)。

104. 🔴 什么是Java的内存泄漏检测工具和方法论?如何建立内存泄漏的预防机制?

答:检测工具:

  • JVisualVM/JConsole:实时监控堆内存趋势,持续增长可能是泄漏。
  • MAT(Eclipse Memory Analyzer):分析堆dump,Leak Suspects报告、Dominator Tree、Path to GC Roots。
  • Arthas:heapdump导出、dashboard实时监控、profiler火焰图。
  • JFR:持续记录对象分配热点(TLAB外分配)。
  • async-profiler:低开销的分配profiling。
    方法论:1)对比两个时间点的堆dump,找增长最多的对象类型;2)从增长对象出发,查找GC Root引用链,定位持有者;3)分析代码确认是否为泄漏(预期持有 vs 意外持有)。
    预防机制:1)代码review关注静态集合、ThreadLocal、资源关闭;2)CI中集成内存压测(长时间运行后检查堆增长);3)生产环境监控堆内存趋势告警;4)使用try-with-resources管理资源;5)WeakReference/SoftReference用于缓存。

105. 🔵 什么是Java的JMH(Java Microbenchmark Harness)?如何正确编写微基准测试?常见的测试陷阱有哪些?

答:JMH是OpenJDK官方的微基准测试框架。正确使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 10, time = 1)
@Fork(2) // 多个JVM进程,避免JIT优化偏差
@State(Scope.Thread)
public class MyBenchmark {
@Benchmark
public int testMethod(Blackhole bh) {
int result = compute();
bh.consume(result); // 防止死代码消除
return result;
}
}

常见陷阱:1)死代码消除(DCE):JIT发现结果未使用,直接消除计算。用Blackhole.consume()或返回结果;2)常量折叠:JIT将常量表达式在编译期计算。用@State中的变量;3)循环优化:JIT可能将循环展开或消除。用@OperationsPerInvocation;4)预热不足:JIT未充分优化。增加Warmup迭代;5)GC干扰:大量对象分配触发GC。用@Fork隔离;6)伪共享:多线程测试时注意@State的Scope。

106. 🔴 什么是Java的协程库(如Quasar/Kilim)?它们和JDK21虚拟线程有什么区别?

答:Quasar/Kilim是JDK21之前的Java协程方案。实现原理:通过字节码增强(Java Agent),在方法的挂起点(如IO操作、sleep)保存栈帧到堆中,恢复时从堆中加载栈帧。本质是Continuation的用户态实现。与虚拟线程区别:1)虚拟线程是JVM原生支持,无需字节码增强,更可靠;2)虚拟线程自动识别阻塞点(所有java.* IO操作),Quasar需要手动标记@Suspendable;3)虚拟线程与现有API完全兼容(Thread、ExecutorService等),Quasar需要使用专用API(Fiber);4)虚拟线程性能更好(JVM级别优化)。结论:JDK21+直接使用虚拟线程,Quasar/Kilim已无必要。

107. 🔵 什么是Java的ClassFile API(JDK22预览)?它如何替代ASM进行字节码操作?

答:ClassFile API是JDK内置的字节码读写API,目标是替代第三方库ASM。优势:1)JDK内置,无需额外依赖,版本始终与JDK同步(ASM需要跟进新版本class文件格式);2)不可变模型(Immutable),线程安全;3)流式API,更易读;4)惰性解析,只解析需要的部分,性能好。

1
2
3
4
5
6
7
8
9
10
ClassFile.of().build(CD_MyClass, classBuilder -> {
classBuilder.withMethod("hello", MTD_void, ACC_PUBLIC, methodBuilder -> {
methodBuilder.withCode(codeBuilder -> {
codeBuilder.getstatic(CD_System, "out", CD_PrintStream)
.ldc("Hello")
.invokevirtual(CD_PrintStream, "println", MTD_void_String)
.return_();
});
});
});

JDK内部已开始用ClassFile API替代ASM(如Lambda的LambdaMetafactory)。框架迁移需要时间,短期内ASM仍是主流。

108. 🔴 什么是Java的Compact Strings和Indified String Concatenation?它们如何优化字符串性能?

答:Compact Strings(JDK9):String内部从char[]改为byte[]。Latin1字符(ASCII等)用1字节存储(LATIN1编码),非Latin1字符用2字节存储(UTF16编码)。coder字段标记编码类型。效果:大多数英文场景下String内存减少约50%。对中文字符串无优化(仍然UTF16)。Indified String Concatenation(JDK9):字符串拼接"a" + b + "c"不再编译为StringBuilder.append链,而是编译为invokedynamic指令,运行时由StringConcatFactory生成最优的拼接策略。策略包括:1)MH_INLINE_SIZED_EXACT:预计算结果长度,一次分配byte[],直接填充(最快);2)BC_SB:生成StringBuilder字节码。效果:比StringBuilder快约30%(避免了StringBuilder的扩容和中间对象)。-XX:-CompactStrings可以关闭Compact Strings(不推荐)。

109. 🔵 什么是Java的模块化对反射的影响?–add-opens和–add-exports有什么区别?

答:JDK9模块化后,非导出包中的类默认不能被反射访问。影响:很多框架依赖反射访问内部API(如Spring、Hibernate、Jackson),升级JDK9+后会报InaccessibleObjectException。

  • –add-exports:允许编译时和运行时访问指定包的public类型。--add-exports java.base/sun.nio.ch=ALL-UNNAMED
  • –add-opens:允许运行时深度反射(setAccessible(true))访问指定包的所有类型(包括private)。--add-opens java.base/java.lang=ALL-UNNAMED
    区别:exports只开放public API的编译时访问,opens开放运行时反射的深度访问。Spring Boot 3.x通过AOT编译减少了对反射的依赖,但仍需要部分–add-opens。长期方案:框架迁移到公开API(如Unsafe→VarHandle,sun.misc→java.lang.invoke)。

110. 🔴 什么是Java的ZGC的染色指针和读屏障?它们是如何实现并发整理的?

答:ZGC的核心创新:

  • 染色指针(Colored Pointers):利用64位指针中的4个bit存储元数据:Marked0、Marked1(标记位,交替使用)、Remapped(重映射完成)、Finalizable(需要finalize)。指针的低44位是实际地址(支持16TB堆)。通过多重映射(Multi-Mapping),同一物理内存映射到多个虚拟地址(不同颜色对应不同虚拟地址),CPU访问任何颜色的指针都能到达同一物理内存。
  • 读屏障(Load Barrier):每次从堆中加载引用时检查指针颜色。如果颜色不对(如对象已被移动但指针未更新),读屏障会”自愈”——将指针更新为新地址。这样不需要STW来更新所有引用。
  • 并发整理流程:1)并发标记(标记存活对象);2)并发预备重分配(选择要整理的Region);3)并发重分配(将对象复制到新Region,旧Region的转发表记录新旧地址映射);4)并发重映射(更新引用,但不急迫,读屏障会自愈)。
    ZGC的STW只在初始标记和再标记阶段,通常<1ms。

111. 🔵 什么是Java的G1的String Deduplication?它如何减少内存占用?

答:G1的String Deduplication(-XX:+UseStringDeduplication,JDK8u20+):在Young GC时,检查存活的String对象,如果多个String的char[]/byte[]内容相同,让它们共享同一个数组。实现:1)Young GC标记阶段,将新晋升到老年代的String加入去重队列;2)后台线程从队列取出String,计算内容哈希值;3)在哈希表中查找是否已有相同内容的数组;4)如果有,将String的value字段指向已有数组,旧数组可被GC回收。效果:对于大量重复字符串的应用(如XML/JSON解析、日志处理),内存减少10-30%。注意:1)只对老年代的String生效;2)去重有CPU开销(后台线程);3)不影响String.intern()(intern是常量池级别,dedup是GC级别)。-XX:StringDeduplicationAgeThreshold控制String存活多少次GC后才参与去重(默认3)。

112. 🔴 什么是Java的Epsilon GC和Shenandoah GC?它们各自适用什么场景?

答:

  • Epsilon GC(JDK11):No-Op垃圾回收器,只分配内存不回收。堆满了直接OOM。适用场景:1)性能测试(排除GC影响,测量纯应用性能);2)极短生命周期的应用(如CLI工具,运行完就退出);3)内存分配极少的应用;4)GC算法研究的基准对比。
  • Shenandoah GC(JDK12+,RedHat):低延迟GC,目标与ZGC类似(亚毫秒级停顿)。核心技术:Brooks Pointer(转发指针),每个对象头部额外一个指针,未移动时指向自己,移动后指向新位置。读写操作都通过转发指针间接访问。与ZGC区别:1)Shenandoah用转发指针+读写屏障,ZGC用染色指针+读屏障;2)Shenandoah的转发指针每个对象额外8字节开销;3)ZGC需要64位系统,Shenandoah支持32位(理论上)。选型:JDK17+推荐ZGC(Oracle官方支持),RedHat系统可选Shenandoah。

113. 🔵 什么是Java的JFR事件流(JFR Event Streaming,JDK14)?它如何实现持续监控?

答:JDK14之前,JFR数据只能事后分析(录制→导出文件→JMC分析)。JFR Event Streaming允许实时消费JFR事件:

1
2
3
4
5
6
7
8
9
10
11
try (var rs = new RecordingStream()) {
rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
rs.enable("jdk.GarbageCollection");
rs.onEvent("jdk.CPULoad", event -> {
System.out.println("CPU: " + event.getFloat("machineTotal"));
});
rs.onEvent("jdk.GarbageCollection", event -> {
System.out.println("GC: " + event.getDuration("duration"));
});
rs.start(); // 阻塞,持续消费事件
}

应用场景:1)自定义监控指标导出(替代JMX的部分场景);2)实时告警(GC时间超阈值、CPU超限);3)自适应调优(根据运行时指标动态调整参数);4)与Prometheus/Grafana集成(通过Micrometer)。优势:低开销(<2%)、无需外部Agent、事件类型丰富(200+种JDK内置事件)。

114. 🔴 什么是Java的CDS归档和AOT编译的结合?Spring Boot 3.2+的启动优化方案是什么?

答:Spring Boot 3.2+的启动优化三板斧:

  1. Spring AOT(Ahead-of-Time Processing):编译期处理Bean定义、配置属性绑定、代理生成等,生成优化后的Java代码,避免运行时反射和类路径扫描。mvn spring-boot:process-aot
  2. CDS(Class Data Sharing):将AOT处理后的类预加载到共享归档。Spring Boot 3.2+提供-Dspring.context.exit=onRefresh训练运行,自动生成CDS归档。
  3. GraalVM Native Image:将整个应用编译为本地可执行文件。Spring Boot 3.x通过AOT引擎提供Native Image支持。
    效果对比(典型Spring Boot应用):
  • JVM模式:启动2-5秒,内存200-400MB
  • JVM + AOT + CDS:启动1-2秒,内存150-300MB
  • Native Image:启动50-200ms,内存50-100MB
    选型:生产环境长期运行的服务用JVM模式(JIT峰值性能更好);Serverless/FaaS用Native Image。

115. 🔵 什么是Java的异步日志?Log4j2的AsyncLogger性能为什么远超同步日志?

答:同步日志:每次log调用都执行格式化+IO写入,IO是瓶颈。异步日志:log调用只将事件放入队列,后台线程批量写入。Log4j2 AsyncLogger基于Disruptor(LMAX无锁环形队列):1)日志事件放入RingBuffer(CAS操作,无锁);2)后台线程从RingBuffer批量取出事件,格式化并写入文件。性能:AsyncLogger吞吐量是同步Logger的6-68倍(官方基准测试)。配置:-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector全局异步。注意事项:1)应用崩溃时队列中的日志可能丢失(可配置immediateFlush但影响性能);2)队列满时的策略:丢弃(默认)或阻塞(影响业务线程);3)异步日志中获取调用位置信息(类名、行号)开销大(需要生成堆栈),建议关闭。

116. 🔴 什么是Java的内存映射文件(Memory-Mapped File)?它的优势和风险是什么?

答:内存映射文件通过FileChannel.map()将文件映射到虚拟内存地址空间,读写文件如同读写内存。优势:1)零拷贝(OS的Page Cache直接映射,无需用户态/内核态数据拷贝);2)随机访问高效(直接通过内存地址访问,无需seek);3)多进程共享(多个进程映射同一文件,共享Page Cache)。RocketMQ用mmap读写CommitLog和ConsumeQueue。风险:1)文件大小受虚拟地址空间限制(32位系统最大2GB);2)MappedByteBuffer没有显式unmap方法(依赖GC回收,可能导致文件无法删除/修改);3)页面错误(Page Fault)导致延迟不可预测(首次访问未加载的页面需要磁盘IO);4)堆外内存不受-Xmx限制,可能导致OOM Killer。解决unmap问题:JDK9+可用Unsafe.invokeCleaner(),或JDK19+的MemorySegment(Arena管理生命周期)。

117. 🔵 什么是Java的ServiceMesh场景下的Sidecar对Java应用的影响?如何优化?

答:Sidecar(如Envoy/Istio)对Java应用的影响:1)额外网络跳转延迟(应用→Sidecar→网络→Sidecar→应用,增加1-3ms);2)CPU开销(Sidecar的TLS终止、协议解析、遥测数据收集);3)内存开销(每个Pod额外100-200MB)。优化方案:1)连接池复用(减少TLS握手次数);2)HTTP/2多路复用(减少连接数);3)Sidecar资源限制合理设置(CPU/Memory request/limit);4)选择性启用功能(不需要mTLS的内部服务可以关闭);5)Proxyless模式(gRPC的xDS API直接与控制面通信,无需Sidecar代理);6)eBPF加速(Cilium的eBPF数据面,绕过Sidecar的用户态代理)。Ambient Mesh(Istio新模式):用共享的ztunnel替代每Pod的Sidecar,减少资源开销。

118. 🔴 什么是Java的GC日志分析?如何从GC日志中诊断问题?

答:JDK9+统一GC日志格式:-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=100m。关键指标:

  • GC频率:Young GC每秒>5次可能需要增大新生代;Full GC每小时>1次需要排查。
  • GC耗时:Young GC >50ms、Full GC >1s需要关注。
  • 回收效率:GC前后堆占用对比,如果Full GC后老年代占用仍然很高,可能是内存泄漏。
  • 晋升大小:每次Young GC晋升到老年代的数据量,过大说明Survivor不够或对象年龄阈值太低。
  • Allocation Failure:Eden区满触发Young GC,正常现象。
  • Concurrent Mode Failure(CMS)/ Evacuation Failure(G1):并发GC来不及回收,退化为Full GC,需要调整触发阈值。
    工具:GCViewer(可视化)、GCEasy(在线分析,推荐)、JClarity Censum。

119. 🔵 什么是Java的容器化最佳实践?JVM在Docker/K8s中需要注意什么?

答:关键注意事项:

  1. 内存感知:JDK8u191+/JDK10+自动识别容器内存限制(-XX:+UseContainerSupport,默认开启)。老版本JDK看到的是宿主机内存,可能分配过大的堆导致OOM Killer。
  2. CPU感知:JDK自动识别容器CPU限制,影响GC线程数、ForkJoinPool并行度等。但CPU limit是CFS调度,可能导致CPU throttling。建议request=limit避免throttling。
  3. 堆大小设置:-XX:MaxRAMPercentage=75(堆占容器内存的75%,留25%给非堆、Native内存、OS)。不要用固定的-Xmx(不灵活)。
  4. 基础镜像:Eclipse Temurin(原AdoptOpenJDK)或Amazon Corretto。Alpine镜像用musl libc,可能有兼容性问题。
  5. 优雅停机:SIGTERM信号处理,Spring Boot的graceful shutdown(server.shutdown=graceful)。
  6. 健康检查:liveness用进程检查,readiness用/actuator/health(确保应用完全启动)。
  7. JFR/诊断:容器中可通过jcmd或Arthas诊断,需要确保工具可用。

120. ⚫ 请对比分析Java生态中主流的序列化框架(Protobuf、Kryo、Hessian、JSON、Avro、Thrift),从性能、体积、兼容性、易用性、Schema演进等维度进行全面评估,并给出不同场景下的选型建议。

答:全面对比:

维度 Protobuf Kryo Hessian JSON(Jackson) Avro Thrift
序列化速度 极快 中等
反序列化速度 极快 中等
体积 中等
跨语言 优秀 仅Java 多语言 优秀 优秀 优秀
Schema演进 优秀(字段编号) 中等 中等 优秀 优秀
可读性 差(二进制) 优秀
易用性 中等(需.proto) 简单 简单 简单 中等(需.avsc) 中等(需.thrift)

选型建议:

  • 内部Java微服务RPC:Kryo(性能最好)或Protobuf(跨语言预留)。
  • 跨语言RPC(gRPC):Protobuf(gRPC原生支持)。
  • 对外API:JSON(可读性好,调试方便)。
  • 大数据存储/传输:Avro(Hadoop生态原生支持,Schema演进好)。
  • 消息队列消息体:Protobuf(体积小,Schema演进好,消费者可以独立升级)。
  • Dubbo RPC:Hessian2(默认)或Protobuf。

七、Spring/Spring Boot 深度(121-140题)

121. 🔵 请详细解释Spring IOC容器的启动流程。从new AnnotationConfigApplicationContext()到Bean可用,中间经历了哪些关键步骤?

答:IOC容器启动是一个复杂的多阶段过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
核心流程(AbstractApplicationContext.refresh()):

1. prepareRefresh()
- 设置启动时间、活跃标志
- 初始化PropertySources

2. obtainFreshBeanFactory()
- 创建BeanFactory(DefaultListableBeanFactory)
- 加载BeanDefinition(从注解/XML/配置类)

3. prepareBeanFactory()
- 配置BeanFactory的标准特性
- 注册ClassLoader、环境变量、系统属性等

4. postProcessBeanFactory()
- 子类扩展点(如Web应用注册Scope)

5. invokeBeanFactoryPostProcessors() ★关键★
- 执行BeanFactoryPostProcessor
- ConfigurationClassPostProcessor在这里解析@Configuration、@ComponentScan、@Import、@Bean
- 这一步完成后所有BeanDefinition都已注册

6. registerBeanPostProcessors()
- 注册BeanPostProcessor(如AutowiredAnnotationBeanPostProcessor)
- 按PriorityOrdered → Ordered → 普通顺序注册

7. initMessageSource() / initApplicationEventMulticaster()
- 国际化和事件广播器初始化

8. onRefresh()
- 子类扩展(如SpringBoot创建内嵌Tomcat)

9. registerListeners()
- 注册ApplicationListener

10. finishBeanFactoryInitialization() ★关键★
- 实例化所有非懒加载的单例Bean
- Bean生命周期:实例化→属性注入→初始化

11. finishRefresh()
- 发布ContextRefreshedEvent
- 启动完成

Bean的完整生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1. 实例化(Instantiation)
- 通过构造器或工厂方法创建对象

2. 属性注入(Population)
- @Autowired、@Value等注入
- AutowiredAnnotationBeanPostProcessor处理

3. Aware接口回调
- BeanNameAware → BeanFactoryAware → ApplicationContextAware

4. BeanPostProcessor.postProcessBeforeInitialization()
- @PostConstruct在这里执行(CommonAnnotationBeanPostProcessor)

5. InitializingBean.afterPropertiesSet()

6. 自定义init-method

7. BeanPostProcessor.postProcessAfterInitialization()
- AOP代理在这里创建

8. Bean可用

9. 容器关闭时:
- @PreDestroy → DisposableBean.destroy() → 自定义destroy-method

122. 🔴 Spring AOP的实现原理是什么?JDK动态代理和CGLIB代理有什么区别?在什么情况下AOP会失效?

答:Spring AOP基于代理模式实现,在运行时为目标对象创建代理。

两种代理方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// JDK动态代理:基于接口
// 目标类必须实现接口
// 代理类实现相同接口,通过InvocationHandler转发调用
public class JdkProxy implements InvocationHandler {
private Object target;

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置增强
System.out.println("Before: " + method.getName());
Object result = method.invoke(target, args); // 调用目标方法
// 后置增强
System.out.println("After: " + method.getName());
return result;
}
}

// CGLIB代理:基于继承
// 不需要接口,通过生成目标类的子类实现
// 使用ASM字节码框架动态生成子类
// final类和final方法无法代理

选择策略:

  • 目标类实现了接口 → 默认JDK动态代理(Spring Boot 2.x+默认CGLIB)
  • 目标类没有接口 → CGLIB
  • 可通过@EnableAspectJAutoProxy(proxyTargetClass = true)强制使用CGLIB

AOP失效的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Service
public class OrderService {

@Transactional
public void createOrder(Order order) {
// 事务生效
}

public void batchCreate(List<Order> orders) {
for (Order order : orders) {
this.createOrder(order); // ★ AOP失效!★
// 因为this是原始对象,不是代理对象
// 内部调用不经过代理,@Transactional不生效
}
}
}

// 解决方案:
// 1. 注入自身
@Autowired
private OrderService self; // 注入的是代理对象
self.createOrder(order); // 通过代理调用,AOP生效

// 2. 从ApplicationContext获取
((OrderService) AopContext.currentProxy()).createOrder(order);
// 需要@EnableAspectJAutoProxy(exposeProxy = true)

// 3. 拆分到不同的类中

其他AOP失效场景:

  • private方法:代理无法拦截私有方法
  • static方法:代理基于对象实例,静态方法不经过代理
  • final方法(CGLIB):无法覆盖final方法
  • 非Spring管理的对象:new出来的对象没有代理

123. 🔴 Spring Boot的自动装配(Auto-Configuration)原理是什么?@EnableAutoConfiguration是如何工作的?

答:自动装配是Spring Boot的核心特性,让开发者无需手动配置大量Bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
自动装配流程:

1. @SpringBootApplication包含@EnableAutoConfiguration

2. @EnableAutoConfiguration通过@Import导入AutoConfigurationImportSelector

3. AutoConfigurationImportSelector.selectImports():
- 读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
(Spring Boot 2.7之前是META-INF/spring.factories)
- 获取所有自动配置类的全限定名(如DataSourceAutoConfiguration)

4. 过滤:
- @ConditionalOnClass:类路径中存在指定类才生效
- @ConditionalOnMissingBean:容器中没有指定Bean才生效
- @ConditionalOnProperty:配置属性满足条件才生效
- 大量自动配置类被过滤掉,只有满足条件的才加载

5. 排序:
- @AutoConfigureOrder:控制自动配置类的加载顺序
- @AutoConfigureBefore/@AutoConfigureAfter:指定相对顺序

自定义Starter的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1. 创建自动配置类
@Configuration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public MyService myService(MyProperties properties) {
return new MyService(properties.getUrl(), properties.getTimeout());
}
}

// 2. 配置属性类
@ConfigurationProperties(prefix = "my.service")
public class MyProperties {
private String url = "http://localhost:8080";
private int timeout = 3000;
// getter/setter
}

// 3. 注册自动配置
// META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.MyAutoConfiguration

// 4. 使用方只需引入依赖 + 配置属性
// application.yml
my:
service:
url: http://api.example.com
timeout: 5000

124. 🔴 Spring的循环依赖是如何解决的?三级缓存的作用分别是什么?

答:Spring通过三级缓存解决单例Bean的循环依赖问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
三级缓存:
- singletonObjects(一级缓存):完整的单例Bean
- earlySingletonObjects(二级缓存):提前暴露的Bean(可能是代理对象)
- singletonFactories(三级缓存):Bean的ObjectFactory(用于创建早期引用)

解决流程(A依赖B,B依赖A):

1. 创建A:实例化A → 将A的ObjectFactory放入三级缓存
2. 注入A的属性:发现依赖B → 去创建B
3. 创建B:实例化B → 将B的ObjectFactory放入三级缓存
4. 注入B的属性:发现依赖A → 从缓存中获取A
- 一级缓存没有A
- 二级缓存没有A
- 三级缓存有A的ObjectFactory → 调用getObject()获取A的早期引用
- 如果A需要AOP代理,这里返回代理对象
- 将A的早期引用放入二级缓存,删除三级缓存
5. B注入A的早期引用 → B初始化完成 → B放入一级缓存
6. 回到A的创建:注入B → A初始化完成 → A放入一级缓存

为什么需要三级缓存而不是两级:
- 如果没有AOP,两级缓存就够了
- 三级缓存的ObjectFactory可以在需要时才创建代理对象
- 保证代理对象只创建一次
- 如果直接放代理对象到二级缓存,那每个Bean都要提前创建代理,浪费资源

无法解决的循环依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 构造器注入的循环依赖无法解决
@Service
public class A {
public A(B b) {} // 构造器注入B
}

@Service
public class B {
public B(A a) {} // 构造器注入A
}
// 因为构造器注入时对象还没实例化,无法放入缓存

// 解决方案:
// 1. 改为Setter注入
// 2. 使用@Lazy
public A(@Lazy B b) {} // 注入B的代理,延迟初始化

125. 🔴 Spring事务的传播行为有哪些?REQUIRED和REQUIRES_NEW在嵌套调用时有什么区别?事务失效的常见原因有哪些?

答:事务传播行为定义了方法被另一个事务方法调用时的行为。

1
2
3
4
5
6
7
8
七种传播行为:
REQUIRED(默认):有事务就加入,没有就新建
REQUIRES_NEW:总是新建事务,挂起当前事务
NESTED:有事务就创建嵌套事务(Savepoint),没有就新建
SUPPORTS:有事务就加入,没有就非事务执行
NOT_SUPPORTED:非事务执行,挂起当前事务
MANDATORY:必须在事务中调用,否则抛异常
NEVER:必须非事务调用,否则抛异常

REQUIRED vs REQUIRES_NEW:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Service
public class OrderService {
@Transactional // REQUIRED
public void createOrder() {
orderMapper.insert(order);
logService.saveLog(log); // 调用日志服务
}
}

@Service
public class LogService {
// 场景1:REQUIRED
@Transactional(propagation = Propagation.REQUIRED)
public void saveLog(Log log) {
logMapper.insert(log);
// 如果这里抛异常,整个createOrder事务回滚
// 因为共用同一个事务
}

// 场景2:REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(Log log) {
logMapper.insert(log);
// 如果这里抛异常,只回滚日志保存
// createOrder的事务不受影响(需要catch异常)
// 反过来,createOrder回滚也不影响已提交的日志
}
}

事务失效的常见原因:

1
2
3
4
5
6
7
8
1. 自调用(最常见):同一个类中方法A调用方法B,B的@Transactional不生效
2. 方法不是public:Spring AOP只能代理public方法
3. 异常被catch了:事务默认只对RuntimeException回滚
4. 异常类型不对:checked异常默认不回滚
修复:@Transactional(rollbackFor = Exception.class)
5. 数据库不支持事务:如MyISAM引擎
6. Bean没有被Spring管理:new出来的对象没有代理
7. 多数据源未正确配置事务管理器

126. 🔴 Spring Boot的启动流程是怎样的?从main方法到应用就绪经历了哪些步骤?

答:Spring Boot的启动流程比传统Spring多了自动配置和内嵌容器的初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
SpringApplication.run(Application.class, args) 流程:

1. 创建SpringApplication实例:
- 推断应用类型(SERVLET/REACTIVE/NONE)
- 加载ApplicationContextInitializer
- 加载ApplicationListener
- 推断主类

2. 运行run()方法:
a. 创建并启动StopWatch(计时)
b. 获取SpringApplicationRunListeners
c. 发布ApplicationStartingEvent

d. 准备环境(Environment):
- 加载配置文件(application.yml/properties)
- 命令行参数、环境变量、系统属性
- Profile激活
- 发布ApplicationEnvironmentPreparedEvent

e. 创建ApplicationContext:
- SERVLET → AnnotationConfigServletWebServerApplicationContext
- REACTIVE → AnnotationConfigReactiveWebServerApplicationContext

f. 准备Context:
- 设置Environment
- 执行ApplicationContextInitializer
- 发布ApplicationContextInitializedEvent
- 注册主类的BeanDefinition
- 发布ApplicationPreparedEvent

g. 刷新Context(refresh()):★核心★
- 就是前面讲的IOC容器启动流程
- 自动配置在这里生效
- 内嵌Tomcat/Netty在onRefresh()中创建并启动
- 所有单例Bean在finishBeanFactoryInitialization()中实例化

h. 发布ApplicationStartedEvent

i. 执行Runner:
- ApplicationRunner
- CommandLineRunner

j. 发布ApplicationReadyEvent
- 应用就绪,可以接收请求

127. 🔵 Spring Boot的配置加载优先级是怎样的?如何实现配置的外部化?

答:Spring Boot支持多种配置来源,按优先级从高到低:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1. 命令行参数:--server.port=8081
2. SPRING_APPLICATION_JSON(环境变量中的JSON)
3. ServletConfig/ServletContext参数
4. JNDI属性
5. Java系统属性:System.getProperties()
6. 操作系统环境变量
7. RandomValuePropertySource(random.*)
8. jar包外的application-{profile}.yml
9. jar包内的application-{profile}.yml
10. jar包外的application.yml
11. jar包内的application.yml
12. @PropertySource注解
13. 默认属性(SpringApplication.setDefaultProperties)

配置文件加载位置(优先级从高到低):
1. file:./config/(项目根目录的config目录)
2. file:./(项目根目录)
3. classpath:/config/
4. classpath:/

外部化配置最佳实践:
- 开发环境:application-dev.yml(本地配置)
- 测试环境:配置中心或环境变量
- 生产环境:配置中心(Apollo/Nacos)+ K8s ConfigMap/Secret
- 敏感配置:Vault或加密存储,不放在配置文件中

128. 🔴 Spring Boot Actuator的原理是什么?如何自定义健康检查和指标?

答:Actuator提供了生产级的监控和管理端点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 自定义健康检查
@Component
public class DatabaseHealthIndicator implements HealthIndicator {

@Autowired
private DataSource dataSource;

@Override
public Health health() {
try (Connection conn = dataSource.getConnection()) {
if (conn.isValid(3)) {
return Health.up()
.withDetail("database", "MySQL")
.withDetail("pool.active", getActiveConnections())
.build();
}
} catch (SQLException e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
return Health.down().build();
}
}

// 自定义指标(Micrometer)
@Component
public class OrderMetrics {

private final Counter orderCounter;
private final Timer orderTimer;

public OrderMetrics(MeterRegistry registry) {
this.orderCounter = Counter.builder("order.created.total")
.description("Total orders created")
.tag("type", "normal")
.register(registry);

this.orderTimer = Timer.builder("order.create.duration")
.description("Order creation duration")
.register(registry);
}

public void recordOrderCreated() {
orderCounter.increment();
}

public <T> T timeOrderCreation(Supplier<T> supplier) {
return orderTimer.record(supplier);
}
}

// 配置暴露端点
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
metrics:
export:
prometheus:
enabled: true

129. 🔴 Spring的@Async异步机制原理是什么?有哪些常见的坑?

答:@Async通过AOP代理实现异步方法调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 开启异步支持
@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {

@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new CallerRunsPolicy());
executor.initialize();
return executor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("Async method {} threw exception: {}", method.getName(), ex.getMessage());
};
}
}

// 使用
@Service
public class NotificationService {
@Async
public CompletableFuture<Boolean> sendEmail(String to, String content) {
// 异步执行
boolean result = emailClient.send(to, content);
return CompletableFuture.completedFuture(result);
}
}

常见的坑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1. 自调用失效(和@Transactional一样):
同一个类中调用@Async方法,不会异步执行
原因:自调用不经过代理

2. 默认线程池问题:
不配置自定义线程池时,Spring使用SimpleAsyncTaskExecutor
该线程池每次创建新线程,不复用,可能导致线程爆炸
必须配置自定义线程池

3. 异常丢失:
@Async方法返回void时,异常默认被吞掉
解决:返回Future/CompletableFuture,或配置AsyncUncaughtExceptionHandler

4. 事务不传播:
@Async方法在新线程中执行,不继承调用方的事务
需要在@Async方法内自己开启事务

5. 上下文丢失:
ThreadLocal中的数据(如用户信息、TraceId)不会传播到异步线程
解决:使用TaskDecorator传播上下文
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// TaskDecorator传播上下文
public class ContextCopyingDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 在主线程中获取上下文
RequestAttributes context = RequestContextHolder.getRequestAttributes();
Map<String, String> mdcContext = MDC.getCopyOfContextMap();

return () -> {
try {
// 在异步线程中恢复上下文
RequestContextHolder.setRequestAttributes(context);
if (mdcContext != null) MDC.setContextMap(mdcContext);
runnable.run();
} finally {
RequestContextHolder.resetRequestAttributes();
MDC.clear();
}
};
}
}

130. 🔴 Spring的事件机制(ApplicationEvent)是如何实现的?同步还是异步?如何实现异步事件?

答:Spring事件机制基于观察者模式,默认同步执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 1. 定义事件
public class OrderCreatedEvent extends ApplicationEvent {
private final String orderId;

public OrderCreatedEvent(Object source, String orderId) {
super(source);
this.orderId = orderId;
}
}

// 2. 发布事件
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher publisher;

@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
publisher.publishEvent(new OrderCreatedEvent(this, order.getId()));
}
}

// 3. 监听事件
@Component
public class OrderEventListener {

@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 默认同步执行,在发布者的线程中运行
// 如果这里抛异常,会影响发布者的事务
log.info("Order created: {}", event.getOrderId());
}

@Async
@EventListener
public void onOrderCreatedAsync(OrderCreatedEvent event) {
// 异步执行,不影响发布者
notificationService.sendNotification(event.getOrderId());
}

// Spring 4.2+支持条件监听
@EventListener(condition = "#event.orderId.startsWith('VIP')")
public void onVipOrderCreated(OrderCreatedEvent event) {
// 只处理VIP订单
}

// 事务提交后执行(Spring 4.2+)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void afterOrderCommitted(OrderCreatedEvent event) {
// 保证事务提交成功后才执行
// 避免事务回滚但通知已发出的问题
}
}

实现原理:

1
2
3
4
5
6
7
8
9
ApplicationEventMulticaster负责事件分发:
- 默认实现:SimpleApplicationEventMulticaster
- 维护一个ListenerRetriever缓存,按事件类型索引监听器
- 发布事件时,找到所有匹配的监听器,逐个调用

同步 vs 异步:
- 默认同步:在publishEvent的调用线程中依次执行所有监听器
- 异步方式1:@Async + @EventListener
- 异步方式2:配置SimpleApplicationEventMulticaster的taskExecutor

131. 🔵 Spring Boot如何实现优雅停机?内嵌Tomcat的关闭流程是怎样的?

答:Spring Boot 2.3+原生支持优雅停机。

1
2
3
4
5
6
# 配置
server:
shutdown: graceful # 开启优雅停机
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 最大等待时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
关闭流程:
1. 收到SIGTERM信号(K8s发送)
2. 触发ContextClosedEvent
3. Web服务器停止接收新请求
4. 等待进行中的请求完成(最多30秒)
5. 关闭Web服务器
6. 销毁所有Bean(@PreDestroy、DisposableBean)
7. 关闭ApplicationContext
8. JVM退出

内嵌Tomcat的关闭细节:
- 停止Connector(不再接受新连接)
- 等待已有请求处理完成
- 关闭线程池
- 释放资源

注意事项:
- K8s的terminationGracePeriodSeconds要大于Spring的timeout
- preStop Hook中加sleep,等待Endpoints更新传播
- 长连接(WebSocket)需要特殊处理
- 定时任务需要在关闭时正确停止

132. 🔴 Spring的条件注解(@Conditional)体系是怎样的?如何自定义条件?

答:条件注解是Spring Boot自动配置的基础。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 内置条件注解:
@ConditionalOnClass(DataSource.class) // 类路径存在指定类
@ConditionalOnMissingClass("com.example.Foo") // 类路径不存在指定类
@ConditionalOnBean(DataSource.class) // 容器中存在指定Bean
@ConditionalOnMissingBean(DataSource.class) // 容器中不存在指定Bean
@ConditionalOnProperty(name = "my.feature.enabled", havingValue = "true")
@ConditionalOnResource(resources = "classpath:schema.sql")
@ConditionalOnWebApplication // Web应用环境
@ConditionalOnExpression("${my.flag:true}") // SpEL表达式

// 自定义条件
public class OnRedisAvailableCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
try {
String host = context.getEnvironment().getProperty("spring.redis.host", "localhost");
int port = Integer.parseInt(context.getEnvironment().getProperty("spring.redis.port", "6379"));
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(host, port), 1000);
return true; // Redis可连接
}
} catch (Exception e) {
return false; // Redis不可用
}
}
}

// 使用自定义条件
@Configuration
@Conditional(OnRedisAvailableCondition.class)
public class RedisCacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
return RedisCacheManager.builder(factory).build();
}
}

133. 🔴 Spring WebFlux和Spring MVC有什么区别?响应式编程在什么场景下有优势?

答:WebFlux是Spring 5引入的响应式Web框架,基于Reactor。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
核心区别:

| 维度 | Spring MVC | Spring WebFlux |
|------|-----------|----------------|
| 编程模型 | 命令式(阻塞) | 响应式(非阻塞) |
| 线程模型 | 一个请求一个线程 | 少量线程处理大量请求 |
| 服务器 | Tomcat/Jetty(Servlet) | Netty/Undertow(非Servlet) |
| 数据库 | JDBC(阻塞) | R2DBC(非阻塞) |
| 适用场景 | 传统CRUD应用 | 高并发IO密集型应用 |

WebFlux的优势场景:
1. 高并发网关/代理:大量请求转发,IO等待多
2. 流式数据处理:SSE、WebSocket
3. 微服务聚合层:并行调用多个下游服务
4. 对延迟敏感的场景:非阻塞避免线程切换开销

WebFlux不适合的场景:
1. CPU密集型计算:非阻塞无优势
2. 依赖阻塞API(JDBC、同步HTTP客户端):会阻塞EventLoop
3. 团队不熟悉响应式编程:学习曲线陡峭,调试困难

134. 🔵 Spring Boot的嵌入式容器(Tomcat/Jetty/Undertow)如何选择和调优?

答:三种嵌入式容器各有特点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
对比:
| 维度 | Tomcat | Jetty | Undertow |
|------|--------|-------|----------|
| 默认 | Spring Boot默认 | 需要排除Tomcat | 需要排除Tomcat |
| 性能 | 好 | 好 | 最好(NIO) |
| 内存 | 中等 | 较低 | 最低 |
| 成熟度 | 最成熟 | 成熟 | 较新 |
| WebSocket | 支持 | 支持 | 支持 |
| HTTP/2 | 支持 | 支持 | 支持 |

Tomcat调优关键参数:
server:
tomcat:
threads:
max: 200 # 最大工作线程数
min-spare: 20 # 最小空闲线程数
max-connections: 10000 # 最大连接数
accept-count: 100 # 等待队列长度
connection-timeout: 20000 # 连接超时(ms)

调优建议:
- max-threads:CPU密集型设为CPU核数×2,IO密集型可以更大
- max-connections:根据预期并发连接数设置
- accept-count:max-connections满后的等待队列
- 监控线程池使用率,动态调整

135. 🔴 如何设计一个Spring Boot Starter?有哪些设计原则?

答:Starter是Spring Boot生态的核心扩展机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Starter的组成:
1. autoconfigure模块:自动配置逻辑
2. starter模块:依赖聚合(pom中引入autoconfigure和其他依赖)

设计原则:

1. 条件化配置:
- 所有Bean都用@ConditionalOnXxx保护
- 用户可以自定义Bean覆盖默认配置
- @ConditionalOnMissingBean是最常用的

2. 配置属性:
- 使用@ConfigurationProperties绑定配置
- 提供合理的默认值
- 配置项命名规范:my-starter.xxx

3. 自动配置元数据:
- spring-configuration-metadata.json提供IDE提示
- 使用spring-boot-configuration-processor自动生成

4. 最小依赖:
- 只引入必要的依赖
- 可选依赖用optional标记

5. 失败友好:
- 缺少依赖时优雅降级,不阻止应用启动
- 配置错误时给出清晰的错误信息

命名规范:
- 官方Starter:spring-boot-starter-xxx
- 第三方Starter:xxx-spring-boot-starter

136. 🔴 Spring Security的认证和授权流程是怎样的?Filter Chain是如何工作的?

答:Spring Security基于Servlet Filter链实现安全控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Filter Chain核心过滤器(按顺序):

1. SecurityContextPersistenceFilter
- 从Session中恢复SecurityContext
- 请求结束后保存SecurityContext

2. UsernamePasswordAuthenticationFilter
- 处理表单登录(/login POST)
- 提取用户名密码,创建UsernamePasswordAuthenticationToken
- 委托给AuthenticationManager认证

3. BasicAuthenticationFilter
- 处理HTTP Basic认证

4. BearerTokenAuthenticationFilter
- 处理JWT/OAuth2 Bearer Token

5. ExceptionTranslationFilter
- 捕获认证/授权异常
- 未认证 → 重定向到登录页
- 未授权 → 返回403

6. FilterSecurityInterceptor
- 最后一个过滤器
- 根据配置的权限规则进行授权检查

认证流程:
请求 → Filter → AuthenticationManager → AuthenticationProvider
→ UserDetailsService.loadUserByUsername()
→ 密码比对(PasswordEncoder)
→ 认证成功 → 创建Authentication对象 → 存入SecurityContext

授权流程:
请求 → FilterSecurityInterceptor → AccessDecisionManager
→ AccessDecisionVoter投票
→ 根据投票结果决定是否允许访问

137. 🔵 Spring中的设计模式有哪些?请举例说明。

答:Spring框架大量使用了经典设计模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
1. 工厂模式:BeanFactory/ApplicationContext
- 根据配置创建Bean实例

2. 单例模式:Bean默认是单例
- DefaultSingletonBeanRegistry管理单例缓存

3. 代理模式:AOP
- JDK动态代理、CGLIB代理

4. 模板方法模式:JdbcTemplate、RestTemplate
- 定义算法骨架,子类实现具体步骤

5. 观察者模式:ApplicationEvent/ApplicationListener
- 事件发布和监听

6. 适配器模式:HandlerAdapter
- 将不同类型的Handler适配为统一接口

7. 策略模式:Resource接口
- ClassPathResource、FileSystemResource、UrlResource

8. 责任链模式:Filter Chain、Interceptor Chain
- 请求依次经过多个处理器

9. 装饰器模式:BeanWrapper
- 为Bean添加额外功能

10. 委托模式:DispatcherServlet
- 将请求委托给具体的Handler处理

138. 🔴 Spring Boot应用的性能优化有哪些方向?

答:Spring Boot性能优化需要从启动速度和运行时性能两个维度考虑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
启动速度优化:
1. 延迟初始化:spring.main.lazy-initialization=true
- 所有Bean延迟到首次使用时创建
- 缺点:第一次请求会慢

2. 减少组件扫描范围:
- 精确指定@ComponentScan的basePackages
- 避免扫描不必要的包

3. 排除不需要的自动配置:
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
SecurityAutoConfiguration.class
})

4. 使用Spring AOT(Ahead-of-Time)编译:
- Spring 6/Spring Boot 3支持
- 编译时生成Bean定义,减少运行时反射

5. GraalVM Native Image:
- 编译为原生可执行文件
- 启动时间从秒级降到毫秒级
- 内存占用大幅减少

运行时性能优化:
1. 连接池调优(HikariCP)
2. 缓存合理使用(@Cacheable)
3. 异步处理(@Async、WebFlux)
4. JVM参数调优(GC选择、堆大小)
5. 数据库查询优化
6. 序列化优化(Jackson配置)

139. 🔴 Spring Cloud和Spring Cloud Alibaba的核心组件有哪些?如何选型?

答:两套微服务技术栈各有侧重。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 功能 | Spring Cloud Netflix(已停更) | Spring Cloud Alibaba | Spring Cloud原生 |
|------|---------------------------|---------------------|-----------------|
| 注册中心 | Eureka | Nacos | Consul/Zookeeper |
| 配置中心 | Spring Cloud Config | Nacos | Consul |
| 网关 | Zuul | - | Spring Cloud Gateway |
| 负载均衡 | Ribbon | - | Spring Cloud LoadBalancer |
| 熔断降级 | Hystrix | Sentinel | Resilience4j |
| 分布式事务 | - | Seata | - |
| 消息 | - | RocketMQ | Spring Cloud Stream |
| 链路追踪 | Sleuth+Zipkin | - | Micrometer Tracing |

选型建议:
- 国内团队 + 阿里云 → Spring Cloud Alibaba(Nacos + Sentinel + Seata)
- 国际化团队 → Spring Cloud原生(Consul + Resilience4j + Gateway)
- 新项目建议:Spring Cloud Gateway + Nacos + Sentinel + Seata

140. ⚫ 如果让你设计一个Spring Boot应用的脚手架(Archetype),你会包含哪些内容?

答:一个好的脚手架能统一团队规范,提高开发效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
脚手架内容:

1. 项目结构:
├── src/main/java
│ ├── controller/ # API层
│ ├── service/ # 业务层
│ ├── repository/ # 数据层
│ ├── model/ # 领域模型
│ │ ├── entity/ # 数据库实体
│ │ ├── dto/ # 数据传输对象
│ │ └── vo/ # 视图对象
│ ├── config/ # 配置类
│ ├── common/ # 公共组件
│ │ ├── exception/ # 全局异常处理
│ │ ├── response/ # 统一响应封装
│ │ └── util/ # 工具类
│ └── infrastructure/ # 基础设施
├── src/main/resources
│ ├── application.yml
│ ├── application-dev.yml
│ └── application-prod.yml
└── src/test/java

2. 内置功能:
- 统一响应格式(Result<T>)
- 全局异常处理(@ControllerAdvice)
- 参数校验(@Valid + 自定义校验器)
- 日志规范(Logback + TraceId)
- Swagger/OpenAPI文档
- 健康检查端点
- Prometheus指标暴露
- Docker + K8s部署文件
- CI/CD Pipeline模板
- 单元测试模板

3. 规范约束:
- Checkstyle代码规范
- SpotBugs静态分析
- JaCoCo覆盖率要求
- Git Hooks(提交前检查)