前两篇文章介绍了 jvm 中的安全点机制,以及为什么(大多数)分析器都很难用。这篇文章介绍一下 async-profiler 这个工具。

这个项目是一个针对 Java 的低开销采样性能分析器,不受 Safepoint 偏差问题的影响。它具有 HotSpot 特定的 API, 用于收集堆栈跟踪和跟踪内存分配。该性能分析器可与 OpenJDK 和其他基于 HotSpot JVM 的 Java 运行时配合使用。
项目文档地址:async-profiler/docs/

性能分析模式

除了 CPU 时间,async-profiler 还提供了各种其他性能分析模式,如 Allocation, Wall Clock, Java Method,甚至是 Multiple Events 性能分析模式。

CPU 分析

在这种模式下,性能分析器收集栈跟踪样本,其中包括 Java 方法、原生调用、JVM 代码和内核函数

通用方法是接收 perf_events 生成的调用栈,并将它们与 AsyncGetCallTrace 生成的调用栈进行匹配,以生成 Java 和本机代码的准确性分析。此外,Async-profiler 提供了一种解决方案,用于在 AsyncGetCallTrace 失败的某些案例情况恢复栈跟踪。

与直接在 Java 代理中使用 perf_events 相比,这种方法具有以下优势,后者可以将地址转换为 Java 方法名称:

  • 不需要 -XX:+PreserveFramePointer, 这会引入性能开销,有时高达 10%
  • 不需要在 JVM 开始时使用代理将 Java 代码地址转换为方法名称
  • 显示解释器帧
  • 不会生成大型中间文件(perf.data),一边在用户空间脚本中进一步处理

如果希望在 libjvm 中解析帧,则需要使用调试符号。

ALLOCATION 性能分析

可以将性能呢分析器配置为收集分配了最大量堆内存的调用站点。

Async-profiler 不使用字节码探测或昂贵的 DTrace 探针等侵入性技术,这些技术对性能有显著影响。它也不影响 Escape Analysis 或阻止 JIT 优化 (如分配消除)。只测量实际的堆分配。

性能分析器具有 TLAB(Thread Local Allocation Buffer, 线程本地分配缓存) 驱动的采样功能。它依赖于 HotSpot 特定的回调来接收两种类型的通知:

  • 当在新创建的 TLAB 中分配对象时
  • 当在 TLAB 之外的慢速路径上分配对象时

可以使用 --alloc 选项调整采样间隔。例如 --alloc 500k 将在平均分配空间 500KB 后采样一次。在 JDK 11 之前,小于 TLAB 大小的间隔不会生效。

在分配分析模式下,每个调用跟踪的顶帧是分配对象的类,计数器是堆压力(分配的 TLAB 或 TLAB 外对象的总大小)。

Native memory leaks

性能分析模式 nativemem 记录 mallocrelloccallocfree 地址,以便将分配与空闲调用进行匹配。这有助于将性能分析报告仅集中在非空闲分配上,这些分配可能是内存泄漏的来源。

示例:

1
2
3
4
5
6
7
asprof start -e nativemem -f app.jfr <YourApp>
# or
asprof start --nativemem N -f app.jfr <YourApp>
# or if only allocation calls are interesting, do not collect free calls:
asprof start --nativemem N --nofree -f app.jfr <YourApp>

asprof stop <YourApp>

现在我们需要处理 jfr 文件,以查找原生内存泄漏:

1
2
3
4
5
# --total for bytes, default counts invocations.
jfrconv --total --nativemem --leak app.jfr app-leak.html

# No leak analysis, include all native allocations:
jfrconv --total --nativemem app.jfr app-malloc.html

当使用 --leak 选项时,生成的火焰图将显示未匹配免费调用的分配。

火焰图

为了避免对分析会话结束时未释放的最新分配产生偏差,泄漏分析器会忽略分析期间最后10%的尾部分配。可以通过–tail选项调整尾部长度,该选项接受比率或百分比(%)作为参数。例如,要忽略10分钟分析中最后2分钟的分配,可以使用以下命令:

1
jfrconv --nativemem --leak --tail 20% app.jfr app-leak.html

原生内存分析的开销取决于原生分配的数量,但通常对生产环境来说也足够小。如果需要,可以通过配置分析间隔来降低开销。例如,添加 nativemem=1m 的分析器选项后,分配样本将限制为每分配 1MB 最多一个样本。

Wall-clock 分析

-e wall 选项告诉 async-profiler 在给定时间段内平等地采样所有线程,而不考虑线程状态:运行、睡眠或阻塞。例如,这在分析应用程序启动时间时可能会很有帮助。

Wall-clock 分析器在每线程模式(-t)下最有用。

示例:asprof -e wall -t -i 50ms -f result.html 8983

Lock 分析

-e lock 选项告诉 async-profiler 测量已分析应用程序中的锁争用。锁性能分析可以帮助开发人员了解锁获取模式、锁争用(当线程需要等待获取锁时)、等待锁花费的时间以及哪些代码路径因锁而被阻塞。

在锁分析模式中,顶部帧是锁/监视器类,计数器是进入该锁/监视器所需要的纳秒数。

示例:asprof -e lock -t -i 5ms -f result.html 8983

Java 方法分析

-e ClassName.methodName 选项检测给定的 Java 方法,以记录对该方法的所有调用以及栈跟踪。

示例:-e java.util.Properties.getProperty 将分析调用 getProperty 方法的所有位置。

仅支持非原生 Java 方法。要分析原生方法,请改用硬件断电时间,例如 -e Java_java_lang_Throwable_fillInStackTrace请注意,如果在运行时附加异步配置文件,非原生 Java 方法的第一次插桩可能会导致所有编译的方法去优化。虽有的插桩只会刷新依赖代码。

