快手电商一面 — 19 道面试题总结
✅ 回答要点
1. Java 泛型的类型擦除
2. JVM 对象从 new 到 GC 回收的全过程
对象创建流程:
- 类加载检查 → 2. 分配内存 → 3. 初始化零值 → 4. 设置对象头 → 5. 执行
<init>方法
内存分配:
- TLAB(Thread Local Allocation Buffer):每个线程在 Eden 区预分配一块私有的内存缓冲区,避免多线程竞争。对象优先在 TLAB 中分配,TLAB 空间不足时在 Eden 区加锁分配。
- 大对象直接进入老年代:通过
-XX:PretenureSizeThreshold参数控制,超过该大小的对象直接在老年代分配,避免在 Eden 和 Survivor 之间频繁复制。
对象生命周期:
- 新生代(Eden → S0 / S1)→ 年龄增长 → 进入老年代 → 老年代满 → Full GC/Major GC → 不可达对象被回收
GC 回收判定:
- 可达性分析(GC Roots Tracing)→ 不可达对象标记 → 第一次标记(finalize)→ 第二次标记 → 回收
- 可达性分析:从一组称为 GC-Roots详解 的根对象出发,沿着引用链(Reference Chain)向下遍历,遍历经过的对象标记为 “可达”(reachable),没有被遍历到的对象就是”不可达”(unreachable),也就是”垃圾候选人”
- 不可达对象标记:第一次不可达只是标记,不会立即回收
- 第一次标记-finalize机会:JVM 会检查这个不可达对象 是否覆盖了
finalize()方法(且未被 JVM 调用过)- 如果没覆盖
finalize()→ 直接跳过此步,进入第 5 步回收 - 如果覆盖了
finalize()→ 将该对象放入 F-Queue(一个低优先级队列)- JVM 启动一个 Finalizer 线程(优先级很低),逐个执行 F-Queue 中对象的
finalize()方法。在finalize()方法中,对象可能重新与 GC Roots 链上的对象建立引用关系。但是实际生产中几乎不用。
- JVM 启动一个 Finalizer 线程(优先级很低),逐个执行 F-Queue 中对象的
- 如果没覆盖
- 第二次标记:
finalize()执行完成后,JVM 对 F-Queue 中的对象进行 第二次标记- 如果对象在
finalize()中 自救成功(重新可达)→ 移出回收队列,对象存活 - 如果对象仍然 不可达 → 正式判定死亡,进入下一步回收
- 如果对象在
开始
│
▼
┌──────────────┐
│ GC Roots Tracing │ ← 从根出发遍历对象图
└──────┬───────┘
│
▼
不可达对象标记 ──────→ 没覆盖 finalize() ────→ 直接回收
│
▼
放入 F-Queue
执行 finalize() ← 对象最后自救机会
│
▼
第二次标记 ──────→ 自救成功(重新可达)──→ 存活(移出队列)
│ ↑
▼ │
正式判定死亡 ──────→ 自救失败 ─────┘
│
▼
回收
3. GC Roots 有哪些?
GC Roots 包括(以 HotSpot 为例):
- 虚拟机栈(栈帧中的局部变量表)引用的对象:当前正在执行的方法中的局部变量
- 方法区中静态属性引用的对象:类的静态成员变量
- 方法区中常量引用的对象:字符串常量池中的引用、final 常量
- 本地方法栈中 JNI 引用的对象:Native 方法引用的对象
- Java 虚拟机内部的引用:系统类加载器、基本类型 Class 对象、常驻异常对象等
- 所有被 synchronized 持有的对象
- JVM 内部的 JMXBean、JVMTI 回调等
局部变量为什么能作为 GC Root?
- 局部变量存储在虚拟机栈的栈帧中,栈帧是当前活跃线程的方法调用帧。只要方法正在执行,局部变量就引用了堆中的对象,这个对象必须存活,不能被回收。GC 从栈帧中的局部变量开始遍历对象图,所有可达对象都需要保留。
静态变量什么时候被移除?
- 静态变量本身属于类元数据,存储在方法区(元空间),它们引用堆中的对象。当类被卸载时,静态变量引用的对象就不再是 GC Root。类卸载发生在 Metaspace 空间不足需要回收时,条件:
- 该类所有的实例都已被回收
- 该类的 ClassLoader 已被回收
- 该类对应的
java.lang.Class对象没有任何地方被引用
4. ReadWriteLock 使用场景与锁降级
详见已有知识卡片:Java读写锁-ReadWriteLock
使用场景: 读多写少的场景(如缓存、配置中心)。 默认锁: 非公平锁(性能优于公平锁)。 锁降级: 指写锁降级为读锁 — 持有写锁时获取读锁,然后释放写锁,最终只持有读锁。
5. synchronized 锁对象分析
详见已有知识卡片:synchronized机制详解
- 修饰静态方法:锁的是当前类的
Class对象 - 修饰实例方法:锁的是当前实例对象
this - 两者能同时执行吗? 不能。因为静态方法锁 Class 对象,实例方法锁实例对象,锁的对象不同,可以同时执行(互不阻塞)
6. wait/notify 机制
wait() 后线程什么时候被唤醒?
- 被
notify()随机唤醒一个等待线程 - 被
notifyAll()唤醒所有等待线程 - 被
interrupt()中断并抛出InterruptedException - 虚假唤醒(Spurious Wakeup):即使没有 notify,线程也可能被唤醒,所以 wait 必须在循环中调用
notify() vs notifyAll():
notify():随机唤醒一个等待线程,可能导致”信号丢失”(唤醒的线程不满足条件又重新 wait)notifyAll():唤醒所有等待线程,更安全但可能有性能开销- 一般建议用
notifyAll()除非你非常确定唤醒一个就够
唤醒后需要重新获取 monitor 吗?
- 需要。线程被唤醒后会从 Waiting 状态进入 Blocked 状态,等待获取 monitor 锁。获取到锁后才能从
wait()返回继续执行。
7. TCP 三次握手与可靠性
SYN-ACK 丢了客户端会怎样?
- 客户端一直收不到 SYN-ACK,会重传 SYN。重传超时时间(RTO)指数退避,默认重试次数由
tcp_syn_retries控制(默认 6 次,约 127 秒)。
SYN Flood 防御:
- SYN Cookie:不分配连接资源,直接计算一个 Cookie 作为初始序列号发给客户端
- SYN Cache:使用有限散列表缓存半连接,超过就丢弃
- 缩短 SYN Timeout:减少半连接等待时间
- tcp_max_syn_backlog:限制半连接队列大小
- 反向探测(如 RST 验证)
TIME_WAIT 等待 2MSL 的原因:
- 保证最后一个 ACK 能到达服务端:如果 ACK 丢了,服务端重传 FIN,客户端需要等待并重新发送 ACK
- 防止旧连接数据包影响新连接:2MSL 时间足够网络中所有数据包消失
- MSL(Maximum Segment Lifetime):报文最大生存时间,Linux 默认 30s/60s
8. MySQL 两阶段提交
详见已有知识卡片:Redo日志详解、MySQL Binlog 日志配置
两阶段提交流程:
- Prepare 阶段:事务写入 redo log,状态设为 prepare
- Commit 阶段:写入 binlog,然后提交事务,将 redo log 状态改为 commit
prepare 后崩溃:
- 若 prepare 后、binlog 写入前崩溃 → 回滚事务
- 若 binlog 已写入、但 redo log 未标记 commit 时崩溃 → 恢复时检查 binlog 和 redo log 的一致性,若 binlog 完整则重新提交
- 关键: 以 binlog 是否写成功作为事务是否提交的依据
9. MySQL 深度分页优化
详见已有知识卡片:MySQL索引创建原则、MySQL联合索引
LIMIT 1000000,10 为什么慢?
- 需要扫描前 1000010 行数据然后丢弃前 1000000 行
- InnoDB 按聚簇索引顺序扫描,即使有覆盖索引也要回表查
优化方案:
- 子查询优化(延迟关联):利用覆盖索引先找到 id,再关联原表
- 范围查询代替分页:
WHERE id > 1000000 LIMIT 10(不支持跳页) - 基于游标的分页:传上次的最后 ID (
WHERE id > last_id LIMIT 10) - 强制跳页优化:预计算偏移量映射表(定时任务更新偏移量到主键 ID 的映射)
10. Redis BigKey
BigKey 定义: 单个 key 的 value 过大(String 类型 > 10KB,集合类型元素 > 5000 个/Len > 10000)
导致的问题:
- 操作阻塞:Redis 单线程模型下,BigKey 的读写操作耗时过长
- 网络延迟:传输大对象占用带宽
- 内存倾斜:集群模式下某个分片内存使用远高于其他分片
- 慢查询:
DEL、GET等操作成为慢查询 - 数据迁移慢:集群扩容/缩容时需要迁移大量数据
删除时为什么会阻塞?
- 对于集合类型(List/Hash/Set/SortedSet),
DEL命令需要遍历所有元素释放内存,时间复杂度 O(N)。如果元素数量巨大,释放过程会长时间占用主线程。 - 优化方案:使用
UNLINK命令(异步删除)或者分批删除(SSCAN/HSCAN+SREM/HDEL)
11. OOM 处理
详见已有知识卡片:JVM-OOM排查指南
Heap Space vs Metaspace:
- Heap Space:存放 Java 对象实例,OOM 时通常是因为对象泄漏或堆太小
- Metaspace(JDK8+):存放类元数据(Class 信息、方法数据等),默认无上限(受本地内存限制),类加载过多会导致 Metaspace OOM
MAT 分析 heap dump 步骤:
- Dominator Tree:找出最大的对象/对象集合
- Leak Suspects:MAT 自动分析泄漏嫌疑
- GC Root Path:找到 GC Root 到泄漏对象的引用链,分析为什么未被回收
- Accumulated Objects:按 Retained Size 排序,找出内存大户
12. 线程死锁定位与避免
快速定位(jstack):
jstack <pid>打印线程堆栈- 搜索
Found one Java-level deadlock直接定位死锁 - 查看死锁线程的堆栈,看哪些锁在等待
- 也可以使用
jconsole或VisualVM的图形界面检测(自动检测死锁)
线上恢复:
- kill -3
触发线程 dump(不停止进程) - 分析 dump 文件找出死锁
- 重启服务(最直接但暴力),如果无法重启则尝试 kill 死锁线程(危险,可能导致数据不一致)
代码层面避免:
- 固定锁顺序:所有线程按统一顺序获取锁
- 使用显式锁的 tryLock 超时机制:
lock.tryLock(timeout, TimeUnit.SECONDS) - 减少锁粒度:使用
ConcurrentHashMap、读写分离等 - 使用并发工具类:如
CountDownLatch、CyclicBarrier等
13. SQL 突然变慢排查
详见已有知识卡片:接口性能排查指南
可能原因:
- 执行计划改变:旧索引被删除、统计信息过期导致优化器选择不同索引
- 锁等待:行锁/表锁升级,被其他事务阻塞
- Buffer Pool 淘汰:热点数据被刷出,需要大量磁盘 IO 重新加载
- 资源竞争:其他查询占用了 IO/CPU
- 数据分布变化:虽然数据量没变,但数据分布发生变化导致索引选择性下降
排查流程:
EXPLAIN ANALYZE查看实际执行计划SHOW PROCESSLIST查看是否有阻塞- 检查慢查询日志(Slow Query Log)
- 分析 Buffer Pool 命中率
- 检查索引统计信息是否更新
14. Spring @Transactional 失效场景
- 同一个类中方法自调用:
this.method()不走代理,AOP 拦截失效 - 私有/静态方法:Spring AOP 默认基于 JDK Proxy/CGLIB,只能拦截 public 方法
- 异常类型不匹配:默认只回滚
RuntimeException和Error,checked Exception不回滚 - 异常被捕获:try-catch 内部吞掉异常不抛出
- 数据库引擎不支持事务:如 MyISAM
- 传播行为配置不当:如
REQUIRES_NEW内层异常不会导致外层回滚 - 多数据源事务:默认的
@Transactional只支持单数据源,跨数据源需要分布式事务 - final 方法被重写:CGLIB 无法重写 final 方法
15. 大数据量导出 Excel 异步方案
异步导出方案设计:
-
分层架构:
- 请求层:接收导出请求,生成任务 ID,立即返回
- 任务管理层:使用线程池/消息队列异步处理
- 数据处理层:分批查询数据库,流式处理
- 文件生成层:使用 SXSSFWorkbook(Apache POI)流式写入,控制内存
- 结果持久化层:生成文件后上传 OSS/本地存储,提供下载链接
-
分批读取(大批量数据):
- 游标查询(Cursor):MyBatis Cursor / JDBC ResultSet 流式读取,不一次性加载到内存
- 分页查询:基于主键 ID 分段(
WHERE id > ? LIMIT 10000) - MySql Streaming ResultSet:
statement.setFetchSize(Integer.MIN_VALUE) - 数据量监控:控制每批次大小(通常 5000-10000 条)
-
进度通知:
- 前端轮询/WebSocket 通知任务进度
- 导出完成后发通知(邮件/钉钉/短信)
16. 接口幂等方案
详见已有知识卡片:接口幂等方案设计
17. AI Coding
AI 幻觉举例: 虚构不存在的 API、方法名、参数,依赖版本与实际不一致等。
直播电商中 AI 场景:
- 商品审核(图片/文本违规检测)
- 实时弹幕审核(敏感词+语义)
- 智能推荐(商品/直播卡片)
- 虚拟主播/智能客服
低延迟策略:
- 本地部署轻量模型:小模型(蒸馏/量化)在端侧推理
- 多级模型级联:简单规则 + 轻量模型过滤大部分请求,仅复杂场景走大模型
- 异步回查:低风险判断走快速通道走,后续异步回查修正
- 缓存/预判:高频内容提前缓存,相似请求复用结果
18. 【算法】无重复字符的最长子串
滑动窗口(双指针)解法:
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> map = new HashMap<>();
int maxLen = 0, left = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (map.containsKey(c)) {
left = Math.max(left, map.get(c) + 1);
}
map.put(c, right);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}复杂度: O(n) 时间,O(min(m,n)) 空间(m 为字符集大小) 核心思想: 维护一个不重复的滑动窗口,right 指针扩展窗口,left 指针收缩窗口,用 HashMap 记录每个字符最近出现的位置。
19. 【场景题】直播弹幕实时审核系统
分层架构设计:
客户端 → 接入层(Nginx/LB) → 缓存层(Redis) → 业务层 → 审核管道 → 存储层
管道分层(审核管道,分阶段并行):
- L0 — 前置过滤层:短时间相同弹幕去重(本地布隆过滤器/分布式计数)
- L1 — 规则引擎层:敏感词匹配(AC 自动机/Trie 树)、正则规则、黑白名单。要求 < 5ms
- L2 — 轻量模型层:部署蒸馏后的 NLP 模型,语义分类。要求 < 50ms
- L3 — 大模型层(异步):复杂语义、上下文理解。走 MQ 异步处理,结果延迟回调
- L4 — 人工审核层:高风险弹幕进入人工审核工单
降级策略:
- L3 降级:大模型超时 → 降级到 L2 规则模型结果
- L2 降级:模型推理失败 → 降级到 L1 规则引擎
- L1 降级:规则引擎过载 → 放行(允许少量漏过,回查追责)
- 缓存降级:Redis 不可用 → 本地缓存 + 限流
机器刷屏识别:
- 时间维度:同一用户/同一 IP 在单位时间内发送频率
- 内容维度:弹幕内容相似度(编辑距离/SimHash)
- 行为维度:多账号在相同毫秒级时间点发送相同内容
🔗 相关知识
- Java泛型-类型擦除 - Java 泛型擦除机制与限制
- JVM-对象创建与内存分配 - 对象从 new 到 GC 全过程
- GC-Roots详解 - GC Roots 的组成与类卸载机制
- Java读写锁-ReadWriteLock - 读写锁使用场景与锁降级
- synchronized机制详解 - synchronized 锁机制分析
- Java-wait-notify机制 - wait/notify 线程协作机制
- TCP三次握手与可靠性 - TCP 握手、SYN Flood、TIME_WAIT
- Redo日志详解 - Redo log 与两阶段提交
- MySQL Binlog 日志配置 - Binlog 配置与作用
- MySQL深度分页优化 - 深度分页问题与优化
- Redis-BigKey解析 - BigKey 问题分析与处理
- JVM-OOM排查指南 - OOM 排查方法
- 线程死锁定位与避免 - 死锁检测与预防
- 接口性能排查指南 - SQL 性能问题排查
- Spring-Transactional失效场景 - @Transactional 失效分析
- 大数据量异步导出方案 - 大数据量导出设计
- 接口幂等方案设计 - 幂等方案
- 滑动窗口算法 - 无重复字符的最长子串
- 直播弹幕审核系统设计 - 实时审核系统架构