面灵AI→

帆软三面:Full GC 排查、Jar 防反编译与亿级数据去重计数

轮次
三面
时间
2026-10
来源
牛客网

《面试题目》

自我介绍与实习经历

  1. 自我介绍,并深挖实习经历。

Java 基础与工程安全

  1. 线上 Java 工程出现 Full GC,完整排查思路是什么?
  2. Java 项目打成 Jar 包交付给客户私有化部署,如何防止 Jar 被反编译泄露业务逻辑?直接 zip 压缩 Jar 为什么不可行?

算法题

  1. 文件中有一亿个 0-20000 的整数,如何做去重计数?
  2. 如果数字范围不固定,方案怎么调整?
  3. Bitmap 只能记录 0/1 代表存在与否,如果要统计每个数字出现的频次怎么做?全部数字都不重复会不会内存爆炸?

Linux 运维

  1. 日常用过哪些 Linux 命令?

个人职业与学习

  1. 短期以及拉长到十年的职业规划是什么?选择技术专家、研发管理、产品,怎么权衡?
  2. 你的新技术学习途径有哪些?
  3. 最近学习了哪些新技术?

反问环节

  1. 你有什么想问我的?

《参考解析》

Full GC 排查:先判断「该不该发生」,再判断「为什么停不下来」。 排查顺序建议按「确认现象 → 保留现场 → 定位对象 → 归因 → 修复与防复发」走。第一步不是急着 dump,而是确认三件事:Full GC 的频率(一天一次和一分钟一次是天壤之别)、单次耗时(超过 1 秒就会明显影响接口 P99)、回收后老年代有没有回落。这一条是分水岭——回收后老年代能降下来,说明是短时间大对象或缓存策略问题;降不下来,基本就是内存泄漏或对象生命周期过长。

工具链要熟:jstat -gcutil <pid> 1000 连续观察各区占用与 FGC/FGCT 计数;-Xlog:gc*:file=gc.log:time,uptime(JDK 9+,老版本 -XX:+PrintGCDetails)拿 GC 日志;jmap -dump:format=b,file=heap.hprof <pid> 或启动时开 -XX:+HeapDumpOnOutOfMemoryError 拿堆快照,用 MAT / JProfiler 看支配树与 Leak Suspects;jmap -histo:live 快速看实例数 TOP;GC 导致的 CPU 飙高用 top -Hp <pid> + jstack 交叉确认。dump 会 STW,生产上优先在摘流量的实例上做,或者提前配好自动 dump——这条是很多人在真实故障里踩过的。

归因集中在几类:① 内存泄漏——静态集合只增不减、ThreadLocal 用完没 remove(线程池场景下尤其致命)、监听器/回调注册了不注销、连接或流没关;② 无界缓存——用了 HashMap 当本地缓存却没有淘汰策略,应该换 Caffeine/Guava 并设最大容量;③ 一次性大对象——全量导出、select * 捞出百万行、批量 List 拼接;④ 堆参数与代际比例不合理——新生代太小导致对象过早晋升、大对象直接进老年代(-XX:PretenureSizeThreshold);⑤ 元空间(Metaspace)持续增长——动态生成类、热部署导致类加载器泄漏;⑥ 显式调用 System.gc()。修复后一定要有防复发动作:GC 日志长期落盘 + 对 Full GC 次数和耗时做监控告警、堆内存增长曲线纳入看板。只把 -Xmx 调大是拖延,不是修复——这句话面试里主动说出来会加分。

Jar 防反编译:先承认「防不住」,再说怎么把成本抬高。 Java 编译产物是字节码,保留了大量类名、方法名和结构信息,jadx、CFR、Procyon 这类工具能把绝大多数 Jar 还原成接近原始的可读代码,所以任何方案都只是提高门槛,不存在绝对安全。按强度递增的手段:① 混淆(ProGuard、Allatori、Zelix KlassMaster)——重命名类与方法、删掉调试信息、字符串加密、插入无效分支,能挡住随手反编译的人,但业务逻辑结构仍在,动态调试照样能看懂;② 字节码加密 + 自定义 ClassLoader——class 文件加密存储,运行时由 native 层解密并 defineClass,逆向者要先找到解密逻辑;③ 代码虚拟化/加壳——把关键方法的字节码转成自定义指令集由解释器执行,强度最高,代价是性能下降和维护复杂;④ 关键逻辑下沉到 native(JNI / GraalVM Native Image),逆向门槛最高但失去了跨平台;⑤ 商业授权 + 服务端化——把核心算法做成远程服务,私有化部署的只是客户端。工程上通常是「混淆打底 + 核心模块加密 + 许可证绑定机器指纹」的组合。

