什么是async-profiler

简介

async-profiler是一个低开销的 Java 采样分析器,它不会遇到安全点偏差问题。它使用HotSpot 特定的 API 来收集堆栈信息,追踪内存分配。该分析器可与 OpenJDK、Oracle JDK ,以及其他基于 HotSpot JVM 的 Java 运行时配合使用。

async-profiler 可以跟踪以下类型的事件:

  • CPU周期
  • 硬件和软件性能计数器,例如缓存未命中、分支未命中、页面错误、上下文切换等。
  • Java 堆中的分配
  • Contented lock尝试,包括 Java 对象监视器和 ReentrantLocks

下载地址

支持的平台

  • Linux / x64 / x86 / arm64 / arm32 / ppc64le
  • macOS / x64 / arm64

功能介绍

CPU profiling

在这个模式中,profiler收集stack traces的样本,其中包括Java methods,native calls, JVM代码和内核函数。

普遍的方法是接收 perf_events 生成的stack traces,然后将它们与 AsyncGetCallTrace 生成的调用堆栈进行匹配,以便生成 Java 和native code 的准确配置文件。此外,在 AsyncGetCallTrace 失败的某些极端情况下,async-profiler 提供了一种解决方法,可以恢复stack traces。

与另一种方式:直接将 perf_events 与 Java agent 结合使用,将地址转换为 Java method name 相比,该方法具有以下优点:

  • 适用于较旧的 Java 版本,因为它不需要 -XX:+PreserveFramePointer,该选项仅在 JDK 8u60 及更高版本中可用。
  • 不会引入 -XX:+PreserveFramePointer 的性能开销,在极少数情况下,这种性能开销可能高达 10%。
  • 不需要生成映射文件来将 Java 代码地址映射到方法名称。
  • 与解释器框架一起使用。
  • 不需要写出 perf.data 文件以在用户空间脚本中进行进一步处理。

ALLOCATION profiling

除了检测检测消耗 CPU 的代码,async-profiler可以配置为收集分配了最大堆内存的调用站点。

async-profiler 不使用字节码检测或昂贵的 DTrace 探针等侵入性技术,这些技术会对性能产生重大影响。它也不会影响逃逸分析或防止 JIT 优化(例如分配消除)。它仅测量实际的堆分配。

该profiler具有 TLAB 驱动的采样功能。它依赖于 HotSpot 特定的回调来接收两种类型的通知:

  • 当在新创建的 TLAB 中分配对象时(火焰图中的水色框)
  • 当对象分配在 TLAB 外部的慢速路径上时(棕色帧)。

这意味着不会计算每个分配,而只会计算每 N kB 的分配,其中 N 是 TLAB 的平均大小。这使得堆采样非常便宜并且适合生产。另一方面,收集的数据可能不完整,尽管在实践中它通常会反映最高的分配来源。

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

支持的最低 JDK 版本是 7u40,其中出现了 TLAB 回调。

Installing Debug Symbols

在 JDK 11 之前,分配分析器需要 HotSpot 调试符号。 Oracle JDK 已将它们嵌入到 libjvm.so中,但在 OpenJDK 版本中,它们通常在单独的包中提供。例如,要在 Debian / Ubuntu 上安装 OpenJDK 调试符号,请运行:

1
apt install openjdk-8-dbg

或者对于 OpenJDK 11:

1
apt install openjdk-11-dbg

gdb 工具可用于验证 libjvm 库的调试符号是否已正确安装。例如在 Linux 上:

1
gdb $JAVA_HOME/lib/server/libjvm.so -ex 'info address UseG1GC'

该命令的输出将包含Symbol "UseG1GC" is at 0xxxxx 或者No symbol "UseG1GC" in current context

Wall-clock profiling

-e wall 选项告诉 async-profiler 在每个给定时间段对所有线程进行同等采样,无论线程状态如何:正在运行、睡眠或阻塞。例如,这在分析应用程序启动时间时非常有用。

Wall-clock profiler在每线程模式下最有用:-t

举例:

1
asprof -e wall -t -i 5ms -f result.html 8983

Java method profiling

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

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

这种方式仅支持non-native Java methods。如果想要分析native method,需要使用hardware breakpoint event 事件,例如 -e Java_java_lang_Throwable_fillInStackTrace

请注意,如果您在运行时附加 async-profiler,non-native Java methods的第一次检测可能会导致所有已编译方法的去优化。后续检测仅刷新相关代码。如果将 async-profiler 作为agent附加,则不会发生大规模 CodeCache 刷新。

下面是一些对于分析有用的 native methods:

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

如何安装

(1)确保环境变量JAVA_HOMEGCC正常,手动make即可。

(1)直接下载二进制文件。

