提速10倍!- 使用JVisualVM优化你的Java、Android程序


作者参于过一个Android项目由近100个程序员开发了3年时间,基本上是把一个服务器做到手机里,当时还是单核时代,整个项目最大的挑战就是性能问题,其间关于框架是不是性能瓶颈有过激烈的争论。本人负责性能优化时使用创新性的方法借助Android的工具TraceView解决了如下问题。度量程序中框架与每一个模块各自的计算资源消耗是多少?性能瓶颈在哪里?然后进行有针对性的优化并最终数倍提升了程序的性能,这种在Android上得来的经验最终可以推广到普通的Java程序中来。作者的一个结论是任何没有经过系统性优化的Java程序,使用本文中的方法最少可以提速一倍,最高可至10倍。

性能优化的方法论

关于优化,本文会给出一些大的方向,但不讨论如何具体的优化某一API,模块之类的问题,也不讨论任何算法。本文的重点在于如何发现性能瓶颈,以量化的方式告诉你慢在哪里和为什么慢,一但有了这些数据,接下来的具体优化和效果的度量就是不是问题了。

从方法论上来说,性能优化一般是在有了性能问题的时候才进行,千万不要怀疑某个地方有问题,然后就进行优化!建立在怀疑基础上的工作很可能是无用功。笔者曾见过某创业公司的CTO给Android的Activity写了一个Stack,不显示的Activity会自动被销毁,而他这样做的原因是要节省内存。错了,全错了。孰不知Android系统会自动管理Activity的生命周期和内存,该优化重复发明轮子不说,还会使得Activity被反复销毁和创建,这对客户端和Server都是更大的销耗。

在确定应用程序很慢,一定要优化之后的第一件事情,就是想办法度量整个程序的CPU、IO和内存的销耗。有人会说这很简单嘛,在几个节点上打几个时间戳,写点日志不就行了。错错错,由于程序的执行分支很多,在不同Load下的表现也不同,打时间戳的方式是只能见树木,不能见森林呀。比如说我有如下的问题:Sping框架在整个程序中消耗多少百分比的CPU?或者在我们的模块中哪个模块是最慢的,有多慢?我要优化哪些地方可以使程序快50%?这些问题怎么回答?本文中将给出答案。

本文推荐的优化大体的流程是度量->优化->度量这样一次次循环。性能优化的最高标准是将其固化到开发的流程中,比如说在每一次Scrum的结尾做一次优化,或者像作者当年那样,将性能度量做到集成测试里,每天都会自动运行并报警。这个最高级别可以称为性能的持续优化级。

如何度量

终于轮到JVisualVM出场了(Android使用TraceView,方法是一样的,本文不详述。),它是Oracle JDK自带的图形化工具,与Android提供的TraceView有类似之处,可以度量VM上每一个方法级别的CPU、内存消耗。然面除此之外,最重要的是它可以累计每一个package在一段时间内的CPU消耗,而我们的Java程序中的模块一定是按package来划分的(不是的拉出去毙了)。比如说上文中提出的度量Spring框架消耗的问题,其实质就是统计出所有包名中含有Sping的方法的总销耗。JVisualVM可以让我们以不同的维度来测量消耗。JVisulaVM还有很多其它的功能,如死锁检测(ThreadDump)等,不再本文范围内。

如何用JVisualVM度量模块的消耗

JVisualVM的基本用法是要先连接到JVM上,具体用法谷歌上有的是,笔者为了节省自己的宝贵时间就略过去了。好了,现在在谷歌老师的指导下,你的JvisulVM已经连到Tomcat的VM上了,然后你看到了下面的UI。

这张图里你要先点击CPU Button,然后JVisualVM就已经开始统计VM上Tomcat的运行数据了,在运行一定时间后点击Stop就可生成这段时间内的所有的性能数据。

接下来这张图就是很Tricky的地方。

在这张图的右边你可以Filter想要分析或不想要分析的Java包。 左边有一个很小的Snapshot图标,点击后才能进入性能数据的分析和展示页面。 进来后就是下面的界面。

我们看到的是默认以Methods来排序的CPU消耗图,这里又有一个藏的很深的功能-以Package来排序,JVisualVM可以自动按包来计算时间并排序。这样我们就知道每个模块的消耗了(比如DAO层/Services层等等)。

除此之外,我们还可以看到下部有不同Sheet的标签,如可以查看某个方法的CallTree之类,方便对调用源头的追踪。下图是其中一种分析图。

经过上面的分析,你就可以明确优化方向和确定优化计划了。

如何优化

前文说过了不讲优化细节,是因为面实在太广了。但是以笔者的经验来说,按大类分无非是是以下几种

  1. 以空间换时间 - 如缓存之类
  2. 以异步代替同步 - IO之类
  3. 算法优化 - Bloom Filter大幅减少数据库查询
  4. 对象重用 - 对象池
  5. 以多线程代替单线程 - 线程池
  6. 减少对象的生成和销毁

特别说下最后一点,大家看看这行语句有什么问题? 怎么改进?

log.debug("Current time is: " + new Date());

上面的语句,即使你把Log Level改到了Info级别Date对象还是会生成,然后马上就出了作用域要被回收!很抱歉Log4J就是这么没效率。如果是LogBack之类是这样的:

log.debug("Current time is: ", new Date());

实测单是这一个改进就可能收获20%的速度提升!

附记

更进一步分析

和Android平台的TraceView相比JVisualVM还是不够强大。TraceView可以使用工具导出原始数据为CSV格式,笔者基本上都是将原始数据导出后放到数据库里用SQL分析,这样可以随心所欲。 TraceView可以可视化线程的CPU使用情况,JVisualVM只能看当前线程的状态。 JVM下有些和VisualVM类似的第三方工具似乎可以导出数据。


Jim - 程序员,近10年工作经验集中在Java, Android, C++,现就职于上海。
Published under (CC) BY-NC-SA in categories Common Tec  tagged with Java