Java性能权威指南-总结19
- Java EE性能调优
- JVM线程调优
- 调节线程栈大小
- 偏向锁
- 自旋锁
- 线程优先级
- 小结
- Java EE性能调优
- Web容器的基本性能
Java EE性能调优
JVM线程调优
JVM的某些调优策略可以影响线程和同步的性能。
调节线程栈大小
当空间非常珍贵时,可以调节线程所用的内存。每个线程都有一个原生栈,操作系统用它来保存该线程的调用栈信息(比如,main()
方法调用了calculate()
方法,而calculate()
方法又调用了add()
方法,栈会把这些信息记录下来)。
不同的JVM版本,其线程栈的默认大小也有所差别,具体如下表所示。一般而言,如果在32位JVM上有128 KB的栈,在64位JVM上有256 KB的栈,很多应用实际就可以运行了。如果这个值设置得太小,潜在的缺点是,当某个线程的调用栈非常大时,会抛出StackoverflowError
。
几种JVM的默认栈大小:
在64位的JVM中,除非物理内存非常有限,并且较小的栈可以防止耗尽原生内存,否则没有理由设置这个值。 另一方面,在32位的JVM上,使用较小的栈(比如128 KB)往往是个不错的选择,因为这样可以在进程空间中释放部分内存,使得JVM的堆可以大一些。
耗尽原生内存
没有足够的原生内存来创建线程,也可能会抛出OutOfNemoryError。这意味着可能出现了以下3种情况之一。
1.在32位的JVM上,进程所占空间达到了4GB的最大值(或者小于4GB,取决于操作系统)。
2.系统实际已经耗尽了虚拟内存。
3.在Unix风格的系统上,用户创建的进程数已经达到配额限制。这方面单独的线程会被看作一个进程。
减少栈的大小可以克服前两个问题,但是对第三个问题没什么效果。遗憾的是,无法从JVM报错看出到底是哪种情况,只能在遇到错误时依次排查。
要改变线程的栈大小,可以使用-Xss=N标志(例如-Xss=256k)。
快速小结
- 在内存比较稀缺的机器上,可以减少线程栈大小。
- 在32位的JVM上,可以减少线程栈大小,以便在4GB进程空间限制的条件下,稍稍增加堆可以使用的内存。
偏向锁
当锁被争用时,JVM(和操作系统)可以选择如何分配锁。锁可以被公平地授予,每个线程以轮转调度方式(round-robin)获得锁。还有一种方案,即锁可以偏向于对它访问最为频繁的线程。
**偏向锁背后的理论依据是,如果一个线程最近用到了某个锁,那么线程下一次执行由同一把锁保护的代码所需的数据可能仍然保存在处理器的缓存中。如果给这个线程优先获得这把锁的权利,缓存命中率可能就会增加。**如果实现了这点,性能会有所改进。但是因为偏向锁也需要一些簿记信息,故有时性能可能会更糟。
特别是,使用了某个线程池的应用(包括大部分应用服务器),在偏向锁生效的情况下,性能会更糟糕。在那种编程模型下,不同的线程有同等机会访问争用的锁。对于这些类应用,使用-XX:-UseBiasedLocking
选项禁用偏向锁,会稍稍改进性能。偏向锁默认是开启的。
自旋锁
在处理同步锁的竞争问题时,JVM有两种选择。对于想要获得锁而陷入阻塞的线程,可以让它进入忙循环,执行一些指令,然后再次检查这个锁。也可以把这个线程放入一个队列,在锁可用时通知它(使得CPU可供其他线程使用)。
如果多个线程竞争的锁的被持有时间较短,那忙循环(所谓的线程自旋)就比另一个方案快得多。如果被持有时间较长,则让第二个线程等待通知会更好,而且这样第三个线程也有机会使用CPU。 JVM会在这两种情况间寻求合理的平衡,自动调整将线程移交到待通知队列中之前的自旋时间。有些参数可以调整自旋时间,但大部分是实验性的,都有可能会发生变化,即使是极小的版本更新。
如果想影响JVM处理自旋锁的方式,唯一合理的方式就是让同步块尽可能短;当然不管什么情况,都是应该这么做的。这样可以限制与程序功能没有直接关系的自旋的量,也降低了线程进入通知队列的机会。
UseSpinning标志
之前的Java版本支持一个`-XX:+UseSpinning`标志,该标志可以开启或关闭自旋锁。在Java 7及更高版本中,这个标志已经没用了:自旋锁无法禁用。不过考虑到向后兼容,
Java 7到7u40这些版本的命令行参数仍然接受该标志,但是不执行任何操作。有点奇怪的是,这个标志的默认值会报告为false,即使自旋锁一直在发挥作用。
从Java7u40(以及Java8中)开始,Java不再支持该标志,使用这个标志会报错。
线程优先级
每个Java线程都有一个开发者定义的优先级,这是应用提供给操作系统的一个线索,用以说明特定线程在其眼中的重要程度。如果有不同线程处理不同任务,可能会认为,可以以让其他任务在优先级较低的线程上运行为代价,使用线程优先级来改进特定任务的性能。遗憾的是,实际不会这么有用。
操作系统会为机器上运行的每个线程计算一个“当前”(current)优先级。 当前优先级会考虑Java指派的优先级,但是还会考虑很多其他的因素,其中最重要的一个是:自线程上次运行到现在所持续的时间。这可以确保所有的线程都有机会在某个时间点运行。不管优先级高低,没有线程会一直处于“饥饿”状态,等待访问CPU。
这两个因素之间的平衡会随操作系统的不同而有所差异。在基于Unix的系统上,整体优先级的计算主要取决于线程上次运行到现在所持续的时间,Java层指定的优先级影响微乎其微。在Windows系统上,在Java层指定的优先级较高的线程,往往会比优先级较低的线程运行更久;但即便优先级较低,那些线程也会得到相对公平的执行时间。
不过,不管是哪种情况,都不能依赖线程的优先级来影响其性能。如果某些任务比其他任务更重要,就必须使用应用层逻辑来划分优先级。 在某种程度上,可以通过将任务指派给不同的线程池并修改那些池的大小来解决。
小结
理解线程如何运作,可以获得很大的性能优势。不过就线程的性能而言,其实没有太多可以调优的:可以修改的JVM标志相当少,而且那些标志的效果也很有限。
相反,较好的线程性能是这么来的:遵循管理线程数、限制同步带来的影响的一系列最佳实践原则。借助适当的剖析工具和锁分析工具,可以检查并修改应用,以避免线程和锁的问题给性能带来负面影响。
Java EE性能调优
Web容器的基本性能
Java EE应用服务器性能的关键是Web容器,它通过基本的servlet和JSP页面处理HTTP请求。有些基本的途径可以改善Web容器的性能,改进的具体方法因Java EE实现的不同而有所不同,但一些概念可以适用于所有服务器。
减少输出
减少服务器产生的结果输出可以加快Web页面返回到浏览器的速度。
减少空格
在servlet代码中调用PrintWriter时不要写入多余的空格,因为空格在网络上传输时同样需要时间(而且,相对于代码的处理,网络传输时间更为重要)。 应该用print()
而不是println()
,主要是为了避免在返回结果的HTML中写入制表符或空格。虽然这确实会使有些人查看Web页面源代码时看不清结构,但如果他们真对源代码感兴趣,总会使用XML或HTML编辑器。也可以让内部QA或者性能优化小组来处理空格。毫无疑问,结构化的页面源代码可以简化调试,但为了改善应用的响应时间,最后还得把它载入格式编辑器以去除多余的空格。绝大多数应用服务器都可以自动去除JSP页面中的空格。比如Tomcat(以及基于Tomcat的开源Java EE服务器)中的trimSpaces指令,可以将JSP页面每行的前后空格都去掉。所以开发和维护JSP页面时可以有适当的缩进,而不用担心会在网络上传输不必要的空格。
合并CSS和JavaScript资源
对于开发者来说,把CSS保存在独立的文件中是有意义的,也更容易维护。对于JavaScript来说也是如此。但使用这些资源时,传输一个大文件的效率比传输几个小文件要高。Java EE没有这方面的标准,而且绝大多数应用服务器也无法自动处理,不过有些开发工具可以帮助你合并这些资源。
压缩输出
从用户角度来看,执行Web请求的最长时间通常是服务器将HTML发回浏览器所需的时间。但由于客户端(模拟浏览器)到服务器的性能测试通常在快速局域网中进行,所以这个时间通常并不是最长的。虽然真实用户可能在“快速”广域网中,但仍然要比你实验室里的机器之间的LAN慢一个数量级。大多数应用服务器在将数据发回浏览器时都有压缩机制:HTML数据压缩发送给浏览器,内容类型(content type)为zip或gzip。这只有在初始请求指明浏览器支持压缩时才行得通。所有的现代浏览器都支持该特性。开启压缩要求服务器有更多的CPU周期,但通常数据量越小,网络传送的时间也越少,从而整体性能就会越高。然而与本节讨论的其他优化不同,它并不总能提高性能。后面的例子表明,在LAN开启压缩时,性能可能会下降。应用发送很小的页面时也会如此(尽管大多数应用服务器允许只有输出大于某个特定尺寸时才压缩)。
字符串是否应该预编码?
应用服务器在字符转换上要花费大量时间:从Java的String对象(以UTF-16格式保存)转换成客户端所需要的字节数组。许多这样的字符串总是相同的。
Web页面的HTML字符串并不会总随着数据发生变动(如果发生了,它们也仍然是从字符串常量集合中获取的)。
字符串是否预先编码取决于服务器:有些服务器会对此提供一个选项,有一些则是自动执行的。
在servlet中,这些字符串可以预编码,然后用ServletOutputStream的write()通过网络发送,不要用PrintWriter的print()。不过动态数据仍然要用print()才能正确编码。
(可以从header中找到目标编码,然后对字符串编码,但这种方法相对容易出错。)
应用服务器实现这些输出接口以及在其内部缓存这些数据的方式有很大差别。对一些服务器来说,混用servlet的输出流(output stream)和它的小伙伴print writer会导致频繁刷新网络缓存。
从性能优化角度看,频繁刷新缓存是非常昂贵的操作——比重新编码这些数据更昂责。与此类似,对一大块数据进行编码的代价通常不会比一小块数据高很多:最主要的代价是建立到编码器的调用。因此,对小段动态数据来说,频繁地编码及发送编码后的字节数组会拖慢应用:多次调用编码器所花费的时间,比一次调用编码所有的东西(包括静态数据)要长。
代码的预编码在某些情况下有一定作用,但要视情况而定。
与测试相比,这些优化措施实际运行中的性能会有很大差别。下表显示了可能会出现的结果。测试中所用股票历史servlet产生的输出比较长,获取的数据范围有10年。所产生的结果是未经压缩和未去除空格的HTML页面,大约为100 KB。为了将带宽的影响降至最低,测试只运行单个用户,思考时间为100毫秒,然后测量请求的平均响应时间。使用局域网时,测试通过100MB的交换机在本地网络上运行;使用宽带时,测试在家里的电缆上运行(平均每秒30 Mb的下载速度)。使用本地咖啡店中的公共WiFi连接的广域网时——网速是相当不可靠的(表格中展示了历时4个小时的平均样本)。
几种Web响应输出尺寸方面的优化在不同网络条件下的效果:
这张表强调了在应用的实际部署环境中进行测试的重要性。如果只在实验室环境中进行测试调优,那得到的一大半性能都是不太靠谱的。 虽然这个例子中的测试实际运行在远程应用服务器上(使用公有云服务),但硬件模拟器可以模拟出实验室环境,控制所有相关的机器。(云服务机器也比局域网机器快;它们之间的机器数量无法直接进行比较。)
快速小结
- 在Java EE应用所实际运行的网络基础设施上对它们进行测试。
- 外部网络相对内部网络来说仍然是慢的。限制应用所写的数据量会取得很好的性能。