关于java:性能优化必备火焰图

4次阅读

共计 5286 个字符,预计需要花费 14 分钟才能阅读完成。

引言

本文次要介绍火焰图及应用技巧,学习如何应用火焰图疾速定位软件的性能卡点。
联合最佳实际实战案例,帮忙读者加粗浅的了解火焰图结构及原理,了解 CPU 耗时,定位性能瓶颈。

背景

以后现状

假如没有火焰图,你是怎么调优程序代码的呢?让咱们来捋一下。

1. 性能开关法

想当年我刚工作,还是一个技术小白时,排查问题只能靠玄学,大抵能猜出问题可能是由某个性能代码导致的,此时的排查伎俩就是删除多余的性能代码,而后再运行查看 CPU 耗费,确定问题。(至今我工作时还会发现一些老人应用如此办法调试性能。)

public void demo() {if (敞开 1) {
        // 性能 1
        handle1();}

    if (敞开 2) {
        // 性能 2
        handle2();}

    if (关上 3) {
        // 性能 3
        handle3();}

    // 性能 4
    handle4();}

此法全靠“教训”和“运气”,而且改变了代码构造,假如这是一个曾经通过测试的集成区代码,此时须要批改代码性能来调试程序是十分危险的一件事,当然有 Git 仓库能够“一键还原”,然而,是人操作,总归会有失手的时候,且定位效率太低

2. StopWatch 埋点法

当程序呈现性能问题时,且不确定是哪一段代码导致耗时,能够借助办法耗时来判断,此时咱们只有在调用办法前后追加执行所需耗时日志,即可断定到底是哪个办法最耗时。

public void demo() {Stopwatch stopwatch = Stopwatch.createStarted();
    handle1();
    log.info("method handle1 cost: {} ms", 
             stopwatch.elapsed(TimeUnit.MILLISECONDS));
    
    handle2();
    log.info("method handle2 cost: {} ms", 
             stopwatch.elapsed(TimeUnit.MILLISECONDS));
    
    handle3();
    log.info("method handle3 cost: {} ms", 
             stopwatch.elapsed(TimeUnit.MILLISECONDS));
    
    handle4();
    log.info("method handle4 cost: {} ms", 
             stopwatch.stop().elapsed(TimeUnit.MILLISECONDS));
}

此法较上一个办法的劣势是,不扭转代码的逻辑状况下,只是加强了一些观测点位,由办法的耗时来定位性能瓶颈。然而,假如办法的解决调用栈很深,就不得不在子办法中再次埋点,此时断定流程即为:埋点 -> 发版 -> 定位 -> 埋点 -> 发版 -> 定位 -> ……. 且实质上也是改了代码,就有出错的可能。心累,不高效!

3. TOP 命令定位热线程

个别企业的软件服务都是部署在 Linux 操作系统上,有教训的新手排查性能最不便的方法就是 top 定位。

top -p pid -H

显著看到,pid 103 耗费了 40% 的 CPU, 找到对应的 stack 线程信息如下(疏忽查找方法,我假如你曾经会了:)):

此时能够得出结论,以后最耗 CPU 的线程是写入磁盘文件,追究代码最终会定位到是因为在高并发场景下打了大量的 INFO 日志,导致磁盘写入成为瓶颈。

总结:TOP 命令对于找 CPU 性能瓶颈时很无效的,然而存在如下几个问题:

  • 排名最前的肯定是过后最耗费 CPU 的,但不肯定是程序性能的诱因。例如因某个 BUG 导致打印了大量 ERROR 日志,最终 LOG 到磁盘是最耗费 CPU 的,但罪魁祸首不是它。
  • TOP 注定使你只会关注最高的,等你修复最耗 CPU 的问题后,往往还会遇到别的程序问题导致 CPU 偏高,即一次只能看到一个问题,看不到全貌。
  • 文本的表现力十分无限:首先你得对 Linux 及 JVM 命令十分相熟,其次文本对两个及以上值做关联性剖析时,就顾此失彼了,此时就迫切的须要另一种剖析工具——图。

什么是火焰图

火焰图(Flame Graphs),因其形似火焰而得名。

如上就是一个典型的火焰图,它由各种大小 / 色彩的方块组成,每个方块外部还标识了文字,整个图片顶部凹凸不平,形似一簇簇“火苗”,因而得名火焰图。
火焰图是 SVG 生成,因而能够与用户互动,鼠标悬浮在某个方块时,会具体展现外部文字。点击后,即会以以后被点击方块为底向上开展。

