影响 Java EE 性能的十大问题

本文作者是一名拥有 10 多年经验的高级系统架构师,主要专业领域涵盖 Java EE、中间件和 JVM 技术。他在性能优化和提升方面有着深刻的见解。以下将分享常见的 10 个影响 Java EE 性能的问题。

1. 缺乏正确的容量规划

容量规划(Capacity Planning)是一个全面且持续发展的过程,旨在预测当前和未来的 IT 环境容量需求。制定合理的容量规划不仅能确保和跟踪当前 IT 生产能力的稳定性,还能确保新项目以最小的风险部署到现有的生产环境中。硬件、中间件、JVM 调优等应在项目部署之前准备就绪。

根据经验,这通常是最常见的“过程”问题,可能会导致短期和长期的性能隐患。以下是一些示例:

观察到的问题可能的容量规划差距
新部署的应用程序会触发当前 Java Heap 或 Native Heap 空间的重载(例如,观察到 java.lang.OutOfMemoryError)。- 对当前的 JVM Java 堆(YoungGen 和 OldGen 空间)利用率缺乏了解
- 新部署的应用程序缺少内存静态和/或动态占用空间计算
- 缺乏性能和负载测试,无法检测到 Java Heap 内存泄漏等问题
新部署的应用程序会触发 CPU 利用率的大幅提高以及 Java EE 中间件 JVM 进程的性能下降。- 对当前的 CPU 使用率缺乏了解(例如,已建立的基准)
- 对当前的 JVM 垃圾回收状况缺乏了解(新的应用程序/额外的负载可能触发增加的 GC 和 CPU)
- 缺乏负载和性能测试,无法预测对现有 CPU 利用率的影响
新的 Java EE 中间件系统已部署到生产环境,但无法处理预期的数量。- 缺少或不充分的性能和负载测试
- 用于性能和负载测试的数据和测试用例不能反映真实的流量和业务流程
- 带宽不足(或页面比预期的容量规划大得多)

容量规划的一个关键方面是每个人都应该熟悉的负载和性能测试。这涉及针对类生产环境或生产环境本身生成负载,以便:

  • 确定您的应用程序可以支持多少并发用户/订单量。
  • 揭示平台和 Java EE 应用程序的瓶颈,使您能够采取纠正措施(中间件调整、代码更改、基础结构和容量改进等)。

2. Java EE 中间件环境规范不足

“没有规矩,不成方圆”。第二个比较普遍的原因是 Java EE 中间件或者基础架构不规范。在项目初始,新平台上面没有制定合理的规范,导致系统稳定性差。这会增加客户成本,所以花时间去制定合理的 Java EE 中间件环境规范是必须的。这项工作应与初始容量规划迭代相结合。

以下是我以往经验中观察到的问题的典型示例:

  • 单个 32 位 JVM 中部署了太多的 Java EE 应用程序。
  • 在一个中间件域中部署了太多的 Java EE 应用程序。
  • 缺乏适当的垂直扩展和未充分利用的硬件(例如,由一个或几个 JVM 进程驱动的流量)。
  • 过多的垂直扩展和过度使用的硬件(例如,太多的 JVM 进程与可用的 CPU 内核和 RAM)。
  • 缺乏环境冗余和故障转移功能。

从成本的角度来看,尝试将单个中间件和/或 JVM 用于许多大型 Java EE 应用程序可能会很有吸引力。但是,这可能会导致操作方面的噩梦和严重的性能问题,例如过多的 JVM 垃圾回收以及许多产生多米诺骨牌效应的场景(例如,“粘滞线程”会导致高业务影响,如 App A 导致 App B、App C 和 App D 失败,因为通常需要重新启动 JVM 才能解决问题)。

推荐建议

  • 项目团队应该花足够的时间为 Java EE 生产环境创建适当的操作模型。
  • 尝试为您的 Java EE 中间件规范找到一个良好的“平衡点”,以在业务中断的情况下为业务和运营团队提供适当的灵活性。
  • 避免在单个 32 位 JVM 中部署太多 Java EE 应用程序。中间件旨在处理许多应用程序,但是您的 JVM 可能遭受的影响最大。
  • 需要时,请选择 32 位 JVM 上的 64 位,但要结合适当的容量规划和性能测试以确保您的硬件将支持它。

3. Java 虚拟机垃圾回收过度

现在,从过多的 JVM 垃圾收集开始,跳到纯粹的技术问题。你们中的大多数人都熟悉这个著名(或臭名昭著)的 Java 错误:java.lang.OutOfMemoryError。这是 JVM 内存空间耗尽(Java 堆、本机堆等)的结果。

