编写高性能 Java 代码的最佳实践

作者 : 开心源码 本文共5336个字,预计阅读时间需要14分钟 发布时间: 2022-05-12 共175人阅读

摘要:本文首先详情了负载测试、基于APM工具的应使用程序和服务器监控,随后详情了编写高性能Java代码的少量最佳实践。最后研究了JVM特定的调优技巧、数据库端的优化和架构方面的调整。以下是译文。

详情

在这篇文章中,我们将探讨几个有助于提升Java应使用程序性能的方法。我们首先将详情如何定义可度量的性能指标,而后看看有哪些工具可以使用来度量和监控应使用程序性能,以及确定性能瓶颈。

我们还将看到少量常见的Java代码优化方法以及最佳编码实践。最后,我们将看看使用于提升Java应使用程序性能的JVM调优技巧和架构调整。

请注意,性能优化是一个很宽泛的话题,而本文只是对JVM探究的一个起点。

性能指标

在开始优化应使用程序的性能之前,我们需要了解诸如可扩展性、性能、可使用性等方面的非功能需求。

以下是典型Web应使用程序常使用的少量性能指标:

应使用程序平均响应时间

系统必需支持的平均并发使用户数

在负载高峰期间,预期的每秒请求数

这些指标可以通过用多种监视工具监测到,它们对分析性能瓶颈和性能调优有着非常大的作使用。

示例应使用程序

我们将用一个简单的Spring Boot Web应使用程序作为示例,在这篇文章中有相关的详情。这个应使用程序可使用于管理员工列表,并对外公开了增加和检索员工的REST API。

我们将用这个程序作为参考来运行负载测试,并在接下来的章节中监控各种应使用指标。

找出性能瓶颈

负载测试工具和应使用程序性能管理(APM)处理方案常使用于跟踪和优化Java应使用程序的性能。要找出性能瓶颈,主要就是对各种应使用场景进行负载测试,并同时用APM工具对CPU、IO、堆的用情况进行监控等等。

Gatling是进行负载测试最好的工具之一,它提供了对HTTP协议的支持,是HTTP服务器负载测试的绝佳选择。

Stackify的Retrace是一个成熟的APM处理方案。它的功能很丰富,对确定应使用程序的性能基线很有帮助。 Retrace的关键组件之一是它的代码分析功能,它能够在不减慢应使用程序的情况下收集运行时信息。

Retrace还提供了监视基于JVM应使用程序的内存、线程和类的小部件。除了应使用程序本身的指标之外,它还支持监视托管应使用程序的服务器的CPU和IO用情况。

因而,像Retrace这样功能全面的监控工具是解锁应使用程序性能潜力的第一步。而第二步则是在你的系统上重现真实用场景和负载。

说起来容易,做起来难,而且理解应使用程序当前的性能也非常重要。这就是我们接下来要关注的问题。

Gatling负载测试

Gatling的模拟测试脚本是使用Scala编写的,但该工具还附带了一个非常有使用的图形界面,可使用于记录具体的场景,并生成Scala脚本。

在运行模拟脚本之后,Gatling会生成一份非常有使用的、可使用于分析的HTML报告。

定义场景

在启动记录器之前,我们需要定义一个场景,表示使用户在浏览Web应使用时发生的事情。

在我们的这个例子中,具体的场景将是“启动200个使用户,每个使用户发出一万个请求。”

配置记录器

根据“Gatling的第一步”所述,使用下面的代码创立一个名为EmployeeSimulation的scala文件:

classEmployeeSimulationextendsSimulation{valscn = 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-sbasic.EmployeeSimulation复制代码

对应使用程序的API进行负载测试有助于发现及其细微的并且难以发现的错误,如数据库连接耗尽、高负载情况下的请求超时、由于内存泄漏而导致堆的高用率等等。

监控应使用程序

要用Retrace进行Java应使用程序的开发,首先需要在Stackify上申请免费试使用账号。而后,将我们自己的Spring Boot应使用程序配置为Linux服务。我们还需要在托管应使用程序的服务器上安装Retrace代理商,按照这篇文章所述的操作就可。

Retrace代理商和要监控的Java应使用程序启动后,我们即可以到Retrace仪表板上单击AddApp按钮增加应使用了。增加应使用完成之后,Retrace将开始监控应使用程序了。

找到最慢的那个点

Retrace会自动监控应使用程序,并跟踪数十种常见框架及其依赖关系的用情况,包括SQL、MongoDB、Redis、Elasticsearch等等。Retrace能帮助我们快速确定应使用程序为什么会出现如下性能问题:

某个SQL语句能否会拖慢系统的速度?

Redis忽然变慢了吗?

特定的HTTP Web服务宕了,还是变慢了?

例如,下面的图形展现了在一段给定的时间内速度最慢的组件。

代码级别的优化

负载测试和应使用程序监控对于确定应使用程序的少量关键性能瓶颈非常有使用。但同时,我们需要遵循良好的编码习惯,以避免在对应使用程序进行监控的时候出现过多的性能问题。

在下一章节中,我们将来看少量最佳实践。

用StringBuilder来连接字符串

字符串连接是一个非常常见的操作,也是一个低效率的操作。简单地说,用+=来追加字符串的问题在于每次操作都会分配新的String。

下面这个例子是一个简化了的但却很典型的循环。前面用了原始的连接方式,后面用了构建器:

publicStringstringAppendLoop(){? ? String s =””;for(inti =0; i <10000; i++) {if(s.length() >0)? ? ? ? ? ? s +=”, “;? ? ? ? s +=”bar”;? ? }returns;}publicStringstringAppendBuilderLoop(){? ? StringBuilder sb =newStringBuilder();for(inti =0; i <10000; i++) {if(sb.length() >0)? ? ? ? ? ? sb.append(“, “);? ? ? ? sb.append(“bar”);? ? }returnsb.toString();}复制代码

上面代码中用的StringBuilder对性能的提升非常有效。请注意,现代的JVM会在编译或者者运行时对字符串操作进行优化。

避免递归

导致出现StackOverFlowError错误的递归代码逻辑是Java应使用程序中另一种常见的问题。假如无法去掉递归逻辑,那么尾递归作为替代方案将会更好。

我们来看一个头递归的例子:

publicintfactorial(intn){if(n ==0) {return1;? ? }else{returnn * factorial(n -1);? ? }}复制代码

现在我们把它重写为尾递归:

privateintfactorial(intn,intaccum){if(n ==0) {returnaccum;? ? }else{returnfactorial(n -1, accum * n);? ? }}publicintfactorial(intn){returnfactorial(n,1);}复制代码

其余JVM语言(如Scala)已经在编译器级支持尾递归代码的优化,当然,对于这种优化目前也存在着少量争议。

谨慎用正则表达式

正则表达式在很多场景中都非常有使用,但它们往往具备非常高的性能成本。理解各种用正则表达式的JDK字符串方法很重要,例如String.replaceAll()、String.split()。

假如你不得不在计算密集的代码段中用正则表达式,那么需要缓存Pattern的引使用而避免重复编译:

staticfinalPatternHEAVY_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的测试就是一个很好的方法。

选择合适的垃圾收集器

Stop-the-world(STW)垃圾收集的周期是影响大多数面向用户端应使用程序响应和整体Java性能的大问题。但是,目前的垃圾收集器大多处理了这个问题,并且通过适当的优化和大小的调整,能够消除对收集周期的感知。

分析器、堆转储和详细的GC日志记录工具对此有肯定的帮助作使用。再一次注意,这些都需要在真实场景的负载模式下进行监控。

有关不同垃圾收集器的更多信息,请查看这个指南。

JDBC性能

关系型数据库是Java应使用程序中另一个常见的性能问题。为了取得完整请求的响应时间,我们很自然地必需查看应使用程序的每一层,并思考如何让代码与底层SQL DB进行交互。

连接池

让我们从众所周知的事实开始,即数据库连接是昂贵的。 连接池机制是处理这个问题非常重要的第一步。

这里建议用HikariCP JDBC,这是一个非常轻量级(大约130Kb)并且速度极快的JDBC连接池框架。

JDBC批解决

持久化解决应尽可能地执行批量操作。 JDBC批解决允许我们在单次数据库交互中发送多个SQL语句。

这样,无论是在驱动端还是在数据库端,性能都可能得到明显地提升。 * PreparedStatement*是一个非常棒的的批解决命令,少量数据库系统(例如Oracle)只支持预解决语句的批解决。

另一方面,Hibernate则更加灵活,它允许我们只要修改一个配置就可快速切换为批解决操作。

语句缓存

语句缓存是另一种提高持久层性能的方法,这是一种鲜为人知但又容易掌握的性能优化方法。

只需底层的JDBC驱动程序支持,你即可以在用户端(驱动程序)或者数据库端(语法树甚至执行计划)中缓存PreparedStatement。

规模的缩放

数据库复制和分片是提高吞吐量非常好的方法,我们应该充分利使用这些经过实践检验的架构模式,以扩展企业应使用的持久层。

架构改进

缓存

现在内存的价格很低,而且越来越低,从磁盘或者通过网络来检索数据的性能代价依然很高。缓存自然而然的变成了在应使用程序性能方面不能忽视的关键。

当然,在应使用的拓扑结构中引入一个独立的缓存系统的确会添加架构的复杂度,所以,应当充分利使用当前用的库和框架现有的缓存功能。

例如,大多数的持久化框架都支持缓存。 Spring MVC等Web框架还可以用Spring中内置的缓存支持,以及基于ETags的强大的HTTP级缓存。

横向扩展

无论我们在单个实例中准备了多少硬件,都会有不够使用的时候。简而言之,扩展有着天生的局限性,当系统遇到这些问题时,横向扩展是解决更多负载的唯一途径。这一步一定会相当的复杂,但却是扩展应使用的唯一办法。

对大多数的现代框架和库来说,这方面还是支持得很好的,而且会变得越来越好。 Spring生态系统有一个完整的项目集,专门使用于处理这个特定的应使用程序架构领域,其余大多数的框架也都有相似的支持。

除了能够提升Java的性能,通过集群进行横向扩展也有其余的好处,增加新的节点能产生冗余,并更好的解决故障,从而提高整个系统的可使用性。

结论

在这篇文章中,我们围绕着提升Java应使用的性能讨论了许多概念。我们首先详情了负载测试、基于APM工具的应使用程序和服务器监控,随后详情了编写高性能Java代码的少量最佳实践。最后,我们研究了JVM特定的调优技巧、数据库端的优化和架构方面的调整。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 编写高性能 Java 代码的最佳实践

发表回复