帆软二面面经:JVM 内存、分布式 ID 发号器与 Linux 排查
- 轮次
- 二面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
实习拷打
Java / JVM
- 语言方面是 Java 用得多对吧?讲一下 JVM 的内存分布。
- 堆内和堆外有什么区别?
- 怎么控制堆内的内存上限?
- 大小怎么设置?
- 是哪个参数?
- 哪个参数记得吗?
- RSS 这个参数是干嘛的?
- 哪些情况会产生 OOM?
- 如果发现了 OOM,该怎么解决?
- 如果是元数据空间发生了 OOM 怎么办?
分布式 / 并发 / 锁
- 设计一个高可用的分布式 ID 发号器,要求全局唯一、高并发,给出方案和选型理由。
- 雪花算法的 64 位怎么划分?为什么它是趋势递增?
- 雪花算法和 UUID 各有什么样的坑点?
- synchronized 底层是怎么实现的?
- 它为什么会有偏向锁、轻量级锁、重量级锁这样的设计?
- 它跟 ReentrantLock 相比有什么优缺点?
MySQL / 数据结构
- MySQL 的索引是什么数据结构?
- 跟 B 树有什么区别?
- 跟二叉树有什么区别?
- 树和图有什么区别?
- 图的遍历算法有哪些?
容器化 / 监控
- 有了解过容器化技术吗?
- Docker 的常见命令有什么?
- K8s 有了解过吗?
- 普罗米修斯这类监控技术有了解过吗?
操作系统 / Linux
- 怀疑 CPU 占用比较高,该用哪些命令去排查?
- 如果想看磁盘占用,用哪个命令?
- 测网速相关的命令还记得吗?
- Java 打堆栈用的是什么命令?
- map / man 命令是干嘛的?
- Linux 操作系统里面的权限是什么样的逻辑?
- 有一个文件权限是 555,555 是什么意思?
- 755 跟 555 的区别是什么?
- 怎么去更改一个文件的权限?
- 线程跟进程有什么区别?
- 用户态和内核态有什么区别?
《参考解析》
-
分布式 ID 发号器的选型要能对着约束讲:需求先拆成四条——全局唯一、趋势递增(对索引友好)、高可用(不能有单点)、高并发。可选方案按适用规模排:数据库自增或号段模式(一次取一段缓存在内存,DB 只承担分段分配,简单可控,是大多数团队的起步方案);Redis 的
INCR(性能好但要考虑持久化与主从切换丢号);雪花算法(本地生成、无网络开销、趋势递增,代价是依赖机器时钟);以及美团 Leaf、百度 UidGenerator 这类成熟实现。高可用的关键点在于「分配器自身不能是单点」——号段模式要做多实例 + 双 buffer 预取,雪花算法要解决时钟回拨(等待、改用备用 workerId、或把回拨时间补进位)。答完补一句:如果业务能接受非连续 ID,优先本地生成而不是每次远程取号。 -
雪花算法的 64 位划分与「趋势递增」,以及和 UUID 的坑:1 位符号位固定为 0、41 位毫秒时间戳、10 位机器 ID(一般 5 位数据中心 + 5 位工作机器)、12 位序列号,所以单机每毫秒可生成 4096 个 ID,41 位时间戳够用约 69 年。趋势递增来自「高位是时间戳」——同一毫秒内序列号自增、毫秒增长时高位变大,因此生成的 ID 整体随时间增大,插入 B+ 树索引时基本在右侧追加,页分裂少;UUID 是随机分布,作为主键会让索引插入点随机、页分裂与缓存命中都变差,而且 128 位字符串占空间、可读性差。但雪花算法也有自己的坑:依赖机器时钟(回拨会产生重复 ID)、机器 ID 需要统一分配与回收、workerId 冲突会导致不同机器生成相同 ID。答这题时把「UUID 的坑在索引与存储,雪花的坑在时钟与机器号分配」分清楚,就比单纯比较位数更有信息量。
-
JVM 内存分布、堆内堆外与上限参数:运行时数据区包括堆(对象与数组,线程共享)、方法区/元空间(类元信息、运行时常量池,JDK 8 之后移到本地内存)、虚拟机栈(每个线程一个,放栈帧、局部变量表)、本地方法栈、程序计数器。堆内由
-Xms/-Xmx控制初始与最大,通常设成相等避免运行期扩缩容带来的抖动;堆外主要来自直接内存(ByteBuffer.allocateDirect、NIO、Netty),用-XX:MaxDirectMemorySize限制,元空间用-XX:MaxMetaspaceSize限制。这里最容易混的是「进程内存」和「堆内存」——像 RSS 是操作系统视角的常驻物理内存,等于堆加元空间加直接内存加线程栈加 JVM 自身开销,所以经常出现「堆只用了 2G、容器却被 OOM Kill」的情况,监控必须同时看 RSS 与容器 limit,而不是只看堆。 -
OOM 的分类与排查:先分类型再谈解法——堆 OOM(对象太多或泄漏,用
-XX:+HeapDumpOnOutOfMemoryError拿 dump,再用 MAT 看支配树找出持有者)、元空间 OOM(动态生成类过多,常见于反射、CGLIB、脚本引擎、热部署反复加载类,可通过限制 MaxMetaspaceSize 先暴露问题,再定位是哪段代码在无节制生成类)、直接内存 OOM(Netty 之类没释放 ByteBuf)、GC overhead limit exceeded(回收效率过低,本质还是堆不够或泄漏)、以及unable to create new native thread(线程数或栈空间问题)。解决路径统一是:拿到现场证据(dump、堆栈、GC 日志)→ 判断是泄漏还是容量不足 → 泄漏就找引用链修代码,容量不足就调整参数并评估是否要换数据结构。回答时强调「先保现场再重启」,是能明显区分候选人经验的一句话。 -
synchronized 的底层实现与锁升级,以及和 ReentrantLock 的对比:
synchronized在字节码层面是monitorenter/monitorexit,JVM 侧靠对象头 Mark Word 记录锁状态,重量级锁对应操作系统的互斥量。锁升级是性能妥协的产物:无锁 → 偏向锁(只有一个线程反复进入,把线程 ID 记在 Mark Word 里,连 CAS 都省了)→ 轻量级锁(出现竞争就用 CAS 自旋抢锁,避免直接陷入内核)→ 重量级锁(自旋失败后挂起线程、进入等待队列,涉及用户态与内核态切换,代价最高)。对比 ReentrantLock:二者都可重入,差别在于 ReentrantLock 支持公平锁、可中断获取、tryLock超时、以及可以配多个 Condition 做精确唤醒,而这些 synchronized 都做不到;反过来 synchronized 由 JVM 保证释放(异常也会解锁)、写法简单、还有锁升级与逃逸分析带来的优化,所以「能用 synchronized 就不用 Lock,需要超时/中断/多条件队列时才上 Lock」是稳妥的回答。 -
MySQL 索引结构以及树、图的延伸:InnoDB 用 B+ 树。跟 B 树的区别是——B 树的非叶子节点也存数据,范围查询要在层间反复回溯,B+ 树非叶子只存键、数据全在叶子且叶子间有链表,因此树更矮、磁盘 IO 更少、范围扫描和排序更快。跟二叉树的区别是「扇出」:二叉树每个节点最多两个子节点,千万级数据树高约 24 层,意味着二十多次随机 IO;B+ 树一个磁盘页能放几百个键,树高通常只有 3~4 层,两三次 IO 就能定位。树和图的本质区别在于有没有环与是否分层——树是连通无环的层级结构、任意两点只有一条路径,图的边可以成环、可以有方向和权重。图遍历两大基础是深度优先(递归或显式栈,适合连通性判断、拓扑排序、回溯类问题)与广度优先(队列,适合无权图最短路径、层序遍历),再往上还有 Dijkstra、Bellman-Ford 等带权最短路。
-
Linux 排查命令与权限:CPU 高先
top看整体与1展开每核、P按 CPU 排序,再用top -Hp <pid>找热点线程,把线程号转成十六进制后jstack <pid> | grep -A 30 <nid>定位到具体代码,必要时配合pidstat、perf;内存用free -h、top的 RES、pmap;磁盘用df -h看容量、du -sh *看目录占用、iostat看 IO;网络用ping、curl -w、mtr、ss -tunlp看连接与端口;Java 打线程栈是jstack,堆直方图jmap -histo,dump 用jmap -dump(生产上更推荐jcmd,对进程的暂停影响更小)。Linux 权限是「属主 / 属组 / 其他」三组各三位(r=4、w=2、x=1)的八进制表示:555 表示三组都是 r-x,谁都只能读和执行、都不能写(脚本、二进制或只读共享目录常用);755 则是属主可读写执行、同组和其他人只读可执行,差别就在属主有没有写权限,用chmod 755 file修改。线程与进程的区别在于资源边界——进程有独立地址空间,线程共享进程的地址空间与文件描述符、只有自己的栈和寄存器,所以创建和切换更轻、但一个线程崩了会带崩整个进程。用户态与内核态的区别在于权限级别与可访问资源:用户态跑应用代码,不能直接碰硬件和特权指令;需要 IO、内存映射、创建进程时通过系统调用(中断或syscall指令)切到内核态,由内核执行完再返回,进入内核态的开销正是很多性能优化的着力点。