想象你是一家叫“代码小馆”的网红餐厅老板。后厨里既有 Go 厨师,也有 Java 厨师,招牌菜一天翻台几百次。最近外卖平台差评如潮——上菜越来越慢,后台还时不时“爆单”:内存飙满、CPU 拉满、传菜员堵在过道里动弹不得。你该怎么办?靠猜吗?猜不出来。你需要的是一套“后厨体检设备”:它能每隔几十毫秒瞄一眼灶台火力烧在哪里,能盘点食材仓库是不是只进不出,能看出传菜员堵在哪个走廊、是不是都在抢同一扇冰箱门,最后把这些数据画成一张看得懂的“火焰监控大屏”。这套设备,在 Go 的后厨叫 pprof,在 Java 的后厨叫 async-profiler。这篇指南会带你认识它们、学会用它们,并用一次完整的排查故事,让你掌握“用数据说话”的性能调优方法。

岗位关联

性能调优是后端工程师从“能写”走向“能扛”的必修课:掌握 pprof 与 async-profiler,你就能在线上事故中快速定位瓶颈,成为团队里值得信赖的“性能医生”。

目录
  1. 可视化演示
  2. 工具家族
  3. CPU剖析
  4. 内存分析
  5. 并发排查
  6. 火焰图解析
  7. 采集命令
  8. 调优实战
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示pprof与async-pr的核心机制,通过入门/进阶/实战/优化四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

pprof与async-pr:ppro pprof与async-pro Go pprof CPU性能分 核心机制可视化 pprof与async-pr整体架构与关键组件

工具家族

假如你经营着“代码小馆”这家网红餐厅,后厨同时站着两位大师傅:一位主打 Go 系轻食,一位擅长 Java 系炖菜。最近差评如潮,你站在后厨门口急得直转:火力是不是烧太旺?食材是不是备太多占地方?传菜员是不是在走廊打转?光靠肉眼可看不出门道。你决定给后厨装上“智能电表”——每隔一小段时间瞄一眼火力烧到了哪个锅,再把这些观察记成流水账。Go 的 pprof 和 Java 的 async-profiler,就是这两块电表。

这两块电表的设计哲学很一致:不打断厨师干活,只需隔几十毫秒“瞄一眼”当前正在执行的动作,攒下的千百条观察记录就组成一份画像(profile)。这种“瞄一眼”的方法在性能分析领域叫采样(Sampling)。它不像给厨师挂秒表那样介入,而是像站在角落数人头,开销极小,却能在足够多的样本下还原火力的真实分布。

pprof 是 Go 官方仓库自带的性能分析工具族。标准库 net/http/pprof 只需一行 import,就会在 HTTP 端口挂出 /debug/pprof 系列端点,能随时导出 CPU、堆、goroutine、mutex 等画像。async-profiler 则是 Java 生态中基于 JVMTI 与 AsyncGetCallTrace 的采样器,能以极低开销采集 CPU、分配与锁事件,并生成 SVG 火焰图。二者语言不同,但最终产出的“画像”在结构上如出一辙。

装上电表后,你不再站在后厨门口干着急。翻开流水账:这一小时火力都烧在哪个锅上、哪个厨师被反复叫停,清清楚楚。同样的,学会这两个工具,服务变慢的原因就从“我感觉”变成了“数据显示”。

profile=⋃t∈Tstack(t)\displaystyle \text{profile} = \bigcup_{t \in T} \text{stack}(t)
每一个 profile(画像)都是一系列时刻上调用栈的并集,采样越密,画像越接近真实。
  1. 时刻集合采样发生的时间点集合,例如每 10ms 一个时刻,共 N 个时刻。
    T={t1,t2,…,tN}T = \{t_1, t_2, \ldots, t_N\}
  2. 调用栈函数在时刻 t 上,CPU 正在执行的函数调用链快照,从最外层一路到最内层。
    stack(t)=(f1→f2→⋯→fk)\text{stack}(t) = (f_1 \to f_2 \to \cdots \to f_k)
  3. 并集汇总把所有时刻的调用栈收集到一起,就构成完整画像,这就是 profile 的数学含义。
    profile={stack(t1),stack(t2),…,stack(tN)}\text{profile} = \{ \text{stack}(t_1), \text{stack}(t_2), \ldots, \text{stack}(t_N) \}

