想知道Java程序如何才能提高性能吗?

介    绍


在本文中,我们将讨论一些有助于提高Java应用程序性能的方法。我们将从如何定义可衡量的性能目标开始,然后查看不同的工具来衡量和监控应用程序性能,并找出瓶颈。

此外,还将介绍一些常见的Java代码级优化以及最佳编码实践。最后,我们将介绍JVM特定的调优技巧和体系结构更改,以提高Java应用程序的性能。

请注意,性能优化是一个广泛的主题,这只是在JVM上探索它的起点。


绩效目标


在我们开始致力于提高应用程序性能之前,我们需要定义和理解围绕关键领域的非功能性需求,例如可伸缩性,性能,可用性等。

以下是典型Web应用程序的一些常用性能目标:

1.    平均应用响应时间

2.    系统必须支持的平均并发用户数

3.    峰值负载期间 每秒的预期请求

使用可通过不同负载测试和应用程序监视工具,测量的这些指标,有助于识别关键瓶颈并相应地调整性能。

测试用例


让我们定义一下我们可以在本文中使用的基线应用程序。我们将使用一个简单的SpringBoot Web应用程序 - 就像我们在本文中创建的那样,此应用程序正在管理员工列表,并公开REST API以添加员工和检索现有员工。

我们将使用它作为运行负载测试的参考,并在接下来的部分,监控不同的应用程序指标。

    

识别瓶颈


负载测试工具和应用程序性能管理(APM)解决方案通常用于跟踪和优化Java应用程序的性能。围绕不同的应用场景运行负载测试,同时使用APM工具监控CPUIO,堆使用情况等是识别瓶颈的关键。

Gatling是负载测试的最佳工具之一,它提供了对HTTP协议的出色支持- 这使其成为负载测试任何HTTP服务器的绝佳选择。

StackifyRetrace是一个成熟的APM解决方案,具有丰富的功能 - 当然,这是帮助您确定此应用程序基线的好方法。Retrace的一个关键组件是其代码分析,它可以在不降低应用程序速度的情况下收集运行时信息。

Retrace还提供了用于监视正在运行的基于JVM的应用程序的内存,线程和类的小部件。除应用程序指标外,它还支持监视并托管我们应用程序的服务器的CPUIO使用情况。

因此,像Retrace这样的成熟监控工具涵盖了解锁应用程序性能潜力的第一部分,第二部分实际上能够重现系统中的实际使用和负载。

这实际上比它看起来更难实现,并且理解应用程序的当前性能配置文件也很关键。这就是我们接下来要关注的内容。

 

负载试验

Gatling仿真脚本是用Scala编写的,但该工具还带有一个有用的GUI,能使我们记录场景。然后,GUI创建表示模拟的Scala脚本。

并且,在运行模拟之后,我们Gatling会生成有用的,可随时分析的HTML报告。

定义方案

在启动记录之前,我们需要定义一个场景。它将表示用户导航Web应用程序时会发生什么。

在我们的例子中,场景将是让我们启动200个用户,每个用户发出10,000个请求

配置记录器

基于Gatling的第一步,使用以下代码创建一个新文件EmployeeSimulation  scala文件:

 

    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进行负载测试有助于查找细微的,难以发现的错误,例如数据库连接耗尽,请求在高负载期间超时,由于内存泄漏导致的不必要的高堆使用率等。


监控应用程序


要开始使用Retrace forJava应用程序,第一步是在Stackify注册免费试用

一旦我们启动了要监视的Retrace代理和Java应用程序,我们就可以转到Retrace仪表板并单击AddApp链接。完成后,Retrace将开始监控我们的应用程序。

找到堆栈中最慢的部分

Retrace自动检测我们的应用程序并跟踪数十种常见框架和依赖项的使用,包括SQLMongoDBRedisElasticsearch.Retrace可以轻松快速识别我们的应用程序出现性能问题的原因,如:

  • 某个SQL语句会让我们放慢脚步吗?

  • Redis突然变慢了吗?

  • 特定的HTTP Web服务下降还是慢

例如,下图提供了在给定持续时间内堆栈最慢部分周围的见解

 

640?wx_fmt=png


代码级优化


负载测试和应用程序监控非常有助于识别应用程序中的一些关键瓶颈。但与此同时,我们需要遵循良好的编码实践,以便在我们开始应用程序监控之前避免许多性能问题。

让我们看一下下一节中的一些最佳实践。


使用StringBuilder进行字符串连接

字符串连接是一种非常常见的操作,也是一种低效的操作。简单地说,使用+=附加字符串的问题在于它将导致为每个新操作分配一个新的String