「为什么直接 zip 压缩 Jar 不可行」这一问的答案是:zip 不改变文件内容,只是换个容器。 JVM 启动时 -jar 需要能直接读取 Jar 的 zip 结构,解压后 class 文件原封不动地躺在那里,反编译工具直接吃。真正的原因有三层:一是格式等价——tar/zip 与 jar 都是打包格式,不提供任何加密;二是即使加了密码,解压密码必须随程序一起交付或被程序读取,等于把钥匙和锁放在同一个盒子里;三是代码必须能被 JVM 执行,加密过的字节码在运行前必须解密回标准字节码,那一刻内存里一定是明文,攻击者用 -javaagent、字节码 dump 或直接读内存就能拿到。如果客户要求「防住」而不是「防随手」,正解是走上面说的加密 ClassLoader 或 native 化,而不是换一个压缩格式。顺带提一句:Jar 签名(jarsigner)只保证完整性和来源、不保证保密性,这是两个完全不同的目标,这一条经常被面试官用来确认你分得清。

一亿个 0-20000 的整数做去重计数:这就是 Bitmap 的标准场景。 值域只有 20001 个取值,一个 bit 表示某个数是否出现过,总共需要 20001 / 8 ≈ 2.5 KB 内存——注意内存只和值域大小有关,和数据量完全无关,一亿条还是一条都是 2.5 KB。流程:流式读文件(BufferedReader 按行或按缓冲区读,不要一次性 readAllBytes 造成 400MB 的字符串)、每读一个数就 bits[x] |= (1 << (x & 7))、最后统计置位数量(Long.bitCount 按 64 位一组统计最快)就是去重后的个数。如果同一份数据要反复用,把 bitmap 落盘成文件,下次直接读 2.5 KB 索引。这里可以主动补一句大数据量下的 IO 优化:用 FileChannel + 大 buffer、或者 mmap,避免逐行解析的开销成为瓶颈。

「范围不固定」是这一题的真正考点——它在问你怎么从已知值域走到未知值域。 分三种情况:① 值域上界已知但更大(比如 0 到 10 亿),纯 bitmap 要 125 MB,单机还能扛,但要按数据量和内存权衡;如果数据稀疏,改用 Roaring Bitmap(按 65536 分块、块内按稠密/稀疏自适应存数组或位图),稀疏场景能省一到两个数量级;② 值域未知但数据量可控——用哈希集合,但要注意内存代价:HashSet<Integer> 每个元素的开销在 4048 字节量级,一亿个不同值就是 45 GB,跑不动。此时应换成先哈希分片再分治:按 hash(x) % N 把数据分成 N 个小文件,每个分片单独用 bitmap 或集合处理,各分片去重数相加(不同分片不可能有重复值),内存可控且可以并行;③ 数据量远超内存——外部排序:分块读入、块内排序去重、写回磁盘,最后多路归并去重;如果只要求近似基数,HyperLogLog 用几十 KB 就能把误差控制在百分之几,但它给不了精确值和频次。

频次统计与「会不会内存爆炸」:这题是陷阱,答案是不会。 Bitmap 一个 bit 只能表达「有/无」,要计数就得换结构:① 计数数组——int[20001] 只有 80 KB,直接遍历累加,这是最简单也最该先说的答案;② 多 bit 位图——给每个数分配 2 个 bit 能表达 0~3 次,通常再配一个溢出表记录超过阈值的值,适合值域大但频次普遍很低的场景(本质是稀疏计数的折中);③ 16 位计数的位图(每数 2 字节)能到 65535,short[20001] 也就 40 KB。「全部数字都不重复会不会内存爆炸」的答案是恰恰相反:计数的空间开销由值域决定,不由数据条数决定——一亿个互不相同的数,在 int[20001] 上只会有一半左右的槽位是 1,内存还是 80 KB。真正会爆的是另一种选型错误:用 HashMap<Integer, Integer> 存频次,此时内存与不同值的个数成正比,值域一大就完蛋。所以这一题的标准答法是先声明「空间复杂度看值域」,再给出值域大时的兜底(分片 + 排序 / Roaring Bitmap / 外部排序),把「精确 vs 近似」「单机 vs 分布式」这两条横轴也点一下。