如果将 async-profiler 作为代理附加,则不会发生大规模的 CodeCache 刷新。

Native function 分析

以下是一些可以分析的 Native function:

  • G1CollectedHeap::humongous_obj_allocate - 跟踪 G1 GC 的巨大分配
  • JVM_StartThread - 跟踪新 Java 线程的创建
  • Java_java_lang_ClassLoader_defineClass1 - 跟踪类加载

Multiple events

可以同时分析 CPU、分配和锁。除了 CPU,还可以选择任何其他执行事件:wall-clock, perf event, tracepoint, Java method 等等

唯一支持多个事件同时分析的输出格式是 JFR。记录将包含以下事件类型:

  • jdk.ExecutionSample
  • jdk.ObjectAllocationInNewTLAB (alloc)
  • jdk.ObjectAllocationOutsideTLAB (alloc)
  • jdk.JavaMonitorEnter (lock)
  • jdk.ThreadPark (lock)

如果分析 cpu + allocations + locks,可以指定:

1
asprof -e cpu,alloc,lock -f profile.jfr ...

或者使用 --alloc--lock 参数并设置所需的阈值:

1
asprof -e cpu --alloc 2m --lock 10ms -f profile.jfr ...

同样,当将分析器作为代理启动时:

1
-agentpath:/path/to/libasyncProfiler.so=start,event=cpu,alloc=2m,lock=10ms,file=profile.jfr

使用 –all 进行多事件分析

--all 标志提供了一个同时启动预定义的常见性能分析时间集合方法。默认情况下,--all 会激活 cpu、wall、alloc、lock 和 nativemem。

注意:虽然 --all 标志对于开发环境来说可以用于获得广泛的概述,但不建议在生产环境中启动它,尤其是对于持续性能分析。用户需要仔细选择要分析的内容以及使用哪些设置。

示例:下面这个命令启用了 --all 中包含的默认事件集:

1
asprof --all -f profile.jfr ...

或者结合 --alloc/--wall/--lock/--nativemem 选项来覆盖单个设置。例如:

1
asprof --all --alloc 2m --lock 10ms -f profile.jfr ...

同样,当将性能分析器作为代理启动时:

1
-agentpath:/path/to/libasyncProfiler.so=start,event=all,alloc=2m,lock=10ms,file=profile.jfr

可以用你选择的任何其他事件类型来覆盖 --all 参数,而不是 cpu。例如,以下命令将分析循环以及 wall, alloc, live, locknativemem

1
asprof --all -e cycles -f profile.jfr

连续性能分析

持续性能分析是一种可以持续性能分析应用程序并在每个指定时间段转储性能分析结果的方法。这是一种主动且高效地发现性能下降的非常有效的技术。持续性能分析有助于用户了解同一应用程序各版本之间的性能差异。可以将最近的输出与持续性能分析的输出历史记录进行比较,以找出差异并优化性能下降情况下引入的变更。aysnc-profiler 提供了使用循环选项持续性能分析应用程序的能力。确保文件名包含时间戳模式,否则输出将在每次迭代中被覆盖。

1
asprof --loop 1h -f /var/log/profile-%t.jfr 8983

perf event types supported on Linux

UsageDescription
Predefined:
-e cpu-clockHigh-resolution per-CPU timer. Similar to -e cpu but forces using perf_events.
-e page-faultsSoftware page faults
-e context-switchesContext switches
-e cyclesTotal CPU cycles
-e instructionsRetired CPU instructions
-e cache-referencesCache accesses (usually Last Level Cache, but may depend on the architecture)
-e cache-missesCache accesses requiring fetching data from a higher-level cache or main memory
-e branch-instructionsRetired branch instructions
-e branch-missesMispredicted branch instructions
-e bus-cyclesBus cycles
-e L1-dcache-load-missesCache misses on Level 1 Data Cache
-e LLC-load-missesCache misses on the Last Level Cache
-e dTLB-load-missesData load misses on the Translation Lookaside Buffer
Breakpoint:
-e mem:<addr>Breakpoint on a decimal or hex (0x) address
-e mem:<func>Breakpoint on a public or a private symbol
-e mem:<func>[+<offset>][/<len>][:rwx>]Breakpoint on a symbol or an address with offset, length and read/write/exec. Address, offset and length can be hex or dec. The format of mem event is the same as in perf-record.
-e <symbol>Equivalent to an execution breakpoint on a symbol: mem:<symbol>:x. Example: -e strcmp will trace all calls of native strcmp function.
Tracepoint:
-e trace:<id>Kernel tracepoint with the given numeric id
-e <tracepoint>Kernel tracepoint with the specified name. Example: -e syscalls:sys_enter_open will trace all open syscalls.
Probes:
-e kprobe:<func>[+<offset>]Kernel probe. Example: -e kprobe:do_sys_open.
-e kretprobe:<func>[+<offset>]Kernel return probe. Example: -e kretprobe:do_sys_open.
-e uprobe:<func>[+<offset>]Userspace probe. Example: -e uprobe:/usr/lib64/libc-2.17.so+0x114790.
-e uretprobe:<func>[+<offset>]Userspace return probe
PMU:
-e r<NNN>Architecture-specific PMU event with the given number. Example: -e r4d2 selects MEM_LOAD_L3_HIT_RETIRED.XSNP_HITM event, which corresponds to event 0xd2, umask 0x4.
-e <pmu descriptor>PMU event descriptor. Example: -e cpu/cache-misses/, -e cpu/event=0xd2,umask=4/. The same syntax can be used for uncore and vendor-specific events, e.g. amd_l3/event=0x01,umask=0x80/