CPU剖析

你发现店里传菜越来越慢,最直观的怀疑是:厨师们火力都烧到哪儿去了?如果一道菜反复回锅,哪怕再快的厨师也白搭。CPU profiling 就像给每口锅装“火力记录仪”,每隔一小段时间看一眼:此刻火苗烧在哪个锅里。Go 的 CPU profile 与 async-profiler 的 cpu 事件,干的就是这件事。

Go 中访问 /debug/pprof/profile 接口,或调用 pprof.StartCPUProfile 即可开启 CPU 采样;async-profiler 用 -e cpu 事件采集。它们以固定频率中断正在运行的线程,记录当时的程序计数器与调用栈,然后立刻恢复执行,整个过程像红外摄像头闪一下,不影响厨师颠勺。

采样几十万轮之后,“烧在每口锅上的时间”就近似等于“每个函数的 CPU 时间”。因为采样是随机的,越耗 CPU 的函数被“逮住”的概率越高。采样点越多,这个近似就越准。

这样一来,“谁的锅烧得最旺”一目了然。如果发现配菜师反复被叫来叫去,就可能要优化它——或者让它少被调用。火力记录仪不会说谎,它会告诉你该把精力投向哪里。

Tf≈SfStotal×Ttotal\displaystyle T_f \approx \frac{S_f}{S_{\text{total}}} \times T_{\text{total}}
函数 f 的 CPU 时间 ≈ 它的采样占比 × 总时长。比例关系是 CPU 剖析的核心。
  1. 样本计数函数 f 在采样中被记录到的次数,数学上等于从时刻集合 T 中筛出包含 f 的时刻数量。
    Sf=∣{t∈T:f∈stack(t)}∣S_f = |\{ t \in T : f \in \text{stack}(t) \}|
  2. 总样本数所有采样点的总数,就是时刻集合的大小。
    Stotal=∣T∣S_{\text{total}} = |T|
  3. 总时间采样持续的真实时间,等于结束时刻减开始时刻。
    Ttotal=结束时刻−开始时刻T_{\text{total}} = \text{结束时刻} - \text{开始时刻}
  4. 占比估计采样占比乘以总时间,就还原出函数 f 的近似 CPU 耗时。
    Tf≈SfStotal×TtotalT_f \approx \frac{S_f}{S_{\text{total}}} \times T_{\text{total}}

内存分析

火力没问题,但后厨偶尔“爆单”——内存用满、进程卡死。这时就要去盘点食材仓库:谁领了一堆食材搁着不用?谁放坏了食材还不扔,搞得仓库越来越挤?内存分析就是这个“仓库盘点”。

pprof 的 heap profile 输出两类关键指标:当前正在使用的对象和字节数(inuse),以及程序启动以来累计分配的次数和字节数(alloc)。前者帮你看“现在仓库堆了啥”,后者帮你看“哪里一直在进货”。如果只盯着当前堆大小,就像只看仓库此刻满不满,却不知道是哪条进货渠道一直在送货。

async-profiler 则支持 alloc 事件,可以记录分配点(allocation site)与大小。当 GC 之后堆占用仍然持续上涨、永不回落,基本就是“食材放了过期不扔”——内存泄漏。判断泄漏看的不是单点绝对值,而是趋势曲线。

仓库盘点完,你发现某位备料员每次都洗一大盆菜放着却用不完——对应代码里某个不断扩容的全局 map 或积压的队列。找到它,限制进货量,仓库立刻清爽。内存调优不是把仓库清空,而是让进货和消耗恢复平衡。

