如何提高 Java 应用程序的性能

确保 Java 应用程序运行在性能最佳状态需要付出一些努力。下面介绍如何帮助确保 Java 应用程序的性能是一流的

每天‬分享‬最新‬软件‬开发‬,Devops,敏捷‬,测试‬以及‬项目‬管理‬最新‬,最热门‬的‬文章‬,每天‬花‬3分钟‬学习‬何乐而不为‬,希望‬大家‬点赞‬,评论‬,加‬关注‬,你的‬支持‬是我‬最大‬的‬动力‬。
简介

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

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

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

服务目标

在开始改进应用程序的性能之前,我们需要围绕关键领域(如可伸缩性、性能、可用性等)定义和理解我们的非功能性需求。

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

  1. Average 应用程序响应时间
  2. Average 并发用户系统必须支持
  3. Expected 每秒的请求在高峰负荷期间

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

申请样本

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

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

识别瓶颈

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

Gatling 是负载测试的最佳工具之一,它为 HTTP 协议提供了极好的支持——这使它成为负载测试任何 HTTP 服务器的极佳选择。

Stackify 的 Retrace 是一个成熟的 APM 解决方案,具有丰富的特性集——所以很自然,这是帮助您确定这个应用程序的基线的一个很好的方法。Retrace 的一个关键组件是它的代码分析,该代码分析收集运行时信息,而不会降低应用程序的速度。

Retrace 还为监视基于 JVM 的运行应用程序的内存、线程和类提供了小部件。除了应用程序指标之外,它还支持监视承载我们的应用程序的服务器的 CPU 和 IO 使用情况。

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

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

Gatling Load Test

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

并且,在运行模拟之后,我们 Gatling 生成有用的、易于分析的 HTML 报告。

定义场景

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

在我们的示例中,场景类似于“让我们启动200个用户,每个用户发出10,000个请求”

配置记录器

基于 Gatling 的第一步,创建一个新文件 EmployeeSimulationscala 文件,其代码如下:

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,比如数据库连接耗尽、高负载期间请求超时、由于内存泄漏导致的不必要的高堆使用等等。

Monitoring the Application

要开始对 Java 应用程序使用 Retrace,第一步是在 Stackify 上注册一个免费试用版。

接下来,我们需要将 SpringBoot 应用程序配置为 Linux 服务。我们还需要在托管应用程序的服务器上安装 Retrace 代理,如下所示。

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

查找堆栈中速度最慢的部分

自动跟踪我们的应用程序,跟踪许多常见框架和依赖项的使用情况,包括 SQL、 MongoDB、 Redis、 Elasticsearch 等。回溯使得快速识别应用程序出现性能问题的原因变得容易,比如:

  • 是一定的SQL 语句拖慢了我们的速度?
  • Redis 突然变慢了吗?
  • 特定的 HTTP Web 服务降低或降低速度?


例如,下图提供了在给定时间段内堆栈最慢部分的见解。

代码级优化

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

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

使用 StringBuilder 进行字符串串联

字符串串联是一种非常常见的操作,也是一种效率低下的操作。简单地说,使用 + = 追加 String 的问题在于,它会在每个新操作中导致一个新 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 确实对 String 操作执行了编译和或运行时优化。

避免递归

导致 StackOverFlowError 的递归代码逻辑是 Java 应用程序中的另一个常见场景。

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

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

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 也有讨论。

小心使用正则表达式

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

如果您绝对必须在计算密集型代码部分中使用正则表达式,那么缓存 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. 估计应用程序将创建的线程数

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

了解应用程序需要什么的最可靠方法是对应用程序运行一个实际的负载测试,并在运行时跟踪指标。我们前面讨论的基于 Gatling 的测试是实现这一点的一个很好的方法。

选择正确的垃圾收集器

停止世界垃圾收集周期曾经是大多数面向客户端的应用程序的响应能力和总体 Java 性能的一个巨大问题。

但是,当前的垃圾收集器基本上已经解决了这个问题,并且通过适当的调优和调整大小,可能导致没有明显的收集周期。也就是说,要达到这个目的,需要深入了解整个 JVM 上的 GC,以及应用程序的特定配置文件。

分析器、堆转储和详细 GC 日志记录等工具肯定会有所帮助。同样,这些都需要在真实的负载模式上捕获,这正是我们前面讨论的 Gatling 性能测试的用武之地。

有关不同垃圾收集器的详细信息,请参阅此指南。

JDBC 性能

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

连接池

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

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

JDBC 批处理

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

无论是在驱动程序还是在数据库方面,性能增益都是显著的。PreparedStatement 是一个优秀的批处理候选者,一些数据库系统(如 Oracle)只支持对准备好的语句进行批处理。

另一方面,Hibernate 更加灵活,允许我们通过单一配置切换到批处理。

语句缓存

接下来,语句缓存是另一种可能提高持久层性能的方法——这是一种不太为人所知的性能优化,您可以很容易地利用它。

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

放大和扩大

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

Architectural Improvements

缓存

内存价格很低,而且越来越低,从磁盘或通过网络检索数据仍然很昂贵。缓存当然是我们不应该忽视的应用程序性能的一个方面。

当然,在应用程序的拓扑结构中引入独立的缓存系统确实增加了架构的复杂性——因此,开始利用缓存的一个好方法是充分利用我们已经使用的库和框架中现有的缓存功能。

例如,大多数持久性框架都有很好的缓存支持。诸如 Spring MVC 之类的 Web 框架还可以利用 Spring 内置的缓存支持,以及基于 ETags 的强大 HTTP 级缓存。

但是,在所有这些容易摘下的果实被摘下之后,在 Redis、 Ehcache 或 Memcache 这样的独立缓存服务器上缓存应用程序中频繁访问的内容可能是一个很好的下一步——减少数据库负载并大大提高应用程序性能。

扩大规模

无论我们在单个实例上投入多少硬件,在某些时候这都是不够的。简单地说,向上扩展具有自然的限制,当系统遇到这些限制时——向外扩展是增长、发展和简单地处理更多负载的唯一方法。

毫不奇怪,这个步骤确实非常复杂,但是它仍然是在某个特定点之后扩展应用程序的唯一方法。

而且,在大多数现代框架和库中,支持是好的,并且总是在不断改进。Spring 生态系统有一整组专门针对应用程序体系结构这一特定领域构建的项目,其他大多数栈也有类似的支持。

最后,在集群的帮助下扩展的另一个好处是,除了纯 Java 性能之外,添加新节点还会导致冗余和更好的故障处理技术,从而提高系统的整体可用性。

结论


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

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

举报
评论 0