环境准备
首先,你需要从github将代码下载下来:
git clone https://github.com/jvm-profiling-tools/async-profiler
然后,进入到下载好的项目中,然后进行编译(也可以直接下载编译好的):
cd async-profiler
make
可以发现在async-profiler项目中有一个脚本叫做“profile.sh”,运行这个脚本,会输出如下提示内容
Usage: ./profiler.sh [action] [options]
Actions:
start start profiling and return immediately
stop stop profiling
status print profiling status
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 seconds
-f filename dump output to
-i interval sampling interval in nanoseconds
-b bufsize frame buffer size
-t profile different threads separately
-o fmt[,fmt...] output format: summary|traces|flat|collapsed
is a numeric process ID of the target JVM
or 'jps' keyword to find running JVM automatically using jps tool
Example: ./profiler.sh -d 30 -f profile.fg -o collapsed 3456
./profiler.sh start -i 999000 jps
./profiler.sh stop -o summary,flat jps
其中几个重要的命令解释如下:
- start : 开始进行应用的profile数据采集,如果没有设定采集时间的话会一直运行下去直到遇到stop命令
- stop: 和start配合使用,用来停止应用的profile数据采集
- status:检测工具的运行状态,比如可以看到是否已经不可用,或者已经运行多少时间了等信息
- list:将可以采集的profile数据类型打印出来
- -d N: 设定采集应用profile数据的时间,单位为秒
- -e event:指定采集数据类型,比如cpu
下面来开始使用async-profiler工具来采集cpu profile数据,并且配合火焰图生成工具工具FlameGraph来生成cpu火焰图,并且从火焰图中找到热点代码。FlameGraph工具可以直接下载下来就可以使用:
git clone https://github.com/brendangregg/FlameGraph
首先将java应用运行起来,你可以试着运行下面的代码来进行测试:
import java.io.File;
class Target {
private static volatile int value;
private static void method1() {
for (int i = 0; i < 1000000; ++i)
++value;
}
private static void method2() {
for (int i = 0; i < 1000000; ++i)
++value;
}
private static void method3() throws Exception {
for (int i = 0; i < 1000; ++i) {
for (String s : new File("/tmp").list()) {
value += s.hashCode();
}
}
}
public static void main(String[] args) throws Exception {
while (true) {
method1();
method2();
method3();
}
}
}
运行起来之后,可以使用jps命令来查看运行起来的java应用的pid,然后使用下面的命令开始使用工具进行cpu profile数据采集:
./profiler.sh start $pid
一段时间之后,比如30秒后,就可以使用下面的命令来停止数据采集了:
./profiler.sh stop $pid
然后,会打印处下面的信息:
--- Execution profile ---
Total samples : 4239
unknown_Java : 1 (0.02%)
Frame buffer usage : 0.2506%
--- 10770000000 ns (25.41%), 1077 samples
[ 0] __getdirentries64
[ 1] readdir_r$INODE64
[ 2] Java_java_io_UnixFileSystem_list
[ 3] java.io.UnixFileSystem.list
[ 4] java.io.File.list
[ 5] tool.Target.method3
[ 6] tool.Target.main
--- 9820000000 ns (23.17%), 982 samples
[ 0] clntraw_private
[ 1] Java_java_io_UnixFileSystem_list
[ 2] java.io.UnixFileSystem.list
[ 3] java.io.File.list
[ 4] tool.Target.method3
[ 5] tool.Target.main
--- 3670000000 ns (8.66%), 367 samples
[ 0] tool.Target.method1
[ 1] tool.Target.main
--- 3500000000 ns (8.26%), 350 samples
[ 0] tool.Target.method2
[ 1] tool.Target.main
--- 2630000000 ns (6.20%), 263 samples
[ 0] cache
[ 1] java.io.UnixFileSystem.list
[ 2] java.io.File.list
[ 3] tool.Target.method3
[ 4] tool.Target.main
--- 1250000000 ns (2.95%), 125 samples
[ 0] fstatfs64
[ 1] __opendir2$INODE64
[ 2] Java_java_io_UnixFileSystem_list
[ 3] java.io.UnixFileSystem.list
[ 4] java.io.File.list
[ 5] tool.Target.method3
[ 6] tool.Target.main
--- 330000000 ns (0.78%), 33 samples
[ 0] __CFStringChangeSizeMultiple
[ 1] __CFStringAppendBytes
[ 2] newStringPlatform
[ 3] Java_java_io_UnixFileSystem_list
[ 4] java.io.UnixFileSystem.list
[ 5] java.io.File.list
[ 6] tool.Target.method3
[ 7] tool.Target.main
--- 240000000 ns (0.57%), 24 samples
[ 0] __CFStringDecodeByteStream3
[ 1] __CFStringAppendBytes
[ 2] newStringPlatform
[ 3] Java_java_io_UnixFileSystem_list
[ 4] java.io.UnixFileSystem.list
[ 5] java.io.File.list
[ 6] tool.Target.method3
[ 7] tool.Target.main
可以很直观的看出,占用cpu时间最多的是method3,然后是method2和method1.所以很明显method3就是性能瓶颈,也就是所谓的热点代码,需要着手进行优化。当然,上面是有的命令式是比较简单的,下面来介绍一个比较厉害的命令,可以设定采集数据的时间,并且可以将采集到的数据dump起来,然后使用FlameGraph工具来生成火焰图进行直观的分析。
./profiler.sh -d 10 -o collapsed -f /tmp/collapsed.txt pid
这个命令的意思是说,采集数据的时间为10秒,并且将数据按照collapsed规范进行dump,并且dump到/tmp/collapsed.txt这个文件,过了10秒之后,工具会自动停止,并且将cpu的profile数据dump到指定的路径(按照指定的规范),可以到/tmp/collapsed.txt查看具体的文件内容,但是很大程度上是看不懂的,所以需要使用FlameGraph工具来进行加工一下,可以使用下面的命令来生成火焰图:
FlameGraph/flamegraph.pl --colors=java /tmp/collapsed.txt > flamegraph.svg
也可以直接生成svg
./profiler.sh -d 30 -f /tmp/flamegraph.svg 79
很快,你就可以在当前目录下发现多了一个flamegraph.svg文件,使用chorm打开,就可以看到下面的图片内容
可以看到,method3是最宽的,也就代表method3占用的cpu时间是最多的,这样看起来就直观很多了。
下面来看一下alloc类型的数据式怎么生成的,可以从这些数据中看出什么,运行下面的代码:
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) {
allocate();
}
}
private static void allocate() {
if (ThreadLocalRandom.current().nextBoolean()) {
sink = new int[128 * 1000];
} else {
sink = new Integer[128 * 1000];
}
}
}
然后使用jps命令取到该应用的pid,然后执行下面的命令:
./profiler.sh stop -e alloc pid
一段时间之后,可以使用下面的命令来停止数据采集:
./profiler.sh stop -e alloc pid
--- Execution profile ---
Total samples : 664942
Frame buffer usage : 0.0018%
--- 164856351600 bytes (28.16%), 321975 samples
[ 0] java.lang.Integer[] (out)
[ 1] tool.AllocatingTarget.allocate
[ 2] tool.AllocatingTarget.run
[ 3] java.lang.Thread.run
--- 164126728800 bytes (28.04%), 320550 samples
[ 0] int[] (out)
[ 1] tool.AllocatingTarget.allocate
[ 2] tool.AllocatingTarget.run
[ 3] java.lang.Thread.run
--- 129803414088 bytes (22.17%), 11353 samples
[ 0] int[]
[ 1] tool.AllocatingTarget.allocate
[ 2] tool.AllocatingTarget.run
[ 3] java.lang.Thread.run
--- 126622144704 bytes (21.63%), 11063 samples
[ 0] java.lang.Integer[]
[ 1] tool.AllocatingTarget.allocate
[ 2] tool.AllocatingTarget.run
[ 3] java.lang.Thread.run
--- 4802496 bytes (0.00%), 1 sample
[ 0] java.lang.String
[ 1] [no_Java_frame]
bytes percent samples top
---------- ------- ------- ---
164856351600 28.16% 321975 java.lang.Integer[] (out)
164126728800 28.04% 320550 int[] (out)
129803414088 22.17% 11353 int[]
126622144704 21.63% 11063 java.lang.Integer[]
4802496 0.00% 1 java.lang.String
可以看出各种类型的对象生成量,并且可以看到是从什么路径生成的.