Rleak=dVheapdt>0且 GC 后不回落\displaystyle R_{\text{leak}} = \frac{dV_{\text{heap}}}{dt} > 0 \quad \text{且 GC 后不回落}
堆体积随时间持续增长,且每次垃圾回收后都降不回低位,即为疑似泄漏。
  1. 堆体积当前堆上存活对象占用的字节数,对应仓库里所有食材的总量。
    Vheap=∑size(live object)V_{\text{heap}} = \sum \text{size}(\text{live object})
  2. 变化率堆体积随时间的变化率,数学上表示为导数,物理上就是堆增长的快慢。
    dVheapdt=lim⁡Δt→0V(t+Δt)−V(t)Δt\frac{dV_{\text{heap}}}{dt} = \lim_{\Delta t \to 0} \frac{V(t+\Delta t) - V(t)}{\Delta t}
  3. 泄漏判定变化率持续大于 0,且 GC 后堆的低点一轮比一轮高,判定存在内存泄漏。
    Vafter GC(n+1)>Vafter GC(n)V_{\text{after GC}}^{(n+1)} > V_{\text{after GC}}^{(n)}

并发排查

后厨里一群传菜员来回穿梭。突然出菜口排起长队,一群人挤在走廊不动。你要查:是传菜员数量太多互相堵路?还是有传菜员躲进冷藏室一直不出来?锁竞争则是另一番景象:只有一扇冰箱门,所有传菜员都排着队等开门,门里的人还慢悠悠。这两类问题,就是 goroutine/线程异常与锁竞争。

Go 的 pprof 提供 goroutine profile,可 dump 当前所有 goroutine 的调用栈,数量异常一望便知;block 和 mutex profile 则专门记录等待锁、被锁阻塞的时间。async-profiler 同样支持 -e lock 事件与线程转储,能抓取 Java 线程的 BLOCKED/WAITING 状态,配合 jstack 查看线程栈。

排查套路很固定:先看数量曲线是否单调上涨(疑似泄漏),再看阻塞栈是否都指向同一把锁或同一个 channel,最后看锁持有时间是否过长。常见病根包括:持锁做网络 IO、锁粒度太粗、并发模型里有隐藏的全局串行点。

查完发现,原来传菜通道被设计成了单行道,一人走完全程才放行下一个——对应一个持锁做 IO 的临界区。改成双行道(缩小临界区),队伍立刻疏通。并发排查的终点不是“找到锁”,而是“理解流量为何排队”。

Wlock=∑i=1Nwi\displaystyle W_{\text{lock}} = \sum_{i=1}^{N} w_i
锁等待总时长等于每一次等待时间的累加,它是锁竞争严重程度的最直接度量。
  1. 单次等待时间第 i 次尝试获取锁时等待的时长,等于拿到锁的时刻减去开始等待的时刻。
    wi=t获取成功,i−t开始等待,iw_i = t_{\text{获取成功},i} - t_{\text{开始等待},i}
  2. 等待次数在 profile 周期内发生等待的总次数,只统计那些真正阻塞过的尝试。
    N=∣{i:wi>0}∣N = |\{ i : w_i > 0 \}|
  3. 总等待时长所有等待时间的累加,体现线程们排队等冰箱门的总损失。
    Wlock=w1+w2+⋯+wNW_{\text{lock}} = w_1 + w_2 + \cdots + w_N

火焰图解析

数据采集好了,怎么一眼看懂?只见后厨墙上挂了一块“监控大屏”:每一个横条就是一位厨师或一道工序,横条越长代表在这上面花的时间越多;纵向上,下层是上层被谁叫来的。这屏幕就是火焰图(Flame Graph)。它是性能分析中最直观的语言。

火焰图的每个矩形代表一个调用栈帧:x 轴方向是采样占比,y 轴方向是调用深度。底部是入口 main,往上层层调用。某一块特别宽,说明它或它调用的子孙函数合计吃掉了大量时间。如果顶部出现宽平的“火焰山”,说明热点集中在少数几个叶子函数上。

定位热点时,先找最宽的“火焰柱”,沿柱往下看它出现在哪些父路径里。真正要优化的是宽柱最上方的叶子,它通常是某个具体函数。许多工具支持点击放大、查看源码行号,让定位细到一行代码。

大屏一亮,你立刻看到“红烧肉”工序的横条最宽——对应某个 JSON 序列化函数占了大半 CPU。顺着火焰往下看,找到真正的幕后黑手,优化方向瞬间清晰。火焰图不是让你背数据的,而是让你一眼看见“时间都去哪了”。