像 Oracle 和 IBM 这样的中间件供应商可以定期为您提供数十种涉及 JVM OutOfMemoryError 问题的支持案例,因此它在我们的列表中排名第三是不足为奇的。

请记住,垃圾回收问题不一定会表现为 OOM 条件。过多的垃圾回收可以定义为 JVM GC 线程(收集器)在短时间内执行过多的次要和/或主要收集,从而导致大量的 JVM 暂停时间和性能下降。有许多可能的原因:

  • 与 JVM 并发负载和应用程序的内存占用相比,选择的 Java 堆大小太小。
  • 使用了不适当的 JVM GC 策略。
  • 您的应用程序的静态和/或动态内存占用量太大,无法容纳在 32 位 JVM 中。
  • JVM OldGen 空间随着时间的推移正在泄漏(相当普遍的问题);数小时/天后,观察到过量的 GC(大量收集)。
  • JVM PermGen 空间(仅适用于 HotSpot VM)或本机堆随着时间的流逝而泄漏(相当普遍的问题);在应用程序动态重新部署后,随着时间的推移经常会观察到 OOM 错误。
  • 对于您的应用程序,YoungGen / OldGen 空间的比率不是最佳的(例如,生成大量短期对象的应用程序需要更大的 YoungGen 空间。创建大量长寿命/缓存对象的应用程序需要更大的 OldGen 空间)。
  • 用于 32 位 VM 的 Java 堆大小太大,无法为本机堆留出空间。尝试使用新的 Java EE 应用程序,创建新的 Java 线程或需要本地内存分配的任何计算任务时,问题可能会显示为 OOM。

在指责 JVM 之前,请记住实际的“根本”原因可能与我们的第 1 和第 2 原因有关。中间件环境过载将产生许多症状,包括过多的 JVM 垃圾收集。

正确分析与 JVM 相关的数据(内存空间、GC 频率、CPU 相关性等)将使您能够确定是否遇到问题。要进行更深入的分析以了解您的应用程序内存占用量,您将需要分析 JVM 堆转储和/或使用您选择的探查器工具(例如 JProfiler)对您的应用程序进行探查。

建议

  • 确保非常密切地监视和了解 JVM 垃圾收集。有几种商业和免费工具可以做到这一点。至少,您应该启用详细 GC,它将提供健康评估所需的所有数据。
  • 请记住,在开发或功能测试期间不太可能发现与 GC 相关的问题。正确的垃圾收集调整将需要您从同时进行的用户中执行负载并进行大量测试。通过本练习,您可以根据应用程序行为和负载水平预测来微调 Java Heap 内存占用量。

4. 与外部系统集成过多或过少

下一个导致 Java EE 性能下降的常见原因主要是适用于高度分布式的系统,通常用于电信 IT 环境。在这样的环境中,中间件域(例如,服务总线)将很少执行所有工作,而是将某些业务流程(例如产品鉴定、客户资料和订单管理)“委托”给其他 Java EE 中间件平台或旧版系统(例如大型机)。

这样的外部系统调用意味着客户端 Java EE 应用程序将触发套接字连接的创建或重用,以通过专用网络向/从外部系统写入和读取数据。根据实现和业务流程的性质,其中一些调用可以配置为同步或异步。需要特别注意的是,响应时间可能会随着时间的推移而变化,具体取决于外部系统的运行状况,因此,通过正确使用超时来保护 Java EE 应用程序和中间件非常重要。

在以下情况下,可以观察到主要问题和性能下降:

  • 以同步和顺序方式执行太多外部系统调用。这样的实现也完全暴露于其外部系统的不稳定和缓慢。
  • Java EE 客户端应用程序和外部系统之间的超时丢失或值太大。这将导致客户端线程卡住,从而导致完全的多米诺骨牌效应。
  • 可以正确实现超时,但不能对中间件进行微调以处理“不愉快”的路径。外部系统响应时间(或中断)的任何增加都将导致线程利用率和 Java 堆利用率的提高(待处理有效载荷数据的数量增加)。中间件环境和 JVM 必须以预测和处理“快乐”和“不快乐”路径的方式进行调整,以防止发生完全的多米诺骨牌效应。

最后,我还建议您花足够的时间进行负面测试。这意味着应该将问题条件“人为地”引入外部系统,以测试您的应用程序和中间件环境如何处理这些外部系统的故障。此练习也应在高容量情况下执行,以使您可以微调应用程序和外部系统之间的不同超时值。