例如,这是一个简化但典型的循环- 首先使用原始连接,然后使用适当的构建器:

 

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的效率要高得多,特别是考虑到这些基于String的操作的常见程度。

在我们继续之前,请注意当前一代JVM确实对字符串操作执行编译和/或运行时优化


避免递归

导致StackOverFlowError递归代码逻辑是Java应用程序中的另一种常见方案。

如果我们不能废除递归逻辑,那么尾递归作为替代方案会更好。

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

 

public int factorial(int n) {
if (n == 0) {
return 1;
    } else {
return n * factorial(n - 1);
    }
}

现在让我们将它重写为tail recursive

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的问题。


小心使用正则表达式

正则表达式在很多场景中都很有用,但它们往往具有非常高的性能成本。了解各种JDKString方法也很重要,这些方法使用正则表达式,例如String.replaceAll()String.split()

如果绝对必须在计算密集型代码段中使用正则表达式,则值得缓存Pattern引用而不是重复编译:

 static final Pattern HEAVY_REGEX = Pattern.compile("(((X)*Y)*Z)*");

 

使用像ApacheCommons Lang这样的流行库也是一个不错的选择,特别是对于操作字符串。

 

避免创建和销毁太多线程

创建和处理线程是JVM上性能问题的常见原因,因为线程对象的创建和销毁相对较重。

如果您的应用程序使用大量线程,则使用线程池很有意义,允许重用这些昂贵的对象。

为此,Java ExecutorService是此处的基础,并提供了一个高级API来定义线程池的语义并与之交互。

来自Java 7的Fork /Join框架也值得一提,因为它提供了一些工具,可以通过尝试使用所有可用的处理器内核来加速并行处理。为了提供有效的并行执行,框架使用一个名为ForkJoinPool的线程池来管理工作线程 

 

JVM调优

堆大小调整

 

确定生产系统的正确JVM堆大小不是一个简单的练习。第一步是通过回答以下问题来确定可预测的内存要求:

1.   我们计划将多少个不同的应用程序部署到单个JVM进程,例如,EAR文件,WAR文件,jar文件等的数量?

2.   在运行时可能加载多少个Java类; 包括第三方API?

3.   估计内存缓存所需的占用空间,例如,由我们的应用程序(和第三方API)加载的内部缓存数据结构,例如来自数据库的缓存数据,从文件读取的数据等。

4.   估计应用程序将创建的线程数。

如果没有一些真实的测试,这些数字很难估计。

了解应用程序需求的最可靠方法是对应用程序运行实际负载测试并在运行时跟踪度量标准。我们之前讨论的基于加特林的测试是一种很好的方法。

 

选择合适的垃圾收集器

垃圾收集周期,用于代表大多数面向客户的应用程序的响应性和整体Java性能的巨大问题。

它确实需要深入了解整个JVM上的GC,以及应用程序的特定配置文件 - 实现目标。

像分析器,堆转储和详细的GC日志记录等工具肯定会有所帮助。而且,这些都需要在实际负载模式中捕获,这是我们之前讨论过的Gatling性能测试的结果。


JDBC性能


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

 

连接池

让我们从众所周知的事实开始,即数据库连接很复杂,用一个连接池机制解决,这一方法是伟大的。

这里的推荐是HikariCP JDBC - 一个非常轻量级(大约130Kb)和快速的JDBC连接池框架。

 

JDBC批处理

我们处理持久性的方式的另一个方面是尝试尽可能批量操作。JDBC批处理允许我们在单个数据库往返中发送多个SQL语句。

在驱动程序和数据库端,性能提升都很重要PreparedStatement是批处理的理想选择,一些数据库系统(例如Oracle)仅支持对预准备语句进行批处理。

另一方面,Hibernate更灵活,允许我们使用单一配置切换到批处理

 

Statement缓存

接下来,Statement缓存是另一种可能提高持久层性能的方法 - 一种鲜为人知的性能优化,您可以轻松利用它。

根据底层JDBC驱动程序,您可以在客户端(驱动程序)或数据库端(语法树甚至执行计划)缓存PreparedStatement

 

向上扩展和向外扩展

数据库复制和分片也是提高吞吐量的绝佳方法,我们应该利用这些经过实战考验的体系架构模式来扩展企业应用程序的持久层


结论


在本文中,我们探讨了许多有关提高Java应用程序性能的不同概念。我们从负载测试,基于APM工具的应用程序和服务器监控开始 - 然后是围绕编写高性能Java代码的一些最佳实践。

最后,我们研究了JVM特定的调优技巧,数据库端优化和体系架构更改,以扩展我们的应用程序。

 

640?wx_fmt=jpeg

长按二维码 ▲

订阅「架构师小秘圈」公众号

如有启发,帮我点个在看,谢谢↓

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值