Pnode=∑s∈nodeSsStotal\displaystyle P_{\text{node}} = \frac{\sum_{s \in \text{node}} S_s}{S_{\text{total}}}
火焰图中节点的宽度占比 = 该节点相关采样数 ÷ 总采样数。
  1. 节点相关采样属于该节点的采样点数量之和,包括它自身的采样和所有后代节点的采样。
    ∑s∈nodeSs\sum_{s \in \text{node}} S_s
  2. 总采样数整张火焰图对应的所有采样点数量。
    Stotal=∣T∣S_{\text{total}} = |T|
  3. 宽度占比节点在 x 轴上占据的宽度比例,也就是它的相对热度。
    Pnode=node 的采样数总采样数P_{\text{node}} = \frac{\text{node 的采样数}}{\text{总采样数}}

采集命令

有了设备,还得会用。就像体检要选对检查项目:测心电去心电图机,查血常规去采血台。对 Go 服务,启动时悄悄导入 _ "net/http/pprof" 就在默认端口挂上了“体检入口”;对 Java 服务,async-profiler 是便携式设备,随用随戴。

Go 常见操作:导入 pprof 后,curl localhost:6060/debug/pprof/heap 即可拉取堆画像;用 go tool pprof 进入交互式分析,或加 -http 参数一键生成网页版报告。生产环境建议通过独立的 localhost 端口暴露,避免安全隐患。

async-profiler 常见操作:./profiler.sh -d 30 -e cpu -f flamegraph.html 23123 表示采集 PID 23123 的 CPU 30 秒并输出火焰图 HTML;-e alloc 切换为分配分析。它还附带 jattach 插件,可热挂载到运行中的 JVM,无需重启应用。

选对按键,采集一次 30 秒,数据量足以看清热点又不会影响线上太多。就像体检抽血也只要一小管。采集命令是连接“问题”与“答案”的桥梁,熟记几条就够。

N=fs×T\displaystyle N = f_s \times T
一次采集的样本总数 = 采样频率 × 采集时长,这决定了火焰图的分辨率。
  1. 采样频率每秒采样次数,profiler 常用 99Hz,接近 100 但避免与系统周期谐振。
    fs=99 Hzf_s = 99 \, \text{Hz}
  2. 采集时长profile 持续的秒数,一般 30 秒足以覆盖业务周期。
    T=30 sT = 30 \, \text{s}
  3. 样本总数采样频率与时长相乘,得到本次采集的样本总量。
    N=99×30=2970N = 99 \times 30 = 2970

调优实战

一切设备就位,开始真正的大扫除。你遵循三步:先复现“慢”(让后厨进入高峰期),再盯着大屏定位最宽的横条,最后改配方、复测对比。少了任何一步,优化都可能白忙。这套流程就是一次完整的性能调优实战。

第一步复现:用压测工具或真实流量把服务打热,保持问题可观测;第二步定位:打开 CPU 火焰图、堆和锁 profiles,找到共同指向的瓶颈点;第三步验证:改动后回到同场景重测,对比耗时和火焰图变化。如果优化无效,立刻回滚,避免引入新问题。

实践中大量性能问题逃不出三大类:CPU 密集热点(序列化、正则、循环)、内存分配过猛(频繁创建大对象、对象逃逸到堆)、锁竞争失衡(临界区过大、锁粒度粗)。每一类都有对应工具链,一次排查可以全部覆盖。

大扫除结束,同一道高峰菜从 2 分钟降到 30 秒,传菜队伍不堵了,仓库不胀了。下次再遇到“上菜慢”,你不再是凭感觉乱猜的老板,而是手握电表和大屏、按流程走一遍就能交出结论的运营专家。

