面灵AI→

百度后端开发岗面经 12:协议栈、JVM 排查与内存池拷打

轮次
多轮面试合集
时间
2026-09
来源
牛客网

《面试题目》

  1. Agent 可观测性中的 Trace 指什么?
  2. 请介绍 TCP 三次握手和四次挥手
  3. 常见的 HTTP 状态码有哪些?
  4. 请介绍 HTTPS 握手过程
  5. 线程、协程和进程有什么区别?
  6. 请介绍 Go 的 GMP 调度模型
  7. Go 如何实现抢占式调度?
  8. Go 协程之间如何通信?
  9. Java HashMap 的底层实现是什么?
  10. Java 中使用过哪些线程锁?
  11. 如何排查 JVM 问题?
  12. 红黑树的原理是什么?
  13. 红黑树和跳表有什么区别?
  14. Redis ZSet 与跳表是如何实现的?
  15. Docker 的实现原理是什么,常用命令有哪些?
  16. Kubernetes 的核心组件有哪些?
  17. Agent 项目是做什么的?
  18. RPC 了解吗?
  19. 请求方法有哪些?
  20. 请求参数放在哪里(三种)?
  21. 设计一个接口:下单接口,怎么设计请求方法和请求参数,请求过程如何?
  22. 引入 Redis、消息队列是为了什么?
  23. 输入 URL 到浏览器渲染出页面,过程如何?
  24. MySQL 如何解决 SQL 性能差的问题,可能的原因是什么?追问:如何解决?
  25. 索引失效是什么?
  26. 你的 MySQL 是 Docker run 的对吧?
  27. 那个请求到了服务器以后,镜像容器运行时内部是不是也有一个流程?
  28. defer 原理是什么,常见应用场景是什么?追问:使用规范是什么?追问:为什么要这么使用?
  29. 用过什么 Linux 命令?
  30. 如何查看 CPU、内存、磁盘使用情况?
  31. 设计文章点赞排行榜
  32. 开发一个功能:文件打开、读、写到另一个文件、关闭,这个过程会有什么问题?
  33. 手撕:最长无重复子串
  34. Redis 的 hash 的底层原理是什么?
  35. 对 Git 有了解吗?
  36. Docker 的镜像分层是什么?
  37. 手撕:3 个协程交替打印 1~100
  38. 自我介绍
  39. 对于 malloc 的三个痛点,你是在哪里去了一把锁?
  40. 内存池为啥要三层结构,如果变成两层或者四层会发生什么?
  41. 你的内存申请是每次多少为一片?
  42. 我把你的中心缓存踢掉会发生啥?
  43. 你现在有一个指针,如何找到它的 span 呢?
  44. 你说三层基数树,那好,为啥不用一层呢?一层不是更快吗?
  45. 你的混合尺寸测试,在 Windows 和 Linux 下性能不一样,这底层是为什么?(原帖在此截断)

《参考解析》

1. 线程、协程与进程。进程是资源分配的单位,拥有独立地址空间;线程是 CPU 调度的单位,同进程内线程共享内存,切换要陷入内核、保存寄存器与栈,成本在微秒级;协程是用户态的轻量线程,切换只发生在用户态、不用进内核,栈可以很小并按需增长,所以能开到几十万个。代价是协程必须协作式让出,一个协程里写了阻塞式系统调用,整个线程上的其他协程就都被卡住。

2. Go 的 GMP 与抢占式调度。G 是 goroutine,M 是内核线程,P 是逻辑处理器,持有一个本地可运行队列。M 必须绑定一个 P 才能执行 G,本地队列空了会去全局队列或其他 P 那里偷(work stealing),这样把调度开销摊到多个核上。抢占在 Go 1.14 之后是基于信号的异步抢占:以前只在函数调用点检查栈增长标记,死循环里插不进调度点;现在运行超过 10ms 就发 SIGURG,让目标 M 在安全点停下来重新排队。

3. 红黑树与跳表的选择。红黑树是自平衡二叉搜索树,通过颜色约束保证最长路径不超过最短路径的两倍,查找、插入、删除都是 O(log n);跳表用多层链表加随机层高达到期望 O(log n),实现简单得多、范围查询友好(顺序遍历底层链表即可),代价是额外指针空间和随机性带来的常数波动。Redis 的 ZSet 在元素少时用 ziplist,元素多或成员较大时用跳表 + 字典:字典负责 O(1) 按成员查分数,跳表负责按分数排序和范围查询——之所以不用红黑树,除了实现简单,还因为跳表的区间操作代码短得多。

4. 排查 JVM 问题。先分层定位:jps 找到进程,jstat -gcutil 看 GC 频率与停顿,jstack 抓线程栈看有没有死锁或大量 BLOCKED、WAITING,jmap -histo 看对象直方图、jmap -dump 导出堆再用 MAT 分析支配树。CPU 飙高时常用组合是 top -Hp <pid> 找出最烫的线程 id,转成十六进制去 jstack 输出里搜 nid,直接定位到那行代码。内存持续上涨要看是老年代泄漏还是缓存没有上限,别一上来就调大堆。

5. 索引失效的常见场景。在索引列上做函数或运算(WHERE DATE(create_time) = ?)、隐式类型转换(字符串列传数字)、LIKE '%xx' 前置通配、OR 连接的条件里有一侧没索引、联合索引不满足最左前缀、!= 与 NOT IN 导致优化器放弃索引、以及在区分度极低的列上建索引——最后一种其实不是「失效」,是优化器算出来走全表更便宜。排查手段就是 EXPLAIN 看 key 和 rows,别凭感觉判断。

6. 内存池项目的连环追问怎么接。这类问题考的是「你知不知道自己为什么这么设计」。malloc 的三个痛点是锁竞争、内存碎片、频繁向 OS 申请,所以 tcmalloc 用线程本地缓存把绝大部分分配留在用户态(不存在锁竞争),用中心缓存和小对象分级(size class)控制碎片,用页堆按 span 批量向 OS 要内存。三层结构(线程缓存 → 中心缓存 → 页堆)是「快路径无锁、慢路径集中管理」的取舍:减到两层就没有了免锁的本地缓存,加到四层则多一级跨线程搬运、命中率提升有限却增加复杂度。为什么用基数树而不是一层数组,答案是地址空间太大、一层映射表本身就会把内存吃光,三层基数树用稀疏结构只映射实际用到的地址段。被问到「把中心缓存踢掉会怎样」时,要立刻答出后果:线程本地缓存耗尽后只能直接向页堆要,锁竞争回到全局,吞吐会明显下降。