面灵AI→

星龙数智 Java 后端面经复盘:从 JVM 到 RAG 落地

时间
2026-05
来源
牛客网

《面试题目》

  1. 请做一下自我介绍。
  2. 大二就找实习,目前的学业阶段是怎样的?
  3. 未来想从事 Java 开发还是大模型开发?
  4. 说说 Java 的类加载过程。
  5. 讲讲双亲委派模型。
  6. JVM 内存区域是如何划分的?
  7. 介绍一下标记清除、标记整理、复制这几种垃圾回收算法。
  8. 解释面向对象的封装、继承、多态三大特征。
  9. Java 类为什么是单继承?
  10. 方法重载和重写的区别是什么?
  11. 运行时异常和编译时异常分别是什么?
  12. 用过哪些 Java 常见异常,空指针和 OOM 分别属于哪类异常?
  13. 除了 != null 还有哪些判空方式,字符串如何判空?
  14. 线程池有哪些参数?描述任务提交后的执行流程,线程池总共有几个参数?
  15. 说说你对 ThreadLocal 的理解,多线程和分布式场景下如何实现数据传递?
  16. 解释缓存穿透、缓存雪崩、缓存击穿的概念以及对应的解决方案。
  17. 应用多实例部署时,本地缓存如何实现缓存同步?
  18. 介绍一下 RAG 项目中文档处理的流程。
  19. 如何解决 RAG 中文档质量差、召回率低的问题?
  20. 项目使用的向量数据库和大模型分别是什么?
  21. 反问:AI 可以给工业制造业业务带来哪些价值?
  22. 反问:未来是否偏向 AI 开发,是否可以接受 Java 后端开发?

《参考解析》

类加载过程与双亲委派

类加载分加载、验证、准备、解析、初始化五步。加载阶段由类加载器读字节码、生成 Class 对象;验证保证字节码合法;准备给静态变量分配内存并设零值(final 常量在这一步就赋真值);解析把符号引用换成直接引用;初始化才真正执行 <clinit>,也就是静态变量的赋值和静态代码块。双亲委派是加载时的委托规则:收到加载请求先委派给父加载器,父加载器加载不了才自己加载。它保证核心类库不被自定义同名类替换(比如自己写一个 java.lang.String 不会被加载),也避免同一个类被多个加载器重复加载。打破它的场景有三个:SPI 用线程上下文类加载器反向委托、Tomcat 每个 webapp 独立加载器实现隔离、OSGi 的网状委派。

JVM 内存区域

线程私有的有程序计数器、虚拟机栈、本地方法栈;线程共享的是堆和方法区(JDK 8 之后由元空间实现,放在本地内存)。程序计数器记录当前线程执行的字节码行号,是唯一不会 OOM 的区域。虚拟机栈里放栈帧,每个方法调用压一个栈帧,栈帧里有局部变量表、操作数栈、动态链接和返回地址;递归太深会抛 StackOverflowError,栈也允许动态扩展,扩不动了抛 OOM。堆是对象分配的主要场所,按分代划分新生代(Eden + 两个 Survivor)和老年代。方法区存类的元信息、运行时常量池、静态变量,元空间默认不设上限,加载的类太多会吃满本地内存。

三种 GC 算法

标记清除先标记存活对象再清除未标记的,实现简单,但会产生大量内存碎片,而且标记和清除效率都不高。标记整理在标记之后把存活对象往一端挪,腾出连续空间,没有碎片,代价是移动对象要更新引用,停顿更长,适合老年代。复制算法把内存分成两块,每次只用一块,回收时把存活对象复制到另一块再整体清空,没有碎片、分配也快,代价是可用内存减半;实际实现里不用 1:1,而是 Eden 加两块小 Survivor 的 8:1:1,配合对象年龄计数,熬过若干次 Minor GC 的对象晋升到老年代。

单继承、重载与重写

Java 类只能单继承,主要是为了避开 C++ 的多继承菱形问题:两个父类有同名方法时,子类调用到底走哪一个无法确定,而且虚函数表的布局会变复杂。要复用多个实现就走接口,接口只有方法签名没有状态,冲突时编译器能明确报错,由开发者显式指定。重载是同一个类里方法名相同、参数列表不同,编译期就根据静态类型确定调用哪一个;重写是子类覆盖父类的实例方法,运行期根据实际对象类型动态分派,约束是方法签名相同、返回值兼容、访问权限不能收紧、抛出的受检异常不能变宽。

异常体系与判空

Throwable 下面分 Error 和 Exception。Error 是 JVM 层面的严重问题,不该被业务捕获;Exception 又分受检异常和运行时异常——受检异常(IOException、SQLException 这类)编译器强制处理,要么 try-catch 要么往上抛;运行时异常(NullPointerException、IndexOutOfBoundsException、ClassCastException)编译期不检查。OOM 是 Error(OutOfMemoryError),空指针是运行时异常。判空除了 != null,还可以用 Objects.requireNonNull 做参数校验、Optional 做链式取值、工具类的 StringUtils.isBlank 兼顾 null 和空白字符。字符串判空要注意 isEmpty 只判长度、不判 null,isBlank 才把空格、制表符也算进去。