Speedup=T前−T后T前×100%\displaystyle \text{Speedup} = \frac{T_{\text{前}} - T_{\text{后}}}{T_{\text{前}}} \times 100\%
优化收益用耗时下降百分比衡量;结果为负说明优化反而变慢,需要回滚。
  1. 优化前耗时基线场景下修改前的平均响应耗时,记为 T 前。
    T前=beforeT_{\text{前}} = \text{before}
  2. 优化后耗时同一场景下修改后的平均响应耗时,记为 T 后。
    T后=afterT_{\text{后}} = \text{after}
  3. 加速比公式前后耗时差与优化前耗时的比值,乘以 100% 得到下降百分比。
    Speedup=T前−T后T前×100%\text{Speedup} = \frac{T_{\text{前}} - T_{\text{后}}}{T_{\text{前}}} \times 100\%
  4. 失效判断如果优化后耗时反而增加,加速比为负,说明改动有问题。
    Speedup<0⇒回滚\text{Speedup} < 0 \Rightarrow \text{回滚}

AI 应用场景

大促前 Go 服务 CPU 尖刺排查

阶段:性能监控与调优阶段

解决的问题:传统排查靠翻日志和拍脑袋,CPU 飙升时无法回答“是哪段代码烧的火”。使用 pprof 的 CPU profiling 和火焰图,可以把 CPU 时间还原到函数级占比。

具体案例:某电商大促前,Go 网关 CPU 长期徘徊在 80%。SRE 用 pprof 采集 30 秒 CPU profile,火焰图显示 JSON 反序列化函数占 43%。替换为更快的解析库后,CPU 降至 35%,大促平稳通过。

Java 推荐服务 OOM 抢险

阶段:内存分析与泄漏定位阶段

解决的问题:Java 服务频繁 OOM,重启后复现,无法靠堆大小判断泄漏源头。async-profiler 的 alloc 事件可以把分配点画成火焰图,直接指出“谁在持续进货”。

具体案例:某推荐服务每 6 小时 OOM 一次,团队用 async-profiler 以 -e alloc 采集 60 秒,发现一个缓存线程池不断创建大型 DTO 且存入静态 Map 未清理。修正生命周期后,堆曲线趋于平稳,OOM 消失。

微服务压测后的锁竞争分析

阶段:并发诊断与锁优化阶段

解决的问题:压测中接口 TP99 偏高,难以判断是线程不足还是锁竞争。使用 Go 的 block/mutex profile 或 async-profiler 的 lock 事件,可量化锁等待总时长和热点锁位置。

具体案例:某下单接口 TP99 达到 800ms。Go 的 mutex profile 显示同一把分布式锁的等待时间占比 60%,临界区内有网络调用。拆锁粒度并将 IO 移出临界区后,TP99 下降到 350ms。

Python 演示

python42 行
import randomfrom collections import Counter # 各函数的真实耗时权重, 模拟后厨灶台火力分配FUNC_WEIGHTS = {    'parse_line': 5,    'read_file': 3,    'handle_request': 1,}  def get_actual_budget():    total = sum(FUNC_WEIGHTS.values())    return {k: v / total for k, v in FUNC_WEIGHTS.items()}  def sample_stack(weights):    # 按概率随机命中当前"正在执行"的函数, 模拟一次采样    r = random.random()    acc = 0.0    for name, w in weights.items():        acc += w        if r <= acc:            return name    return 'handle_request'  def sample_profiling(n=20000):    counts = Counter()    actual = get_actual_budget()    for _ in range(n):        counts[sample_stack(actual)] += 1    estimated = {k: v / n for k, v in counts.items()}    return actual, estimated  if __name__ == '__main__':    actual, estimated = sample_profiling(20000)    print('真实占比:', {k: round(v, 3) for k, v in actual.items()})    print('采样估计:', {k: round(v, 3) for k, v in estimated.items()})    max_error = max(abs(actual[k] - estimated[k]) for k in actual)    print('最大误差:', round(max_error, 4))
目的:用 2 万次模拟采样,展示 CPU 采样剖析如何从“盲人摸象”逼近真实函数耗时占比。
  1. FUNC_WEIGHTS = {...}定义了各函数的真实耗时权重,对应后厨灶台的火力分配。
  2. get_actual_budget()把权重归一化成真实占比分布,相当于“上帝视角”的基准数据。
  3. for _ in range(n):循环采样 n 次,如同 pprof 每隔 10ms 记录一次当前栈。
  4. counts[sample_stack(actual)] += 1每次采样给命中的函数加一票,这就是采样的核心投票动作。
  5. estimated = {k: v / n for k, v in counts.items()}用票数除以总采样数,得到各函数耗时占比的估计。
  6. max_error = max(abs(actual[k] - estimated[k]) for k in actual)与真实占比对比,直观看到采样法误差随样本量收敛。

