星龙数智 Java 后端面经复盘:从 JVM 到 RAG 落地
- 时间
- 2026-05
- 来源
- 牛客网
《面试题目》
- 请做一下自我介绍。
- 大二就找实习,目前的学业阶段是怎样的?
- 未来想从事 Java 开发还是大模型开发?
- 说说 Java 的类加载过程。
- 讲讲双亲委派模型。
- JVM 内存区域是如何划分的?
- 介绍一下标记清除、标记整理、复制这几种垃圾回收算法。
- 解释面向对象的封装、继承、多态三大特征。
- Java 类为什么是单继承?
- 方法重载和重写的区别是什么?
- 运行时异常和编译时异常分别是什么?
- 用过哪些 Java 常见异常,空指针和 OOM 分别属于哪类异常?
- 除了
!= null还有哪些判空方式,字符串如何判空? - 线程池有哪些参数?描述任务提交后的执行流程,线程池总共有几个参数?
- 说说你对 ThreadLocal 的理解,多线程和分布式场景下如何实现数据传递?
- 解释缓存穿透、缓存雪崩、缓存击穿的概念以及对应的解决方案。
- 应用多实例部署时,本地缓存如何实现缓存同步?
- 介绍一下 RAG 项目中文档处理的流程。
- 如何解决 RAG 中文档质量差、召回率低的问题?
- 项目使用的向量数据库和大模型分别是什么?
- 反问:AI 可以给工业制造业业务带来哪些价值?
- 反问:未来是否偏向 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 模型,生成侧再按质量、延迟、成本三者权衡,并且要留好降级链路。