5. 缺乏适当的数据库 SQL 调优和容量规划

下一个常见的性能问题对任何人都不应该感到惊讶:数据库问题。大多数 Java EE 企业系统都依赖关系数据库来执行从门户内容管理到订单供应系统的各种业务流程。坚实的数据库环境和基础将确保您的 IT 环境能够适当扩展以支持您的客户不断增长的业务。

以我的生产支持经验,与数据库相关的性能问题非常常见。由于大多数数据库事务通常都是通过 JDBC 数据源执行的(包括关系持久性 API,如 Hibernate),因此性能问题最初将表现为 Java EE 容器线程管理器中的“阻塞线程”。以下是我在过去十年中看到的与数据库有关的常见问题:

请注意,以 Oracle 数据库为例,因为它是我的 IT 客户使用的通用产品。

  • 孤立的、长期运行的 SQL。此问题将表现为线程卡住,通常表现为缺少 SQL 调整、缺少索引、执行计划不理想、返回的数据集太大等。
  • 表或行级别的数据锁定。特别是在处理两阶段提交事务模型(例如:臭名昭著的 Oracle In-Doubt Transactions)时,此问题尤其明显。在这种情况下,Java EE 容器可以留下一些未决的事务来等待最终提交或回滚,从而留下可能触发性能问题的数据锁,直到这些锁被删除为止。这可能是由于触发事件(例如中间件中断或服务器崩溃)而发生的。
  • 执行计划突然更改。我经常看到此问题,通常是某些数据模式更改的结果,这可能(例如)导致 Oracle 动态更新查询执行计划并触发严重的性能下降。
  • 缺乏适当的数据库设施管理。例如,Oracle 有几个方面需要检查,例如 REDO 日志、数据库数据文件等。诸如磁盘空间不足和日志文件未旋转之类的问题可能会引发主要的性能问题和断电情况。

推荐建议

  • 在此处进行包含负载和性能测试的正确容量规划对于微调数据库环境并检测 SQL 级别的任何问题至关重要。
  • 如果您使用的是 Oracle 数据库,请确保您的 DBA 团队定期检查 AWR 报告,尤其是在事件和根本原因分析过程中。其他数据库供应商也应执行相同的分析方法。
  • 利用 JVM 线程转储和 AWR 报告来查明运行缓慢的 SQL 和/或使用您选择的监视工具来执行此操作。
  • 确保花费足够的时间来加强数据库环境的“操作”方面(磁盘空间、数据文件、REDO 日志、表空间等)以及适当的监视和警报。否则,可能会使您的客户端 IT 环境面临严重的停机情况,并造成大量的停机时间。

6. 特定应用程序性能问题

回顾一下,到目前为止,我们已经看到了适当的容量计划、负载和性能测试、中间件环境规范、JVM 运行状况、外部系统集成以及关系数据库环境的重要性。但是 Java EE 应用程序本身呢?毕竟,您的 IT 环境可能拥有市场上最快的硬件,其中包含数百个 CPU 内核、大量的 RAM 和数十个 64 位 JVM 进程。但是如果应用程序实现不足,性能仍然会很糟糕。本节将重点介绍各种 Java EE 环境中最严重的 Java EE 应用程序问题。

我的主要建议是确保代码审查与发布管理流程一起成为常规开发周期的一部分。这将使您能够按照下面以及在主要测试和实施阶段之前查明主要实施问题。

线程安全代码问题

使用 Java 同步和非最终静态变量/对象时,必须格外小心。在 Java EE 环境中,任何静态变量或对象都必须是线程安全的,以确保数据完整性和可预测的结果。错误地将静态变量用于 Java 类成员变量,可能会导致负载下无法预测的结果,因为这些变量/对象在 Java EE 容器线程之间共享(例如,线程 B 可以修改线程 A 的静态变量值,从而导致意外和错误的行为)。应该将类成员变量定义为非静态变量,以保留在当前类实例上下文中,以便每个线程都有自己的副本。

在处理非线程安全的数据结构(例如 java.util.HashMap)时,Java 同步也非常重要。否则,可能会触发 HashMap 损坏和无限循环。处理 Java 同步时要小心,因为过度使用还可能导致线程卡住和性能下降。

缺少通信 API 超时

对于每个通信 API,实现和测试事务(套接字 read()write() 操作)和连接超时(Socket connect() 操作)非常重要。Java EE 应用程序与外部系统之间缺少适当的 HTTP / HTTPS / TCP IP ... 超时,可能会由于线程卡住而导致严重的性能下降和中断。正确的超时实现将防止在下游系统严重减速的情况下线程等待太长时间。