特色
应用火焰图剖析之前,咱们得首先理解火焰图的根本结构

  • 每一列代表一个调用栈,每一格代表一个被调用函数
  • 方块上的字符标识调用办法,数字示意以后采样呈现次数
  • Y 轴示意调用栈深度,X 轴将多个调用栈归并,并首字母排序展现
  • X 轴宽度示意采样数据中呈现频次,即宽度越大,导致性能瓶颈的起因可能就越大(留神:是可能,不是确定
  • 色彩没什么意义,随机调配(可能创始人想让你看起来更像一个火焰。。)

    火焰图能够做什么

    那此时你曾经晓得了火焰图,如何定位软件问题呢?咱们须要一套寻找性能瓶颈的方法论。
    能够明确的是 CPU 耗费高的口径

    CPU 耗费高的口径 = 调用栈呈现频率最高的肯定是吃 CPU 的

如上咱们曾经晓得了火焰图的结构,及“物料”含意,此时咱们的关注点应该在方形的宽度上,方形的宽度大小代表了该调用栈在整个抽样历史中呈现的次数。次数意味着频率,即呈现次数越多的即可能最耗费 CPU。

但只关注最长的是没用的,如底部的 root 和中部的方块都很宽,只能阐明这些办法是“入口办法”,即每次发动调用都会通过的办法。
咱们更应该关注火焰山顶部的 ”平顶山 “(plateaus) 呈现的次数多,即没有子调用,抽样呈现的频率高,阐明执行办法的工夫较长,或者执行频率太高(如长轮询),即 CPU 大部分执行都调配给了“平顶山”,它才是性能瓶颈的根因。

总结方法论:火焰图看“平顶山”,山顶的函数可能存在性能问题!

最佳实际

实际是测验真谛的唯一标准!上面我将以一个小的 Demo 来展现如何定位程序性能问题,加深对火焰图应用的了解。

Demo 程序如下:

public class Demo {public static void main(String[] args) throws InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(20);

        while (true) {executorService.submit(Demo::handle1);
            executorService.submit(Demo::handle2);
            executorService.submit(Demo::handle3);
            executorService.submit(Demo::handle4);
        }
    }

    @SneakyThrows
    private static void handle4() {Thread.sleep(1000);
    }

    @SneakyThrows
    private static void handle2() {Thread.sleep(50);
    }

    @SneakyThrows
    private static void handle3() {Thread.sleep(100);
    }

    @SneakyThrows
    private static void handle1() {Thread.sleep(50);
    }
}

代码很简略,当然事实中也不会这么写,次要是配合上演。。
次要是开了一个线程池,且别离执行四个 task,不同的 task 耗时不统一,此时咱们的性能瓶颈在 handle4 这个工作上,在晓得论断的前提下,咱们比拟看火焰图得出答案的是否合乎预期!

1. JVM 堆栈信息拉取

以后我是在本人的 Mac 上运行的程序,idea 执行这一段程序十分便捷,那如何获取以后运行 main 函数的 PID?
此时须要用到 TOP 命令,下面是个 while 死循环,很显著吃 CPU 最厉害,只有找到归属 Java 线程的最高一个 PID 即为所求。

很显著失去 COMMAND = java 最高的 PID = 20552
此时执行如下命令获取堆栈信息,并写入 tmp.txt 文件

jstack -l 20552 > tmp.txt

2. 生成火焰图

生成火焰图的工具有很多,我个别会借助 FastThread,在线剖析堆栈,十分不便,同时反对生成火焰图,不便咱们定位问题

关上官网首页,抉择刚刚 dump 的堆栈文件,点击 Analyze,此时只须要期待网站剖析好后(失常 3~5 s),即可查看火焰图

fastThread 网站剖析报告十分丰盛,个别的问题咱们间接通过它给出的论断根本能定位到问题了,本文暂且无需关注,感兴趣的话,后续我会分享,间接拉到 Flame Graph 子标题处

此时显著能看出 4 个“平顶山”,且 com.Demo.handle4 宽度最大,com.Demo.handle3 次之,合乎预期!

原理分析

基于上述小 Demo,咱们深刻了解下火焰图的生成原理。

举个例子,便于你了解,假如咱们要观测一个人在忙些什么,哪些事最占用他的工夫,会怎么做?
从工夫维度的话,且不思考老本的话,我必定安顿一个监控摄像头,全天候 24h,360 度监控他,而后再安顿人员,逐帧排查,并汇总他所做的事,得出:睡觉 8h,工作 8h,玩手机 4h,吃饭 2h,其它 2h。从而得出结论:睡觉占用他工夫最多。

由上能够总结一套剖析流程:

记录(监控)-> 剖析 & 归并(逐帧排查)->  Top N -> 得出结论

带着流程去看咱们应该如何排查 CPU 在执行中,哪些事(过程 / 线程)最占用它的工夫呢?
简略粗犷的办法是每时每刻都记录执行的办法堆栈,再汇总归并,得出最耗时的办法栈在哪。此法的问题在于

  • 数据量大
  • 工夫长

其实只有采样去观测 CPU 在干什么就好了,这是一个概率学问题,如果 CPU 因为执行某个办法耗时,大概率采样下来,失去的归并后果也是最多的,尽管有误差,然而屡次统计下,差不了多少的。
同理,dump 下的堆栈,查看大多数线程在干什么,根据堆栈内每个办法呈现的频率聚合,呈现的频次最多的就是以后 CPU 调配执行最多的办法。

"pool-1-thread-18" #28 prio=5 os_prio=31 tid=0x00007f9a8d4c0000 nid=0x8d03 sleeping[0x000000030be59000]
    java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at com.Demo.handle2(Demo.java:31)
    at com.Demo$$Lambda$2/1277181601.run(Unknown Source)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

    Locked ownable synchronizers:
- <0x00000006c6921ac0> (a java.util.concurrent.ThreadPoolExecutor$Worker)

至于咱们的 jstack 信息如何被解决成火焰图的格局,社区曾经为常见的 dump 格局都提供了工具,stackcollapse-jstack.pl 解决 jstack 输入。

Example input:

"MyProg" #273 daemon prio=9 os_prio=0 tid=0x00007f273c038800 nid=0xe3c runnable [0x00007f28a30f2000]
    java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.read(SocketInputStream.java:121)
        ...
        at java.lang.Thread.run(Thread.java:744)

Example output:

MyProg;java.lang.Thread.run;java.net.SocketInputStream.read;java.net.SocketInputStream.socketRead0 1

总结 & 瞻望

火焰图的介绍到此结束,置信你又多了一种排查问题的伎俩!
存在即正当,工具之开发重要性而言不用多说,我始终持容纳态度面对新事物,它确确实实解决了某些痛点而怀才不遇的。
后续我会介绍更多排查问题的伎俩,如果你喜爱本文格调,请关注或留言,欢送探讨!

正文完
 0