线程池参数与提交流程

ThreadPoolExecutor 的构造参数一共七个:corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。提交流程是:当前线程数小于核心数就直接新建核心线程执行;核心线程满了就尝试入队;队列满了再看线程数是否小于最大线程数,是就新建非核心线程;还满就交给拒绝策略(AbortPolicy 抛异常、CallerRunsPolicy 让提交线程自己跑、DiscardPolicy 直接丢、DiscardOldestPolicy 丢最老的)。这七个之外的允许核心线程超时回收是 setAllowCoreThreadTimeOut,不算构造参数。生产上要避免用 Executors 的快捷方法:newFixedThreadPool 和 newSingleThreadExecutor 用无界队列,会把内存堆爆;newCachedThreadPool 的最大线程数是 Integer.MAX_VALUE,会无限建线程。

ThreadLocal 与跨线程传递

ThreadLocal 让每个线程持有自己的变量副本,底层是 Thread 对象里挂一个 ThreadLocalMap,key 是 ThreadLocal 的弱引用,value 是强引用。因为 key 弱引用、value 强引用,ThreadLocal 对象被回收后 value 还在,线程池里线程长期存活就会造成内存泄漏,所以用完必须 remove。跨线程传递时子线程拿不到父线程的值,可以用 InheritableThreadLocal,但它只在创建线程那一刻拷贝,线程池里线程复用就失效了;线程池场景要用阿里的 TransmittableThreadLocal,在任务提交和执行的包装里做值的捕获与回放。分布式场景下 ThreadLocal 帮不上忙,得靠请求头携带 traceId 并在链路里透传。

缓存三大问题

穿透是查一个数据库里也不存在的 key,缓存永远不命中,请求全打到库里——缓存空值并设短过期时间,或者用布隆过滤器把不存在的 key 挡在前面。击穿是某个热点 key 恰好在过期瞬间被大量并发请求,全都去回源——热点 key 不设过期时间靠定时刷新,或者用互斥锁、单飞让一个请求去加载、其他请求等待。雪崩是大量 key 在同一时刻集中失效,或者缓存服务整个挂掉——过期时间加随机抖动打散,多级缓存兜底,Redis 侧用集群和哨兵保证高可用,业务侧加限流和降级。

多实例下的本地缓存同步

本地缓存在进程内,多实例之间天然不一致,常见解法按实时性从弱到强排:设很短的 TTL 让它自然过期,简单但有一段时间的脏读;用 Redis 的 pub/sub 或 Stream 广播失效消息,各实例收到后删掉本地 key,实时性好、成本低,但消息丢了就漏;用消息队列(Kafka、RocketMQ)广播,可靠但重;也可以让实例把变更写进一条带版本的记录,本地缓存每次读时比对版本号。要注意广播风暴和启动时的惊群——实例刚起来时别一次性把所有 key 都拉回来。

RAG 文档处理与召回优化

文档处理是一条流水线:解析(PDF、Word、HTML 各自有解析器,PDF 要处理双栏和表格)、清洗(去页眉页脚、目录、乱码)、分块、向量化、入库。分块是最影响效果的一步:纯按固定长度切会把一句话、一张表切开,常见做法是按语义或标题层级切,块之间留重叠(overlap)保住上下文,表格和代码块单独成块并保留表头。召回率低的排查顺序是先把原始问题改写(查询重写、同义扩展、指代消解),再上混合检索——向量检索擅长语义、关键词检索(BM25)擅长专有名词和编号,两路召回后用 RRF 或加权融合;然后加一层精排模型(cross-encoder 或 bge-reranker)对候选重排。文档质量差的话,问题和检索无关,得回到解析和清洗:把扫描件做 OCR、把表格转成结构化文本、删掉模板化的重复内容,并在入库时给每个块补上标题、来源、时间这些元数据,方便做过滤。

向量数据库与大模型选型

这一类问题答「选了什么」之外,更要说清「为什么」和「代价」。向量库常见的几类:Milvus 适合数据量大、要独立部署和水平扩展;Qdrant、Weaviate 开箱体验好、过滤能力强;pgvector 适合数据量不大、又不想额外维护一套存储的场景;FAISS 是库不是服务,适合离线或本地检索。选型要看数据规模、要不要按元数据过滤、要不要混合检索、以及团队能不能养得起一套独立服务。大模型侧则要区分「生成」和「向量化」两条线:向量化通常用小而专的 embedding 模型,生成侧再按质量、延迟、成本三者权衡,并且要留好降级链路。