1
2
3
4
cd async-profiler/
make
chmod +x ./profiler.sh
./profiler.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
Usage: ./profiler.sh [action] [options] <pid>
Actions:
start start profiling and return immediately
resume resume profiling without resetting collected data
stop stop profiling
dump dump collected data without stopping profiling session
check check if the specified profiling event is available
status print profiling status
meminfo print profiler memory stats
list list profiling events supported by the target JVM
collect collect profile for the specified period of time
and then stop (default action)
Options:
-e event profiling event: cpu|alloc|lock|cache-misses etc.
-d duration run profiling for <duration> seconds
-f filename dump output to <filename>
-i interval sampling interval in nanoseconds
-j jstackdepth maximum Java stack depth
-t profile different threads separately
-s simple class names instead of FQN
-g print method signatures
-a annotate Java methods
-l prepend library names
-o fmt output format: flat|traces|collapsed|flamegraph|tree|jfr
-I include output only stack traces containing the specified pattern
-X exclude exclude stack traces with the specified pattern
-v, --version display version string

--title string FlameGraph title
--minwidth pct skip frames smaller than pct%
--reverse generate stack-reversed FlameGraph / Call tree

--loop time run profiler in a loop
--alloc bytes allocation profiling interval in bytes
--live build allocation profile from live objects only
--lock duration lock profiling threshold in nanoseconds
--total accumulate the total value (time, bytes, etc.)
--all-user only include user-mode events
--sched group threads by scheduling policy
--cstack mode how to traverse C stack: fp|dwarf|lbr|no
--begin function begin profiling when function is executed
--end function end profiling when function is executed
--ttsp time-to-safepoint profiling
--jfrsync config synchronize profiler with JFR recording
--lib path full path to libasyncProfiler.so in the container
--fdtransfer use fdtransfer to serve perf requests
from the non-privileged target

<pid> is a numeric process ID of the target JVM
or 'jps' keyword to find running JVM automatically
or the application's name as it would appear in the jps tool

Example: ./profiler.sh -d 30 -f profile.html 3456
./profiler.sh start -i 999000 jps
./profiler.sh stop -o flat jps
./profiler.sh -d 5 -e alloc MyAppName

基本用法

1
2
3
4
5
jps
18023 Jps
18006 Hello
sudo ./profiler.sh start 18006
sudo ./profiler.sh start 18006

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
--- Execution profile ---
Total samples : 2189

--- 12710254800 ns (58.06%), 1271 samples
[ 0] libmCos
[ 1] Hello.for_loop
[ 2] Hello.loopbig
[ 3] Hello.main

--- 5710052600 ns (26.08%), 571 samples
[ 0] libmSin
[ 1] Hello.for_loop
[ 2] Hello.loopbig
[ 3] Hello.main

--- 1859944300 ns (8.50%), 186 samples
[ 0] libmCos
[ 1] Hello.for_loop
[ 2] Hello.loopsmall
[ 3] Hello.main

--- 760076900 ns (3.47%), 76 samples
[ 0] libmSin
[ 1] Hello.for_loop
[ 2] Hello.loopsmall
[ 3] Hello.main

--- 699998700 ns (3.20%), 70 samples
[ 0] Hello.for_loop
[ 1] Hello.loopbig
[ 2] Hello.main

--- 150021300 ns (0.69%), 15 samples
[ 0] Hello.for_loop
[ 1] Hello.loopsmall
[ 2] Hello.main

ns percent samples top
---------- ------- ------- ---
14570199100 66.56% 1457 libmCos
6470129500 29.56% 647 libmSin
850020000 3.88% 85 Hello.for_loop

注意!可以使用以下内容代替 pid (18006):

  • 关键字 jps,它将使用最近启动的 Java 进程。
  • jps 输出中显示的应用程序名称:例如Hello

当然,我们也可以只用单个命令完成统计(指定 -d(持续时间)参数,在固定时间段内分析应用程序)。举例:

1
sudo ./profiler.sh -d 30 18006

默认情况下,分析频率为 100Hz(每 10 毫秒 CPU 时间)。

以Agent的方式启动

如果你需要在 JVM 启动后立即分析某些代码,可以在命令行上附加 async-profiler 作为代理,而不是使用 asprof。例如:

1
java -agentpath:/path/to/libasyncProfiler.so=start,event=cpu,file=profile.html ...

Agent library通过 JVMTI 参数接口进行配置。参数字符串的格式在源代码中描述。 asprof 实际上就是将命令行参数转换为该格式。比如,-e wall 转换为 event=wall-f profile.html 转换为 file=profile.html 等。然而,有些参数是由 asprof 直接处理的。例如。 -d 5 其实包含 3 个操作:

1
2
3
(1)使用启动命令附加探查器代理
(2)休眠 5 秒
(3)然后使用停止命令再次附加代理

多个事件

使用async profiler,我们可以同时分析 CPU、allocations 和locks 。当然,也可以选择任何其他执行事件来代替 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

火焰图可视化

async-profiler 提供开箱即用的 Flame Graph 支持。指定 -o Flamegraph 参数将分析结果转储为交互式 HTML 火焰图。此外,如果目标文件名以 .html 结尾,则会自动选择火焰图输出格式。

1
2
3
4
jps
9234 Jps
8983 Hello
asprof -d 30 -f /tmp/flamegraph.html 8983

image-20230928213514878