常见误区

误区火焰图里某个函数占了 60%,它一定是效率黑洞,我得把它整段重写。

正解火焰图宽度是“该函数 + 它调用的子孙函数”的合计。就像后厨大屏显示“配菜组”很宽,但宽的原因是配菜组把活派给了洗菜员,锅在洗菜那一步。要沿火焰柱往下看最底层的宽叶,或看该函数自身的 self 时间,才能下结论。只看父函数条宽就动手优化,常常白忙一场。

为何常见:火焰图按调用关系聚合,新手容易把调用者的宽度当成自身开销,忽略了子调用者的贡献。

误区采样只是“瞄一眼”,线上一直开着 pprof 也没关系。

正解采样有开销,频率越高、线程越多,中断与记录栈的代价越大。像在厨房装高清摄像头还连着云端,虽然不直接干扰炒菜,但网线和存储也要占地方。生产上建议低频短时采样(如 30 秒、100Hz),并通过独立 localhost 端口配合权限隔离。

为何常见:工具宣传“低开销”被误读为“零开销”,忽略了采样频率与线程规模放大后的成本。

误区heap profile 显示 inuse 5GB,赶紧查泄漏。

正解堆大可能是缓存、连接池的合理占用,泄漏要看“GC 后是否长期单边上涨”这一趋势,而不是单点绝对值。就像仓库堆满食材不一定浪费,关键看是不是只进不出。要结合 inuse(当前存量)和 alloc(累计进货)两个维度,看分配速率与回收趋势。

为何常见:静态快照最容易看,新手常把“当前值大”当成故障判据,忽略了动态趋势。

误区goroutine 有几万个,肯定泄露了,先砍数量。

正解goroutine 多可能是设计如此,例如每个请求并发拆多个任务。泄漏的判断标准是数量随时间单调上升且不回落,阻塞栈长期停在同一点。要用 goroutine profile 看栈是否都卡在 channel 或锁上,再决定是否优化。

为何常见:数量指标最直观,看到大数字就慌,忽略了“并发模型本来就该有多少并发”这个前提。

误区锁等待次数排第一,那把锁就该拆掉。

正解要看等待总时长和持锁临界区大小。像冰箱门,虽然每天被开 1000 次,但每次只用 1 秒,不是瓶颈;如果每次开门要 30 秒,那才是问题。优化方向是缩小临界区、减小锁粒度,或换无锁结构。

为何常见:次数和时长是两个维度,新手常把“次数多”等同于“伤害大”,忽视了单次持锁时间的影响。

面试题

Q1. Go 服务中引入哪个标准库能最快开启 pprof 的 HTTP 端点?

单选题 · easy

  • A.net/http/pprof← 正确答案
  • B.runtime/debug
  • C.sync/atomic
  • D.encoding/json

答案:A。net/http/pprof 会在默认 mux 上注册 /debug/pprof 系列路由,相当于在后厨门口挂好了“体检入口”牌。其他选项分别是内存统计、原子操作和 JSON 编解码库,都不提供 profile 的 HTTP 端点。

Q2. async-profiler 最突出的特点是基于什么实现低开销采样?

单选题 · easy

  • A.JVMTI 异步取调用栈← 正确答案
  • B.Java Agent 字节码插桩
  • C.JMX 定时轮询统计
  • D.GC 日志离线分析

答案:A。async-profiler 利用 JVMTI 的 AsyncGetCallTrace 能力异步取栈,像红外摄像头一样“闪一眼”而不打断业务执行。字节码插桩、JMX 轮询和 GC 日志都不是它低开销采样的核心机制。

Q3. pprof 的哪个端点用于查看 goroutine 的调用栈?

