Java性能优化指南系列(一):概述和性能测试方法

时间:2022-07-09 05:56:43
  • Java性能分析是一门艺术和科学;科学指的是性能分析一般都包括大量的数字、测量和分析。绝大多数的性能工程师都有科学背景,运用科学的严谨是获取最大性能的重要组成部分。艺术部分指的是什么呢?性能调优是部分科学部分艺术的观点是很早就有的,但是关于性能的主题很少会给定特定的知识,这就是艺术的部分了,它和我们平常接受到的培训是不一样的,培训是确定了的。还有部分原因是对于某些人来说,性能调优是建立在深入的知识和经验上面的。这里艺术就是知识、经验直觉的使用。
  • 这本书不能帮助我们提升经验直觉,但是可以帮助我们提升对知识的深入理解,我们持有这样的观点:多次运用知识能够提升我们称为Java性能工程师的能力。这本书的目标就是给予我们对Java平台性能方面的深入理解。
  • 这种知识主要分为两大部分:一是JVM本身的性能,研究JVM的配置是如何影响程序的各方面性能的。其它语言的有经验的开发人员会发现这个调优是比较繁琐的,尽管实时上JVM的调优和C++程序员在编译的时候测试和选择不同编译器参数或者是PHP程序员在php.ini文件设置合适的变量是一样的。二是理解Java平台的特性是如何影响性能的。注意这里的平台,一些特性(比如:线程和同步)是语言的一部分,另外一些特性(比如:XML解析性能)是Java的标准API。尽管Java语言和Java API是不同的,但是这里我们把他们看成是类似的。
  • JVM的性能很大程度上是和调优参数相关的,平台的性能则和应用代码的优劣相关。很多时候,代码开发人员和性能测试小组是分割开的,认为他们是不同方面的专家。只有性能工程师能够对JVM虚拟机进行调优,以榨取更多的性能;只有开发人员才会关心代码写的好不好。区分这个是没有什么作用的--任何在Java平台上工作的人都需要了解这些。

平台和约定

  • 本书使用的平台是:Java 7update 40Java 8Java7是进行性能调优的一个好的起点,因为它提供了很多新的特性,比如:G1垃圾回收器,同时还提供了一些和性能相关的工具,便于我们可视化的查看Java应用的工作细节。Java8也做了较大幅度的改进(比如:引入lambda表达式)。
  • 尽管所有平台都会做兼容性测试,以便实现Java规范的功能;但对于本书来说,这里的兼容性是不够的,特别是tuning flag,每个JVM都会实现一个或多个垃圾收集器,但是它们的调优参数一般是不一样的,尽管这本书讨论的很多概念是适用于所有Java的实现平台的,但是对于一些调优参数和建议,那只适用于OracleJVMHotSpot)。

调优参数

  • 除了少部分例外,JVM会接受两种参数:boolean参数和带有参数的参数。Boolean参数使用-XX:+FlagName来开启参数,使用-XX:-FlagName来关闭参数。带有参数的参数,使用-XX:FlagName=something来设置参数值。当介绍某个参数的时候都会讨论它的默认值。这个默认值是不同方面的组合,比如:当前JVM运行在什么操作系统上面以及其它JVM命令行参数是什么。如果有疑问,可以使用-XX:+PrintFlagsFinal来查看在特定平台上面,给定JVM命令行参数会给其它参数什么默认值。对于这些会根据其它参数和平台来自动选定调优参数的过程,我们叫做ergonomics(不知道怎么翻译)。
  • Client classServer class:当JVM运行在32-bitwindows服务器上(不管有多少个CPU),或机器只有一个CPU,那么这台机器是Client class。其它机器都称之为server class