以下是一些较旧的和当前的 API(Apache 和 Weblogic)的示例:

通讯 API供应商协议超时代码段
commons-httpclient 3.0.1ApacheHTTP/HTTPSHttpConnectionManagerParams.setSoTimeout(txTimeout); // 交易超时
HttpConnectionManagerParams.setConnectionTimeout(connTimeout); // 连接超时
axis.jar (v1.4 1855)ApacheWS 通过 HTTP/HTTPS请注意,AXIS 1.x 版存在 SSL 套接字创建的已知问题,该问题忽略了指定的超时值。解决方案是重写 client-config.wsdd 并将 HTTPS 传输设置为 <transport name="https" className="java:org.apache.axis.transport.http.CommonsHTTPSender" />
((org.apache.axis.client.Stub) port).setTimeout(timeoutMilliseconds); // 交易和连接超时
WLS103 (旧的 JAX-RPC)OracleWS 通过 HTTP/HTTPS// 交易和连接超时
((Stub) servicePort)._setProperty("weblogic.webservice.rpc.timeoutsecs", timeoutSecs);
WLS103 (JAX-RPC 1.1)OracleWS 通过 HTTP/HTTPS((Stub) servicePort)._setProperty("weblogic.wsee.transport.read.timeout", timeoutMills); // 交易超时
((Stub) servicePort)._setProperty("weblogic.wsee.transport.connection.timeout", timeoutMills); // 连接超时

I/O、JDBC 或关系持久性 API 资源管理问题

在实现原始 DAO 层或使用关系持久性 API(例如 Hibernate)时,正确的编码最佳实践很重要。目的是确保正确关闭会话/连接资源。必须在 finally {} 块中关闭此类与 JDBC 相关的资源,以正确处理任何故障情况。否则可能导致 JDBC 连接池泄漏,并最终导致线程卡住和完全中断的情况。

相同的规则适用于 I/O 资源,例如 InputStream。当不再使用时,需要适当的关闭;否则,它可能导致 Socket / File Descriptor 泄漏并导致 JVM 完全挂起。

缺乏适当的数据缓存

性能问题可能是由于重复和过多的计算任务而导致的,例如 I/O / 磁盘访问、关系数据库中的内容数据以及与客户相关的数据。具有合理内存占用量的静态数据应正确地缓存在 Java Heap 内存中或通过数据缓存系统缓存。

静态文件(例如属性文件)也应缓存,以防止过多的磁盘访问。简单的缓存策略可以对 Java EE 应用程序的性能产生非常积极的影响。

在处理 Web 服务和与 XML 相关的 API 时,数据缓存也很重要。此类 API 可能会产生过多的动态类加载和 I/O / 磁盘访问。确保遵循此类 API 最佳做法,并在适用时使用适当的缓存策略(Singleton 等)。我建议您阅读有关该主题的 JAXB 案例研究

数据缓存过多

具有讽刺意味的是,尽管数据缓存对于确保适当的性能至关重要,但它也可能会导致严重的性能问题。为什么?好吧,如果您尝试在 Java Heap 上缓存太多数据,那么您将在过多的垃圾回收和 OutOfMemoryError 条件下苦苦挣扎。目标是在数据缓存、Java 堆大小和可用硬件容量之间找到适当的平衡(通过您的容量计划过程)。

这是我的一位 IT 客户提出的一个问题案例的示例:

  • 从 Weblogic 门户应用程序观察到非常差的性能。
  • 实施数据缓存是为了提高性能并产生最初的积极影响。
  • 他们在产品目录中添加的产品越多,数据缓存的需求就越大,Java Heap 内存也就越大。
  • 最终,IT 团队不得不升级到具有每个 JVM 进程 8 GB 的 64 位 JVM,以及更多的 CPU 内核。
  • 最终,这种情况是不可持续的,必须对设计进行审查。
  • 最终的解决方案最终在 Java EE 中间件和 JVM 之外通过单独的硬件使用了分布式数据缓存系统。

从这个故事中要记住的重要一点是,当需要太多的数据缓存来达到适当的性能水平时,就该回顾整体解决方案和设计了。

日志过多