Linux 命令不要求背全,要求能按场景串起来。 说得出口的应该是一组排查动作而不是一串命令名:看资源用 top -Hp(找线程热点)、free -m、vmstat 1、iostat -x 1、pidstat;看磁盘用 df -h / du -sh * / lsblk;看进程与端口用 ps -ef、lsof -p / lsof -i:8080、ss -lntp(netstat 的老替代);看网络用 curl -v / ping / traceroute / tcpdump;文本处理用 grep -rn / awk / sed / sort | uniq -c | sort -rn(TOP N 统计的经典组合)/ wc -l / tail -f / less(大文件不要用 cat);文件定位用 find;服务与日志用 systemctl / journalctl -u;权限用 chmod / chown / umask。回答时每条带一句「什么时候用它」,比如「线上接口 RT 抖动用 vmstat 1 看 r 队列和 si/so,先排除 CPU 排队和换页,再去查应用」。面试官问这题的本质是确认你有过真实的机器经验,所以挑两三个你真用过的场景讲透,比报二十个命令名有效。

职业规划题:不要给标准答案,要给一个自洽的取舍逻辑。 技术专家、研发管理、产品这三条路的差别在于「你的产出通过什么放大」——专家靠深度和个人技术影响力,管理靠团队规模和资源配置,产品靠对需求和商业的理解。一个好的回答结构是:短期(13 年)先把某一块技术做扎实、成为团队里遇到这类问题会来找你的人;中期(35 年)开始从「解决问题」转向「定义问题」,判断标准是能不能独立负责一个方向;长期(5~10 年)才是三选一,而选择应该取决于那时候自己的实际优势(是更享受把技术做到极致,还是更擅长协调人和资源),现在就把十年后写死反而显得不真诚。面试官真正想筛的是两件事:你是不是想清楚过、以及你的方向和这个岗位对不对得上。所以一定要把规划和这家公司挂上钩——帆软是 To-B 私有化交付为主的厂商,长期做客户现场部署意味着技术深度和线上排障能力比产品感觉更值钱。

学习途径与最近学的新技术:这一题要能落到具体产出上。 学习途径可以分三档讲:一手信息(官方文档、源码、规范、论文)、二手信息(技术博客、掘金/知乎、公众号、视频)、以及实践(自己搭项目、读开源代码提 issue/PR、把踩坑写成文章)。第三档是分水岭——面试官会顺着问「最近学的这个,你用在哪了、遇到什么问题」,说不出来就暴露了只是看过。所以「最近学习了什么新技术」这一题要选一个你真跑过的东西:说清为什么学它(解决了什么具体问题)、学到什么程度(跑通了什么、卡在哪)、以及你的判断(适不适用于当前业务,有什么代价)。这个回答方式同时也是反问环节的铺垫——你可以接着问面试官团队里对应技术在什么场景下用。

反问:把这一轮当作双向尽调的机会。 原帖里面试官主动给了不少信息,值得记下来对照自己的关注点:团队技术栈是 Java、Spring、Hibernate、GoQuery、Spark、自研的 Redis-MQ 与 Redis 分布式锁,ES 间接用于埋点和日志,另有 Dora 自然语言 BI 仪表板产品、独立的 AI-Agent 团队,内部用 CodeX 辅助开发;他给校招生的观点是——AI 是学习工具,To-B 私有化场景下线上问题不能只依赖 AI,工程师要夯实底层基础、掌握问题排查能力,校招更看重基础知识与问题分析能力,工作经验不是重点。从这些信息能提炼出几个有信息量的反问:自研 Redis-MQ 与分布式锁在什么场景下不能用开源方案替代、维护成本怎么摊;Dora 这类自然语言 BI 的准确率是怎么评估和兜底的;客户私有化部署环境下版本升级和问题定位是怎么做的(这直接决定后端工程师的日常);校招生进来后多久能独立跟一个客户现场。不要问薪资福利、加班、能不能远程这类放最后谈的问题,也不要问官网首页就能查到的信息。一个好的收尾是顺着面试官的话接一句:「您提到 To-B 场景不能只依赖 AI,那团队在排查客户现场问题时有没有沉淀成一套固定的工具或流程」——既是真问题,也顺带证明你听懂了他前面说的话。