Java性能优化——性能优化概念

1,738 阅读8分钟

概述

Java性能优化个人觉得是Java进阶的必经之路。很多Java工程师对于执行代码后,底层运行的Java虚拟机可能一知半解。Java相比C/C++最大的区别是,少了内存管理。让工程师可以专注于应用主体逻辑,而不用去管理内存的使用,但这是一把双刃剑,如果让程序达到最佳的性能,是Java性能优化的初衷。

性能调优

关于Java性能调优或者是关于性能调优,有诸多的影响因素,JVM只占了整体性能的一小部分。不单单是Java虚拟机本身,还有数据库连接,网络开销等等。我们今天就专注于应用本身。

更好的算法

了解数据结构和算法的人,是非常清楚一个好的算法和一个差的算法的差距是多么大。对于应用编码本身也是一样。好方法的封装,往往可以节省更多的内存空间和时间,并且对于异常处理很到位,而不是像刚接触编程语言的小白,目的是一股脑先实现具体的功能。对于应用的编码要具有工匠精神,保证可读性的基础上去追求更好的性能。

更少的代码

同上一节中所讲述的,同样的一个功能,更少的代码的小程序会比大程序的运行速度要快,需要编译的代码越多,等待程序启动所耗费的时间就越长,要创建和销毁的对象越多,垃圾收集的工作量就越大;要分配和持有的对象越多,GC的周期就越长;要从磁盘装载进JVM的类越多,程序启动所花费的时间就越长;要执行的代码越多,机器硬件缓存的效率就越低;而执行的代码越多,花费的时间就越长。
同样的,当引入的小功能,造成性能的小衰减,但是当应用越来越大时,累计的小衰减,会积少成多造成应用性能的显著下降。

过早优化

原话是“我们不应该把大量时间都耗费在那些小的性能改进上;过早考虑优化是所有噩梦的根源”,在应用的功能初期就需要编写可读性高,结构清晰的代码。这里所指的过早优化,并不包括避免那些已经知道对性能不好的代码结构。每行代码,如果有两种简单、直接的编程方式,那就应该选择性能更好的那种。

性能优化原则

  • 借助性能分析来优化代码,重点关注性能分析中最耗时的操作。然而请注意,这并不意味着只看性能分析中的叶子方法(参见第3章)
  • 利用奥卡姆剃刀原则诊断性能问题。性能问题最可能的原因应该是最容易解释的:新代码比机器配置更可能引入性能问题,而机器配置比JVM或者操作系统的bug更容易引入性能问题。
  • 为应用中最常用的操作编写简单算法。以估算数学公式的程序为例,用户可以决定他所期望的最大容许误差为10%或1%。如果10%的误差适合多数用户,那么优化代码就意味着即便误差范围缩小为1%,但是速度变慢了

性能测试方法

测试真实应用

微基准测试

微基准测试用来测量微小代码单元的性能,包括调用同步方法的用时与非同步方法的用时比较,创建线程的代价与使用线程池的代价,执行某种算法的耗时与其替代实现的耗时,等等

  • 必须使用被测结果,现代编译器会对代码进行智能优化,对于没有使用的结果会被去除,无论数据量多大,导致流逝的时间相差无几。可以将对应的局部变量定义为实例变量,并用volatile声明,就可以测试方法的性能了。
  • 去除无关操作。将方法使用的无关操作去除,例如生成随机数进行运算,需要在测试方法前,将随机数计算好。
  • 使用合理参数。对于被测试的方法,制定对应的合理参数的区间。

宏基准测试

宏基准测试则是测试应用整体的性能,包括负载均衡,应用主体,数据库性能,网络开销等。测试应用整体性能时分别进行每个模块的单独测试,举个例子比如测试应用主体的性能时,伪装成对应的数据库连接操作,忽略数据库连接性能的影响。如果是模块化的模型,数据进入子系统的速率取决于前一个模块或系统的输出速率。比如数据库只能以100 RPS的速率装载数据。虽然向数据库发送请求的速率为200RPS,输出到其他模块的速率却只有100 RPS。即便业务逻辑处理的效率加倍,系统整体的吞吐量仍然只能达到100 RPS。所以,除非花时间改善环境其他方面的效率,否则业务逻辑做再多改进也是无效的。
先进行整体应用测试,确认各个模块的性能优化的优先级。

介基准测试

介基准测试用于测试应用的某方面的性能,例如Socke管理,读取请求,查找JSP,写入响应等代码,没有安全管理,没有会话管理,也不使用其他大量的Java EE特性,介基准测试只是一个实际应用的子集。介基准测试相对于微基准测试相比隐患更少,又比宏基准测试容易,介基准测试不包含会被编译器优化的大量死代码。

测试指标

批处理流逝时间

测试一个应用流程最简单的方法。就是看处理这个任务花费了多少时间。在Java应用中,由于即时编译(JIT),JVM会花几分钟(或更长时间)全面优化代码并以最高性能执行。由于这个(以及其他)原因进行Java的性能优化,需要密切注意代码优化的热身期:应该在运行代码执行足够长时间,已经编译并优化之后再测量性能。

吞吐量

吞吐量测试是基于一段时间内所能完成的工作量,客户端会报告它所完成的操作总量。吞吐量就是客户端(包括多线程处理)所完成的操作总量。这个数字就是每秒完成的操作量,不是测量期间的总操作量。这个指标被称作每秒事务数(TPS)、每秒请求数(RPS)或每秒操作次数(OPS)。客户端-服务端之间的测试存在一定风险:

  • 客户端机器的CPU不足以支持所需数量的客户端线程
  • 客户端需要花大量时间处理响应之后才能发送新的请求

吞吐量之间如何比较,吞吐量相同,那就比较响应时间,如果吞吐量不相同。能够承受500 OPS、响应时间0.5秒的服务器,它的性能要好过响应时间0.3秒但只有400 OPS的服务器。(吞吐量测试也需要在合适的热身期结束之后进行)

响应时间

响应时间是从客户端发送请求至收到响应之间的流逝时间。会尽量模拟用户行为。它与吞吐量测试(假设后者是基于客户端-服务器模式)之间的差别是,响应时间测试中的客户端线程会在操作之间休眠一段时间(为思考时间)。在思考时间固定,指定客户端的数量的情况下,服务器的吞吐量就固定了(有少许差别)。所以对应的响应时间就决定了服务器的效率。

测试客户端包括思考时间时的吞吐量,例如,思考时间为30秒,响应时间为1秒,意味着客户端每31秒发送一个请求,由此可得吞吐量(计算公式为:请求数量/(响应时间+思考时间))为0.0032 OPS。如果响应时间是2秒,客户端就是每32秒发送一个请求,吞吐量就是0.031 OPS。但是如果使用固定周期请求(例如30秒)的话,无论响应时间多少,都会产生固定的吞吐量,每个客户端0.033 OPS(假设本例中的响应时间都少于30秒)。

衡量响应时间有两种方式:

  • 响应时间的平均值,请求时间的总和除以请求数。平均值容易收到离群值的影响,导致数据不准确,例如GC(垃圾收集)引入的停顿时间,引入较大的离群值。
  • 响应时间百分位值,例如第90百分位响应时间。如果90%的请求响应小于1.5秒,且10%的请求响应不小于1.5秒,则1.5秒就是第90百分位响应时间。

响应时间的衡量可以结合平均值和至少一种百分位值进行衡量。

负载生成器

Faban是一个开源的、基于Java的负载生成器。Faban带有一个简单程序(fhb),可用来测试简单URL的性能,具体使用可以参考官网的方式。

参考资料

Java性能权威指南