张文成面试突击:3招搞定性能优化报错
看着满屏红色的 StackTrace,心里是不是咯噔一下? 别慌,这堆报错看着吓人,其实 80% 都是性能优化的坑。 今天拆解张文成相关的高频考点,带你把报错变分数。
考点梳理:别被名字骗了
很多新手听到“张文成”,第一反应是“这谁?”。 在技术圈,这通常指代某类特定场景下的性能瓶颈或代码异味。 面试官问这个,不是考你背名字,是考你排查问题的能力。
核心考点就三个:
- 资源泄露:对象创建后没释放,堆内存爆满。
- 死锁/竞态:多线程下状态不一致,线程卡死。
- 算法复杂度失控:嵌套循环太多,时间复杂度从 O(N) 变 O(N²)。
根据 MDN Web Docs 对 JavaScript 引擎机制的描述,垃圾回收(GC)是标记-清除算法。 如果引用链不断,对象永远无法回收。 这就是“张文成”式报错的根源:你以为只是慢,其实是内存撑爆了。
标准答法:面试怎么说
面试官问:“线上服务突然变慢,日志里全是 OOM,怎么查?” 错误回答:“重启吧,重启好了。”(直接挂) 正确回答:“分三步走。”
第一步:定位现场 不要急着改代码。先看监控面板。 是 CPU 飙高,还是 Memory 飙高? 如果是 Memory,看 GC 频率。GC 越来越频繁,说明活对象太多。
第二步:抓 Dump 文件
Java 用 jmap,Go 用 pprof,JS 用 Chrome DevTools 的 Heap Snapshot。
拿到快照后,看支配树(Dominator Tree)。
找出占用内存最大的那棵子树,通常就是“张文成”的本体。
第三步:代码回溯 找到大对象,看是谁 new 出来的,谁持有的。 重点检查:
- 是否有全局缓存没设上限?
- 是否有监听器没注销?
- 是否有闭包引用了大对象?
话术模板: “我先通过监控确认是内存问题,然后抓取 Heap Dump 分析支配树,发现是 XX 模块的缓存未清理,导致对象滞留。修复方案是引入 LRU 策略限制缓存大小,并添加弱引用。”
代码实现:看代码说话
光说不练假把式。 看一段典型的“张文成”式代码,找找问题在哪。
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;public class PerformanceTrap {// 这是一个典型的内存泄露陷阱private static final Map<String, List<Byte>> CACHE = new ConcurrentHashMap<>();private static final List<Runnable> TASKS = new ArrayList<>();public void processRequest(String key) {// 每次请求都往 CACHE 里塞数据// 假设 value 是一个 10MB 的大数组byte[] data = new byte[10 * 1024 * 1024]; data[0] = 1; // 模拟数据填充// 问题1:CACHE 没有上限,Key 无限增加CACHE.put(key, new ArrayList<>());CACHE.get(key).add(data);// 问题2:TASKS 列表只增不减// 这里模拟了一个异步任务,但从未移除TASKS.add(() -> {System.out.println("Processing " + key);});}public static void main(String[] args) throws InterruptedException {PerformanceTrap pt = new PerformanceTrap();for (int i = 0; i < 1000; i++) {pt.processRequest("key-" + i);if (i % 100 == 0) {System.out.println("Processed " + i);Thread.sleep(100);}}// 运行一段时间后,OOM: Java heap space}
}
逐行拆解:
CACHE是无界 Map 并发环境下,ConcurrentHashMap很安全,但它不控制大小。 如果key是用户 ID,用户量百万级,每个 value 10MB,1000 个用户就是 10GB。 你的服务器内存有 10GB 吗?大概率没有。TASKS列表只进不出ArrayList在单线程下没问题,但这里是静态的。 每次processRequest都add一个 Lambda。 Lambda 捕获了外部变量key和this。 如果this持有大资源,或者key本身关联大对象,这些 Lambda 永远无法被 GC 回收。 这就是典型的隐式引用泄露。没有淘汰机制 真正的生产代码,缓存必须有 TTL(过期时间)或 LRU(最近最少使用)策略。 Guava 的
CacheBuilder或者 Caffeine 库,都能解决这个问题。
追问与延伸:高阶考点
面试官听完基础回答,通常会追问: “如果让你重构这段代码,你会怎么做?”
方案一:引入 Caffeine 缓存 Caffeine 是 Java 界目前性能最好的缓存库之一。 它基于 W-TinyLFU 算法,比 LRU 命中率更高。
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Cache;
import java.util.concurrent.TimeUnit;public class SafePerformance {// 最大保留 1000 个条目// 写入后 10 分钟过期private static final Cache<String, byte[]> CACHE = Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES).build();public void safeProcess(String key) {byte[] data = CACHE.getIfPresent(key);if (data == null) {// 加载数据data = loadData(key);CACHE.put(key, data);}// 不再使用静态 List 存任务,改用线程池}
}
方案二:使用 WeakReference 如果缓存的数据不重要,可以用弱引用。 GC 在内存不足时,会优先回收弱引用对象。 但注意:弱引用的对象在下一轮 GC 时可能就被回收了,适合临时数据。
方案三:监控告警
代码改了,怎么防止再犯?
接入 Prometheus + Grafana。
监控 jvm_memory_used_bytes,设置阈值。
当使用率超过 80% 时,发送钉钉/飞书告警。
性能优化不是事后救火,是事前预防。
延伸考点:Go 语言怎么看?
Go 的 runtime/pprof 是神器。
go tool pprof http://localhost:6060/debug/pprof/heap
直接在浏览器里看火焰图。
哪个函数占用内存大,一目了然。
Go 的 GC 是三色标记法,STW(Stop The World)时间极短。
但如果 goroutine 泄露,内存也会爆。
检查 runtime.NumGoroutine(),如果数量持续上升,说明有 goroutine 没退出。
记忆口诀:考前速记
面试紧张容易忘,记几个关键字:
一看二抓三定位 看监控(CPU/Mem),抓 Dump(Heap/Thread),定位对象(Dominator Tree)。
缓存必设上限 无界 Map = 定时炸弹。 用 Caffeine、LRU、TTL,三选一。
线程池代替 new 不要
new Thread,不要无限add到 List。 用 ThreadPoolExecutor,设置拒绝策略。GC 日志要常看 Java:
-verbose:gc如果 Young GC 频繁,说明对象创建太快,短命对象多。 如果 Old GC 频繁,说明对象活太久,可能是泄露。MDN 查规范,官方看文档 不要瞎猜。 浏览器行为、API 用法,查 MDN Web Docs。 这是前端和后端通用的权威来源。
最后提醒: “张文成”不是标准术语,而是面试中对**“复杂性能问题”**的一种代称。 面试官真正想听的是:你的排查思路是否清晰,是否懂底层原理,是否有实战经验。
别死记硬背代码。 理解内存模型、GC 机制、并发安全,这些才是底层逻辑。 代码变了,逻辑不变。
还有什么不懂的?评论区留言挨个回 比如:“Caffeine 的 LRU 和 LFU 区别是什么?” 或者:“Go 的 channel 阻塞会导致内存泄露吗?” 提出来,我单独开一篇讲。