性能

  • 写更好的算法
  • 写更少的代码:需要编译的代码越多,代码运行快需要更多的时间;分配和丢弃的对象越多,GC需要更多的工作;分配和回收更多的对象,GC周期会更长;加载的类越多,程序启动需要更长的时间;执行的代码越多,命中硬件缓存的可能性越小;执行的代码越多,需要的时间越长。
  • 不要过早优化
  • 数据库(或其它第三方资源)常常是瓶颈

  • 针对一般情况进行优化1)对代码进行分析,并关注分析中消耗时间最长的部分,这也不是说只关注分析中的方法 2)使用奥卡姆剃刀定律来诊断性能问题,最新加入的代码比系统配置更有可能引起性能问题,而系统配置比JVM或操作系统的bug更有可能引起性能瓶颈。一个测试用例很有可能会发现一个潜在的性能问题,但是我们不会立马就去优化,而要看看这个测试用例是否是经常发生的。3)使用更简单的算法来完成大多数的工作。
  • 测试真正应用

    第一个原则就是性能测试必须在即将上线的产品上进行测试。

    Microbenchmarks

    • Microbenchmarks是为了衡量非常小的代码单元的性能而设计的测试。比如:使用Synchronized和不使用Synchronized的方法;使用线程池和不使用线程池的开销;使用算术算法和另外一种实现的时间比较等等。Microbenchmarks写的很准确是非常难的。比如:

    public voiddoTest(){

                    // Main Loop

                    double l;

                    long then= System.currentTimeMillis();

                    11

                    for(int i= 0; i< nLoops; i++) {

                    l=fibImpl1(50);

                    }

                    long now= System.currentTimeMillis();

                    System.out.println("Elapsedtime: " + (now- then));

            }

            ...

            privatedoublefibImpl1(int n) {

                    if(n<0)throw newIllegalArgumentException("Must be > 0");

                    if(n==0)return0d;

                    if(n==1)return1d;

                    double d=fibImpl1(n-2) +fibImpl(n-1);

                    if(Double.isInfinite(d))throw new ArithmeticException("Overflow");

                    return d;

    }

     上面的程序存在几个问题,第一,因为变量'l'没有使用,所以编译器会优化掉调用fibImpl1的所有代码,所以实际执行的就是打印时间的代码,根本测试不到fibImpl1的性能。第二,因为我们的目的是得到第50Fibonacci的值,对于比较智能的编译器,可能会去除循环。第三,必须要对正确的输入进行测试,因为在实际运行的时候,输入一般都是正确的。

    注意Java代码有一个特征就是:执行的次数越多,它执行得越快(所谓:warm-up period)。所以在进行microbenchmark的时候需要包含warm-up period,以便给予编译器产生优化代码的机会。

     

    • 还有一点要清楚的是:额外编译效益(Complilation effects。编译器会使用分析回馈(profile feedback),编译使用代码的分析回馈来决定在编译一个方法的时候使用的最佳优化手段。分析回馈profile feedback)是基于:方法频繁调用的程度、它们被调用时的栈深度以及它们参数的真正类型等等----一句话就是基于方法代码运行的环境。编译器对代码的优化,在microbenchmark时候比在代码运行在应用时更加频繁。如果使用同样的microbenchmark类来评测第二个Fibonacci的实现方法,那么各种编译效应(Complilation effects)都有可能会发生,特别是两个Fibonacci实现分别在不同的类中。
    • 最后要说明的是mircrobenchmark真正意味着什么?在benchmark中,总时间(比如上面讨论的Fibonacci)可能是循环很多次得到的,一般以秒为单位;但是对于每一次循环的时间,可能是以纳秒为单位。是的,随着纳秒的增加,逐步会变成一个性能瓶颈。但是,如果被测试的函数执行的次数很少,那么在纳秒级别进行优化就没有意义了。

    Macrobenchmarks

    用于测量一个应用的性能的best thing(最佳东西?)就是应用本身,并结合它使用任何外部资源。如果应用调用了LDAPAPI来确认用户的身份,那么测试的时候就会考虑LDAP调用。撇开LDAP进行测试对于模块测试是有意义的,但是对于性能测试,必须要考虑到LDAP调用。

    随着应用的增长,满足上面说的准则(使用应用本身并结合它使用的外部资源进行测试)会变得更加重要,但是也变得更加困难。复杂系统比组成它的各个部分之和更加复杂,当把各个部分组合在一起的时候,它们的行为会变得非常不一样。比如:如果我们mock了数据库,那就意味着我们不用关注数据库的性能了。但是,在真正运行的时候,数据库连接会消耗很多heap space来存放它们的缓存数据;网络会变得更加繁忙,因为传输数据会增加;代码的优化会变得非常不同(简单的代码和复杂的代码)。CPUpipelineCache在较短的代码路径上比在较长的代码路径上更高效等等。另外一些原因就是资源的分配。在理想环境下,对应用中的每一行代码都有足够的时间进行优化,但是在现实环境中,对优化消耗的时间是有要求的,并且只优化复杂环境下的一部分也许不会立马得到很好的效果。考虑下图中,用户发送数据到系统,首先确认用户权限,然后做一些业务相关的计算,然后从数据库中加载所需的数据,再做一些业务相关的计算,并把一些数据存入数据库,最后向用户发送响应。下图中的每个框都是一个小模块,框中小括号部分是该模块最大处理的并发数。

    Java性能优化指南系列(一):概述和性能测试方法Java性能优化指南系列(一):概述和性能测试方法
  • 从业务的角度来看,业务相关的计算是最重要的,这也是整个系统的目的;但是在上面的例子中,将它们的处理速度提升1倍是没有意义的(因为LoadDataRPS只有100)。任何应用(包括:单机JVM)都可以模块化为上面这样的一个一个步骤,每个步骤都以一定的速率向下一个步骤传输数据。

    小贴士:如果有多个JVM同时运行在同一台机器上,我们必须要将所有JVM作为整体同时进行测试;因为有可能出现这种情况,单个JVM运行得很好,但是当多个JVM同时运行的时候,一些应用的性能会非常不同,比如:一些应用在GC的时候会占用比较多的CPU,当它在独立运行的时候没有问题,但是如果有其它应用一起运行的时候,它就可能得不到充分的CPU,导致运行性能下降。这就是为什么我们要对应用进行整体测试的一个理由。

    Mesobenchmarks

    • 对于JAVA SEJAVA EE都有一系列称之为microbenchmark的测试;对于Java SE工程师来说,microbenchmark意味着非常小的测试单位(比上文中的Fibonacci还要小);而Java EE工程师,microbenchmark意味着性能测试的某个方面(仍然需要执行比较多的代码),举个例子:从应用服务器的某个JSP返回结果有多快;但是在这个过程中有非常多的操作,比如:socket管理的代码,请求处理等等,这个和Java SE里面的microbenchmark测试是不一样的。但是这个测试也不是Macrobenchmark,因为它没有登陆,没有会话管理,没有使用Java EE的其它特性,我们称之为Mesobenchmark

    代码样例

    • 本书中的样例都是基于下面这样一个应用程序:它用于计算一定时间范围内某只股票的历史最高和最低的价格以及在这段时间内的价格偏差。
    • 类:StockPrice用来存储给定日期的该股票的价格范围。
    Java性能优化指南系列(一):概述和性能测试方法
  • 样例应用就是处理StockPrice的一个集合;这个集合代表这支股票一段时间的历史(1年或25年),所以有下面的接口:
  • Java性能优化指南系列(一):概述和性能测试方法
  • 这个类的基本实现就是从数据库中加载一些列的价格
  • Java性能优化指南系列(一):概述和性能测试方法
  • 注意到:curDate是按天进行增加的。

    • 另外要注意的是这个类的性能和BigDecimal的性能是精密相关的;之所有选择这个类,有两点:1)提升计算的精度 2)对于我们做为例子来说,BigDecimal的计算量有助于增加业务的计算量
    • 下面一个函数是对BIgDemial进行平方根求解(使用巴比伦方法):
    Java性能优化指南系列(一):概述和性能测试方法
  • 这个实现不是最高效的算法,但是这么做是故意的,它可以增加些业务计算的时间。

    • 接口StockPriceHistory的标准方差、平均价格以及直方图实现都会产生新的值。在许多例子当中,这些值或者提前计算出来(在将数据从EntityManager中加载的时候)或者延迟进行计算(等到调用对应的函数)。同样的,接口StockPrice引用了一个StockOptionPrice接口,它用来存放该股票给定日期的可选价格。这些价格也可以提前或延迟进行计算。无论是提前计算和延迟计算,这些接口的定义可以对在不同情况下,将这两种方式进行比较。
    • 这些接口也天然的复合J2EE应用:用户通过访问一个JSP页面,输入股票的代码和起止日期。这个请求会被一个Servlet进行处理(解析参数,访问stateless EJB),得到结果后,将响应转到一个JSP页面上,它负责将数据格式化为HTML页面。
    Java性能优化指南系列(一):概述和性能测试方法
  • 这个类可以注入不同的history bean的实现(提前计算或延迟计算);它还可以缓存数据(或不缓存),这个对于一个企业应用是很平常的事情。

    理解吞吐量、处理时间(批量操作)和响应时间

    处理时间(批量操作)

    • 测试性能的一个最简单的方式就是看看应用在多长时间内能够完成某个任务。比如:获取10000只股票的25年内的历史纪录,并计算这些价格的标准方差;生成某个公司5万名员工的工资报告;执行1百万次等等
    • 对于非Java程序,这些测试都是很显然的,写好程序,然后进行执行并评估时间。但是对于java来说,就不是这样了,因为它有JITJava即时编译器)。这个过程会在第4章进行介绍,简单来说就是Java代码需要几分钟(或更长)的时间来达到最优化,这个时候代码执行的性能才是最高的。因此,Java性能的研究非常关注warm-up period,性能测试通常在代码执行了足够长的时间后才启动测试,因为这个时候的代码才是最优化的。
    • 注意warm-up period通常是指JIT编译和优化代码的时间,但是还存在其它因素会影响warm-up period时间的长短的。比如:JPA通常会缓存数据;操作系统也会对文件进行缓存等等。
    • 但是另一方面,通常情况下应用的性能关注的是从启动到结束的时间,用户不会关注你的warm-upperiod的时间是多长。

    吞吐量

    • 吞吐量的测量是指在给定时间内,应用可以完成的工作量。一般情况下,测试吞吐量都需要一个客户端不停的发送请求给服务端,但并不是所有吞吐量测试都需要这样;对于独立程序测试吞吐量就像测试程序处理时间一样简单。
    • 吞吐量可以使用TPSRPSOPS来表示。
    • 所有client-server测试的运行都存在一个风险:客户端不能足够快地发送数据到服务端。吞吐量测试比响应时间测试需要更少的client线程,因为吞吐量测试基本没有什么业务逻辑
    • 吞吐量测试常常需要经过warm-up period时间后进行,特别是当被测量的应用工作集不是固定的(不知道什么含义?)

    响应时间

    • 响应时间是指请求从发送到接收到响应经历的时长。响应时间测试和吞吐量测试的区别是响应时间测试的客户端线程在操作之间会睡眠一段时间。这段时间,我们称之为:think time。响应时间测试更加接近于模拟用户的行为。
    • 响应时间考虑进测试当中,吞吐量就变得固定:给定数目的客户端使用给定think time来执行请求,通常会产生同样的TPS(当然,肯定有细微的变化)。在这个时候,请求的响应时间是更重要的测量目标:服务端的效率是通过它对固定数目的负载响应有多快来衡量的。

    小贴士响应时间和吞吐量:使用包含think time的客户端来进行吞吐量测试有两种方式。最简单的方式是让客户端在请求之间睡眠一段时间:

    Java性能优化指南系列(一):概述和性能测试方法
  • 在这种情况下,吞吐量在一定程度上依赖于响应时间。如果响应时间是1秒,那么客户端每31秒发送一个请求,因此吞吐量为0.032OPS;如果响应时间是2秒,那么客户端每32秒发送一个请求,吞吐量变为0.031OPS

    另外一种方式是使用cycle time(而不是think time)。Cycle time设置请求之间的总时间为30秒,因此客户端睡眠的时间依赖于响应时间。

    Java性能优化指南系列(一):概述和性能测试方法
  • 这使得吞吐量固定为0.033OPS,而不管响应时间是多少(假定每个请求的响应时间都小于30秒)。在测试工具中think time常常是变化的,它们的平均时间大致是某个值,但是为了更好地模拟用户行为,每个请求的hink time是随机的。除了这个原因,线程调度也不可能是实时的,因此请求之间的真实时间都是有些不同的。因此,即使使用提供cycle time的工具,测试多次,每次测试的吞吐量也是会有所不同的。

    • 这里有两种不同的方式来测试响应时间。1)平均时间:将每个请求的时间加在一起,除以请求的数目 2)百分点请求(percentile request),比如:90%请求的响应时间。如果90%的请求响应时间少于1.5秒,10%的请求响应时间大于1.5秒,那么1.5秒就是90%的请求响应时间。
    • 上面两种方式的一个区别是极值对平均值计算的影响:因为它们被包含在平均值的计算当中,更大的极值对平均响应时间的计算影响更大。

    下图中20个请求,它们的响应时间分布在不同的范围当中。其中下面的黑粗线是平均响应时间为2.35秒;第一个黑粗线是90%的请求响应时间,这个值是4秒。

    Java性能优化指南系列(一):概述和性能测试方法
  • Java性能优化指南系列(一):概述和性能测试方法
  • 而上图中,90%的请求响应时间为1秒,但是平均响应时间却为6秒,这就是极值对响应时间产生了巨大的影响。

    • 尽管上面的极值在实际情况中比较少见,但是对于Java应用却是比较容易发生的,因为GC引入了pause time。(这里不是说GC会导致100秒的pause time,而是在测试响应时间很小的应用时,pause time变成了极值)。在性能测试的时候,我们通常关注90%的请求响应时间(当然95%99%都是可以)。如果我们只能关注其中一个,百分比请求响应时间是更好的选择,因为百分比请求响应时间更小对绝大多数用户都是有好处的。但是最好两个都要看,请求平均响应时间和至少一个百分比响应时间,以便我们不会丢失有比较大极值的情况(以便发现问题)。
    • 负载生成器:有很多开源和商用的负载生成工具。本书使用Faban,一个开源的,基于Java的负载生成器,它可以用来简单测试一个URL的性能,比如:
    Java性能优化指南系列(一):概述和性能测试方法Java性能优化指南系列(一):概述和性能测试方法
  • 上面的例子,使用25个客户端(-c)产生请求到stock servlet上面(SDO来指示);每个请求有1秒的Cycle time-w 1000)。这个测试有300秒的warm-up period,然后是5分钟的测试,然后是1分钟的ramp-downperiod -r 300/300/60)。

    理解波动性

    • 第三个原则就是理解每次测试的结果是如何每次都不一样的。处理同样数据集的程序每次都会产生不一样的结果。服务器上的后台进程会影响应用,网络或多或少地会和正在运行的程序进行CPU的竞争等等。一个好的测试方案不会每次让服务端都处理相同的数据集,它们应该产生随机的数据集以便更好地模拟现实情况。但这个会导致一个问题,当对多个测试结果进行比较的时候,是回归了,还是因为测试中各种变化因素导致的?
    • 这个问题可以通过多次测试,然后对结果求平均值来解决。但是问题并不是这么简单,要理解两次测试结果的不同是由于回归还是因为测试中变化因素导致的,这个是非常困难的。