最后但并非最不重要的一点是:过多的日志记录。确保在 Java EE 应用程序实现中正确记录日志是一种很好的做法。但是,请注意在生产环境中启用的日志记录级别。过多的日志记录将触发服务器上的高 IO,并增加 CPU 使用率。对于使用较旧硬件的较旧环境或处理大量并发卷的环境而言,这尤其可能成为问题。我还建议您实现“可重载”日志记录级别功能,以便在日常生产支持中需要时打开/关闭额外的日志记录。

7. Java EE 中间件调优问题

重要的是要认识到您的 Java EE 中间件规范可能足够但可能缺乏适当的调整。如今,大多数 Java EE 容器都可以根据您的应用程序和业务流程需求为您提供多种调优机会。

如果无法执行适当的调整和最佳实践,可能会使 Java EE 容器处于非最佳状态。我强烈建议您在适用时查看并实施适当的 Java EE 中间件供应商建议。

在高级视图下面找到所需内容的样本清单。

8. 主动监控不足

缺乏监控,并不会带来实际性能问题,但它会影响你对 Java EE 平台性能和健康状况的了解。最终,这个环境可以达到一个破发点,这可能会暴露出一些缺陷和问题(JVM 的内存泄漏,等等)。

以我的经验来看,如果一开始不进行监控,而是运行几个月或者几年后再进行,平台稳定性将大打折扣。

也就是说,改善现有的环境永远都不会晚。下面是一些建议:

  1. 复查现有 Java EE 环境监测能力和找到需改进的地方。
  2. 监测方案应该尽可能的覆盖整个环境。
  3. 监控方案应该符合容量规划进程。

9. 公共基础设施硬件饱和

性能问题的另一个常见原因是硬件饱和。当在现有硬件上部署太多 Java EE 中间件环境及其 JVM 进程时,通常会观察到此问题。太多的 JVM 进程与物理 CPU 内核的可用性可能是一个真正的问题,会破坏您的应用程序性能。同样,随着客户业务的增长,容量规划过程还应注意硬件容量。

我的主要建议是研究硬件虚拟化。如今,这种方法非常普遍,并具有许多好处,例如减少了物理服务器、数据中心的大小、每个虚拟主机专用的物理资源、快速实现并降低了客户成本。每个虚拟主机的专用物理资源非常重要,因为您想要的最后一件事是一个 Java EE 容器,由于过度的 CPU 使用率而导致所有其他容器崩溃。

10. 网络延迟

最后一个影响性能问题的是网络。网络问题时不时都会发生,如路由器、交换机和 DNS 服务器失败。更常见的是在一个高度分散的 IT 环境中定期或间歇性延迟。

下图中的例子是一个位于同一区域的 Weblogic 集群通信与 Oracle 数据库服务器之间的延迟:

性能问题的最后一个来源是网络。主要的网络问题可能会不时发生,例如路由器、交换机和 DNS 服务器故障。但是,观察到的更常见的问题通常是由于在高度分布式的 IT 环境上工作时有规律或间歇性的延迟。下图突出显示了一个 Weblogic 群集的两个地理区域与仅位于一个地理区域中的 Oracle 数据库服务器通信的网络延迟差距的示例。

间歇性或常规延迟问题肯定会触发一些主要的性能问题,并以不同的方式影响 Java EE 应用程序:

  • 由于大量的获取迭代(跨网络来回转发),使用具有大型数据集的数据库查询的应用程序完全暴露于网络延迟。
  • 处理来自外部系统的大数据有效载荷(例如大 XML 数据)的应用程序也暴露于网络等待时间,这可能会在发送和接收响应时触发间歇的高响应时间。
  • Java EE 容器复制过程(集群)可能会受到影响,并使其故障转移功能(例如,多播或单播数据包丢失)面临风险。

诸如 JDBC 行数据“预取”、XML 数据压缩和数据缓存之类的调整策略可以帮助减轻网络延迟。但是,在首先设计新的 IT 环境的网络拓扑时,应仔细检查此类延迟问题。

我希望本文能帮助您了解在开发和支持 Java EE 生产系统时可能遇到的一些常见性能问题和压力点。由于每个 IT 环境都是唯一的,所以我并不希望每个人都会面临完全相同的问题。因此,我邀请您发表评论并分享您对该主题的看法。

英文链接


说明:本文内容基于 Java EE 技术体系,部分提及的技术(如 32 位 JVM、Axis 1.x、WebLogic 10.3、Java EE 规范等)可能已过时或不再主流(现多转向 Jakarta EE 及 64 位环境)。文中建议的核心原则(如容量规划、监控、超时设置等)依然适用,但具体实施请参考当前最新的技术版本和最佳实践。