单选题 · easy

  • A./debug/pprof/goroutine← 正确答案
  • B./debug/pprof/threaddump
  • C./debug/pprof/mutex
  • D./debug/pprof/block

答案:A。goroutine 端点专门 dump 当前所有 goroutine 的栈,对应查看“传菜员都在哪”。threaddump 不是 pprof 的端点,mutex 和 block 分别对应锁持有与阻塞等待。

Q4. CPU profile 中某函数条宽占 70%,下列推断最准确的是?

单选题 · mid

  • A.该函数自身代码必为热点
  • B.含自身与被调函数总占比← 正确答案
  • C.该函数必然存在内存泄漏
  • D.该函数需要立即全面重写

答案:B。火焰图条宽是该函数自身及其所有被调函数的采样总和,相当于“配菜组”宽度包含洗菜员。它不能直接证明自身代码慢,也不能推导出内存泄漏或必须重写,要看更底层的叶子节点。

Q5. 判断内存泄漏的最佳证据是?

单选题 · mid

  • A.当前堆的 inuse 为 5GB
  • B.GC 后堆占用持续单边增长← 正确答案
  • C.alloc 的分配次数极大
  • D.CPU 占用率突破 80%

答案:B。泄漏要看动态趋势:每次 GC 后堆的低点不断抬高,才是“只进不出”的铁证。单点 inuse 大可能是缓存或连接池,alloc 次数大只能说明分配活跃,CPU 高与泄漏没有必然关系。

Q6. 使用 async-profiler 采集 30 秒 CPU 画像并生成火焰图的命令是?

单选题 · mid

  • A.profiler.sh -d 30 -e cpu 成图← 正确答案
  • B.profiler.sh -e alloc 采集分配
  • C.jstack PID 转储线程快照
  • D.jmap dump 导出堆快照文件

答案:A。-d 30 指定时长,-e cpu 指定 CPU 事件,输出火焰图是 async-profiler 的招牌用法。B 是采集分配,C 和 D 是 JDK 自带的线程与堆快照命令,都不是本题要求的 CPU 火焰图采集。

Q7. 火焰图 x 轴上某个矩形宽度直接反映?

单选题 · hard

  • A.函数调用嵌套的深度层次
  • B.该节点的采样计数占比← 正确答案
  • C.函数源码文件与行号位置
  • D.分配内存对象的字节数

答案:B。x 轴宽度由该节点相关采样数除以总采样数决定,采样占比就是时间占比。调用深度由 y 轴表现,源码位置是辅助信息,内存字节数需要专门的分配火焰图才能体现。

Q8. 下列哪些 profile 属于 Go pprof 家庭?

多选题 · mid

  • A.heap 堆内存画像← 正确答案
  • B.goroutine 栈转储← 正确答案
  • C.block 锁阻塞画像← 正确答案
  • D.threaddump 线程转储

答案:ABC。pprof 家族包括 heap、goroutine、block、mutex 等。threaddump 是 Java jstack 的概念,Go 里与之对应的是 goroutine profile,所以 D 不属于 pprof 端点。

Q9. 定位锁竞争时应当重点关注?

多选题 · hard

  • A.锁等待的总时长值← 正确答案
  • B.持锁临界区的大小← 正确答案
  • C.等待次数排行表← 正确答案
  • D.GC 触发频率快照

答案:ABC。锁竞争伤害 = 等待总时长 × 临界区大小 × 等待次数。真正要优化的是持锁临界区与总等待时间,次数排行是线索而非结论。GC 频率与锁竞争无直接因果,不应混入判断。

Q10. 关于采样式剖析的误差,下列说法正确的是?

多选题 · extreme

  • A.采样次数越多估计越准← 正确答案
  • B.低频采样遗漏短命热点← 正确答案
  • C.采样概率正比于耗时← 正确答案
  • D.采样对线上完全无影响

答案:ABC。采样统计依赖大数定律,样本越多越接近真实;低频采样对持续时间短于采样间隔的热点可能完全漏掉;函数越耗电越容易被逮到,所以概率正比于耗时。但采样有中断与记录开销,D 的“完全无影响”是错的。