Profiler Options

  • start - 以半自动模式开始分析,即分析器将运行直到显式调用stop命令。
  • resume - 启动或恢复之前已停止的分析会话。所有收集的数据仍然有效。分析选项不会在会话之间保留,应重新指定。
  • stop - 停止分析并打印报告。
  • dump - 转储收集的数据而不停止分析会话。
  • check - 检查指定的分析事件是否可用。
  • status - 打印分析状态:分析器是否处于活动状态以及持续时间。
  • meminfo - 打印已用内存统计信息。
  • list - 显示可用于目标进程(如果指定了 PID)或默认 JVM 的分析事件列表。
  • -d N - 分析持续时间(以秒为单位)。如果未给出startresumestopstatus选项,profiler将运行指定的时间段,然后自动停止。
  • -e event - 分析事件:cpu、alloc、lock、cache-misses 等。使用 list 查看可用事件的完整列表。
  • -o fmt - 指定分析结束时转储哪些信息。 fmt 可以是以下选项之一:
    • traces[=N] - 转储call traces (最多 N 个 samples)
    • flat[=N] - 转储 flat profile(前N个热门方法);可以与 traces 结合使用,例如traces=200,flat=200
    • jfr - 以 Java Mission Control 可读的 Java Flight Recorder 格式转储事件
    • collapsed - 以 FlameGraph 脚本使用的格式转储折叠的调用跟踪。这是调用堆栈的集合,其中每一行都是以分号分隔的帧列表,后跟一个计数器
    • flamegraph - 生成 HTML 格式的火焰图。
    • tree - 生成 HTML 格式的调用树。
      --reverse 生成回溯视图。

还有很多,自行查阅相关文档…………

实战案例参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import java.util.ArrayList;
import java.util.Random;
import java.util.UUID;

/**
* <p>
* 模拟热点代码
*
* @Author niujinpeng
*/
public class HotCode {

private static volatile int value;

private static Object array;

public static void main(String[] args) {
while (true) {
hotmethod1();
hotmethod2();
hotmethod3();
allocate();
}
}

/**
* 生成 6万长度的数组
*/
private static void allocate() {
array = new int[6 * 1000];
array = new Integer[6 * 1000];
}

/**
* 生成一个UUID
*/
private static void hotmethod3() {
ArrayList<String> list = new ArrayList<>();
UUID uuid = UUID.randomUUID();
String str = uuid.toString().replace("-", "");
list.add(str);
}

/**
* 数字累加
*/
private static void hotmethod2() {
value++;
}

/**
* 生成一个随机数
*/
private static void hotmethod1() {
Random random = new Random();
int anInt = random.nextInt();
}
}

CPU profiling

运行命令:

1
./profiler.sh -d 30 -f /tmp/19365.html 19365

结果

image-20230929101110602

以另一种形式输出

1
./profiler.sh -d 30 -o tree -f /tmp/tmp.html 19685

image-20230929112756707

Allocation profiling-1

1
./profiler.sh -d 30 -e alloc -f 19365-alloc.svg 19365

image-20230929101848379

Allocation profiling-2

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.concurrent.ThreadLocalRandom;

public class AllocatingTarget implements Runnable{

public static volatile Object sink;

public static void main(String[] args) {
new Thread(new AllocatingTarget(), "AllocThread-1").start();
new Thread(new AllocatingTarget(), "AllocThread-2").start();
}

@Override
public void run() {
while (true) {

}
}

private static void allocate() {
if (ThreadLocalRandom.current().nextBoolean()) {
sink = new int[128 * 1000];
} else {
sink = new Integer[128 * 1000];
}
}
}

运行命令:

1
2
./profiler.sh start -e alloc 20172
./profiler.sh end -e alloc 20172

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
--- Execution profile ---
Total samples : 1743534

--- 442711562304 bytes (49.58%), 864644 samples
[ 0] int[]
[ 1] AllocatingTarget.allocate
[ 2] AllocatingTarget.run
[ 3] java.lang.Thread.run

--- 441959922816 bytes (49.50%), 863176 samples
[ 0] java.lang.Integer[]
[ 1] AllocatingTarget.allocate
[ 2] AllocatingTarget.run
[ 3] java.lang.Thread.run

--- 4181196800 bytes (0.47%), 7975 samples
[ 0] java.lang.Integer[]
[ 1] AllocatingTarget.allocate
[ 2] AllocatingTarget.run
[ 3] java.lang.Thread.run

--- 4057464832 bytes (0.45%), 7739 samples
[ 0] int[]
[ 1] AllocatingTarget.allocate
[ 2] AllocatingTarget.run
[ 3] java.lang.Thread.run

bytes percent samples top
---------- ------- ------- ---
446769027136 50.04% 872383 int[]
446141119616 49.96% 871151 java.lang.Integer[]

生成火焰图命令:

1
./profiler.sh -d 60 -e alloc -f /tmp/20417.alloc.html 20417

结果:

image-20230929120559171

Wall-clock profiling

1
./profiler.sh -e wall -t -i 5ms -f /tmp/19685.result.html 19685

image-20230929104152228

放大main线程,发现与单独采样结果是一样的

image-20230929104249052

methods profiling

采集methods

1
./profiler.sh -e HotCode.allocate 19685

image-20230929105109973

参考

[1] https://github.com/async-profiler/async-profiler