一文了解 Java 应用程序性能优化指南
点击上方“CSDN”,选择关注
关键时刻,第一时间送达!
在《2018 最具就业前景的 7 大编程语言》一文中,通过分析了来自 Indeed 的 25 门编程语言、栈和框架的数据,我们盘点了18年最具就业前景的七大编程语言,其中,Java毫无悬念拔得头筹。
那么对于开发者来说,如何让 Java 应用程序达到性能最佳?本文将一步步教你将 Java 应用程序性能优化到一流。
本文我们将探讨一系列方法,用来提升 Java 应用程序的性能。首先定义出可度量的性能指标,然后通过不同的工具来衡量和监控应用程序性能,并找到影响性能的瓶颈所在。
此外,我们还将展示一些常用的 Java 代码级别优化方法以及最佳的编码实践。最后,我们将深入 JVM 特定的调优技巧和架构改进方法,以提升 Java 应用程序的性能。
性能指标
在开始动手改进应用程序的性能之前,我们需要定义和理解非功能性需求的一些关键领域,比如可扩展性、性能、可用性,等等。
以下是一些用来衡量 Web 应用程序性能的常用指标:
平均应用响应时间;
系统支持的平均并发用户数;
在最大负载期间每秒支持的请求数。
借助不同的负载测试手段以及应用程序监测工具对这些指标进行量化,有助于找出性能瓶颈的关键点并对其进行相应的优化,从而提升 Java 程序性能。
示例程序
首先创建一个示例程序,下文将基于该示例程序探讨性能优化方法。我使用一个简单的 Spring Boot Web 应用程序用作本文示例程序(可参考https://stackify.com/spring-boot-level-up/)。该程序负责管理员工列表,通过暴露出 REST API 用来进行员工新增和检索。
在下文中我们将把它作为负载测试的参考,来监控多种性能指标。
找到性能瓶颈
负载测试工具和应用程序性能管理(APM)解决方案通常用于跟踪和优化 Java 应用程序的性能。围绕不同的应用场景运行负载测试,同时使用 APM 工具监控 CPU、IO、内存占用等情况是识别瓶颈的关键。
Gatling 是负载测试的最佳工具之一,它提供了对 HTTP 协议的极佳支持,这使得它成为对任何 HTTP 服务器进行负载测试的绝佳选择。
Stackify 的 Retrace 是一个非常成熟的 APM 解决方案,它具有非常丰富的功能,可以帮助你确定应用程序的基准。Retrace 的关键组件之一是其代码性能分析模块,它能够在不减缓应用程序的情况下收集运行时信息。
Retrace 还提供了其他组件,用于监控基于 JVM 运行的应用程序的内存、线程和类。除了应用程序指标之外,它还能够监控托管应用程序的服务器的 CPU 和 IO 使用情况。
因此,像 Retrace 这样全面的监控工具将解锁应用程序性能优化的第一部分。第二部分则需要对应用程序在真实世界的使用情况和负载进行重现来优化。
想要重现并不容易,并且了解应用程序的当前性能配置文件也非常重要。接下来我们将重点关注这个问题。
Gatling 负载测试
Gatling 模拟脚本采用 Scala 编写,它附带了一个功能强大的 GUI,提供场景记录功能。GUI 自动创建 Scala 脚本呈现模拟测试结果。模拟测试完成之后,Gatling 能够生成有用的、即时分析的 HTML 报告。
定义一个场景
在启动记录器之前,我们需要定义一个场景,用于呈现用户在浏览 Web 应用程序时发生的情况。
在我们的例子中,测试方法是:“模拟200个用户,每个用户发起10000个请求。”
配置记录器
根据 Gatling 的第一步,使用以下代码创建一个名为 EmployeeSimulation 的 scala 新文件:
class EmployeeSimulation extends Simulation { val scn = scenario("FetchEmployees").repeat(10000) { exec( http("GetEmployees-API") .get("http://localhost:8080/employees") .check(status.is(200)) ) } setUp(scn.users(200).ramp(100)) }
运行负载测试
执行以下命令启动负载测试:
$GATLING_HOME/bin/gatling.sh -s basic.EmployeeSimulation
对应用程序的 API 进行负载测试有助于发现微小且隐蔽的 bug,例如数据库连接耗尽,请求在高负载期间超时,由于内存泄漏而导致的超高堆栈占用,等等。
监控应用程序
想要使用 Retrace 进行 Java 应用程序的开发,首先需要在 Stackify 上注册一个免费的试用版。
接下来,我们需要将我们的 Spring Boot 应用程序配置为 Linux 服务。我们还需要在托管应用程序的服务器上安装 Retrace 代理。
一旦启动了 Retrace 代理与要监控的 Java 应用程序,我们即可在 Retrace 仪表板上点击 AddApp 链接。之后 Retrace 将开始监控我们的应用程序。
寻找堆中最慢的部分
Retrace 自动监测我们的应用程序,并跟踪多种常见框架和依赖项的使用情况,包括SQL、MongoDB、Redis、Elasticsearch 等。如果应用程序包含下列性能问题,Retrace 能够帮助我们快速找到原因:
是否有某条 SQL 语句减缓了程序速度?
Redis 是否突然变慢?
是否有某个 HTTP Web 服务变缓或者 down 掉?
例如,下图展示了在给定时间内系统中运行最缓慢的部件。
代码级别优化
负载测试和应用程序监控对于确定应用程序中的一些关键瓶颈非常有用。但同时,我们需要遵循良好的编码实践,尽量避免性能问题在开始应用程序监控之前出现。
使用 StringBuilder 进行字符串连接
字符串连接是编程中非常普遍的操作,同时也是低效率的操作。简而言之,使用 + = 来追加字符串的问题在于,每次操作都会分配新的字符串。
以一个简化的典型循环为例,我们分别用原始的字符串连接和 builder 方式来实现。
public String stringAppendLoop() { String s = ""; for (int i = 0; i < 10000; i++) { if (s.length() > 0) s += ", "; s += "bar"; } return s;}public String stringAppendBuilderLoop() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { if (sb.length() > 0) sb.append(", "); sb.append("bar"); } return sb.toString(); }
在上面的代码中使用 StringBuilder 效率更高,尤其是对于频繁进行字符串操作的程序来说效率更加显著。
需要说明的是,当前版本的 JVM 自动对字符串操作执行了编译和运行时优化。
避免递归
在 Java 应用程序中,由于递归导致 StackOverFlowError 错误是很常见的。
如果我们无法避免使用递归逻辑,那么尽量使用尾递归。
来看一个采用头递归的例子:
public int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); }}
将其改为尾递归:
private int factorial(int n, int accum) { if (n == 0) { return accum; } else { return factorial(n - 1, accum * n); }}public int factorial(int n) { return factorial(n, 1); }
其他一些 JVM 语言(如 Scala)已经具有编译器级别的支持来优化尾递归代码,并且正在设法将这种优化类型引入到 Java 中。
谨慎使用正则表达式
正则表达式在很多应用场景下确实作用明显,但是它们往往需要非常高的性能成本。了解各种使用正则表达式的 JDK 字符串方法(如 String.replaceAll()或String.split())尤为重要。
如果你必须在计算密集型的代码段中使用正则表达式,那么尽量使用 *Pattern *缓存模式,避免重复编译:
static final Pattern HEAVY_REGEX = Pattern.compile("(((X)*Y)*Z)*");
对于操作字符串来说,使用像 Apache Commons Lang 这样的流行库也是一个很好的选择。
避免创建和销毁太多的线程
创建和处理线程是影响 JVM 性能的常见因素,因为线程对象的创建和销毁开销昂贵。
如果你的应用程序需要使用大量的线程,那么线程池的使用将意义非凡,它可以让这些昂贵的对象得到重复利用。
为此,Java 的 ExecutorService 提供了高级 API 用来定义线程池并与之交互。
Java 7 中的 Fork/Join 框架也值得一提,它提供了一些工具来利用处理器多核优势从而加速并行处理。为了提供有效的并行执行,框架使用了名为 ForkJoinPool 的线程池来管理工作线程。
JVM 调优
优化堆大小
为生产系统指定合适的 JVM 堆大小并非易事。首先需要回答下列问题来预测内存需求:
我们计划将多少个不同的应用程序部署到单个 JVM 进程中?有多少个 EAR、WAR、jar 这样的文件?
运行时可能会加载多少个 Java 类?是否包括第三方的 API?
估算内存中缓存所需的空间,例如,由我们的应用程序(和第三方 API)加载的内部缓存数据结构、来自数据库的缓存数据以及从文件中读取的数据,等等。
估算应用程序将创建的线程数量。
在缺乏真实测试的情况下,这些数字很难估算。
获得关于应用程序需求最可靠的方法是对应用程序进行负载测试,并在运行时跟踪性能指标。我们之前讨论的基于 Gatling 的测试就是一个很好的方法。
选择合适的垃圾收集器
对于大多数面向客户端的 Java 应用程序来说,Stop-the-world 垃圾收集器影响了程序的响应能力和整体性能。
但是,新一代的垃圾收集器大多已经解决了这个问题,并且通过适当的优化和调整,收集周期得到了弱化。但是想要做到这样,你需要深入了解整个 JVM 的垃圾收集机制以及应用程序本身。
像分析器、heap dumps 以及 GC 日志记录这样的工具用处很大。同样,它们都需要在真实的负载模式下才能派上用场,正如前文讨论的 Gatling 性能测试那样。
有关不同垃圾收集器的更多信息,请参阅https://stackify.com/what-is-java-garbage-collection/。
JDBC 性能
关系数据库是影响 Java 应用程序性能的另一个常见因素。为了获得更快的请求响应速度,我们必须关注应用程序的每一层,并考虑代码如何与底层 SQL DB 进行交互。
连接池
众所周知,数据库连接代价是昂贵的。连接池是优化该问题的重要机制。
强烈推荐 HikariCP JDBC ,它是一个轻量级(大约130Kb)且速度非常快的 JDBC 连接池框架。
JDBC 批量处理
在数据持久化过程中尽可能地批量操作。JDBC 批处理允许我们在单个数据库交互中发送多个 SQL 语句。
批处理使得驱动和数据库本身的性能都得到提升。PreparedStatement 是批处理的绝佳选择,一些数据库系统(例如 Oracle)仅支持预处理语句的批处理。
Hibernate 则更加灵活,允许我们切换到单一配置的批处理。
语句缓存
语句缓存是另一种能够提高持久层性能的方法 - 一种你可以轻松利用但鲜为人知的性能优化手段。
基于 JDBC 驱动程序,你可以在客户端(Driver)或数据库端(语法树甚至执行计划)上缓存 PreparedStatement。
纵向扩展与横向扩展
数据库复制和分片是提高吞吐量的重要手段,我们应该充分利用这些久经沙场的体系结构来扩展企业应用程序的持久层。
架构改进
高速缓存
如今内存价格不再昂贵而且会变得越来越低,但是从磁盘或者网络检索数据的代价依然很高。显然,缓存是我们在提升程序性能时不容忽视的环节。
当然,将独立缓存系统引入到应用程序的拓扑结构中会增加架构的复杂性,因此想要利用缓存,最直接的方式是充分利用已经使用的库和框架中的现有缓存功能。
例如,大多数持久性框架都有很好的缓存支持。Spring MVC 等 Web 框架还可以利用Spring 中内置的缓存支持以及基于 ETags 的强大的 HTTP 级缓存。
简单使用了缓存之后,便能频繁访问应用程序。如果想要更进一步,那么诸如 Redis、Ehcache 或 Memcache 这样的独立缓存服务器是很好的选择,它们能够减少数据库负载并提升应用程序性能。
横向扩展
无论我们为程序堆砌了多少硬件,总有某个时刻会显得依然不够。虽然横向扩展天生存在局限性,但是当系统遇到问题时,横向扩展依然是支撑更多负载的唯一途径。
横向扩展实施起来实属不易,但它是在系统遭遇某些瓶颈时的唯一解决方法。
而且,大多数的现代框架和库都支持横向扩展。Spring 生态系统有项目组专门用于解决该领域的应用程序体系结构问题,其他大多数项目都有类似的支持。
最后,除了纯粹的 Java 性能之外,在集群的帮助下进行扩展的另外一个好处是,添加新节点还会导致冗余和更好的处理故障的技术,从而提高整个系统的可用性。
总结
我们探讨了许多不同的方法来提高 Java 应用程序的性能。我们首先介绍了负载测试、基于 APM 工具的应用程序和服务器监控,随后介绍了编写高性能 Java 代码的一些最佳实践。
最后,我们研究了 JVM 特定的调优技巧、数据库优化和架构改进方案,以扩展我们的应用程序。
原文:How to Improve the Performance of a Java Application
链接:https://dzone.com/articles/how-to-improve-the-performance-of-a-java-applicati
作者:Eugen Paraschiv
译者:安翔
编辑:言则