Java 性能调优:调整 GC 线程获得最佳性能

405 阅读7分钟

垃圾回收(GC)在 Java 内存管理中扮演着重要角色。它有助于回收不再使用的内存。垃圾回收器使用自己的线程集来回收内存,这些线程被称为 GC 线程。 有时 JVM 可能会出现 GC 线程过多或过少的情况。在本文中,我将讨论 JVM 为何会出现 GC 线程过多或过少的情况、其后果以及解决这些问题的潜在方法有哪些。

如何查找应用程序的 GC 线程数

你可以通过以下步骤进行线程转储分析来确定应用程序的 GC 线程数:

  1. 从生产服务器捕获线程转储。
  2. 使用线程转储分析工具分析转储。
  3. 或者使用下图中这个工具 fastthread ,它能够立即报告 GC 线程数,如下图所示。

fastThread_tool 报告 GC 线程数

如何设置 GC 线程数

你可以通过设置以下两个 JVM 参数手动调整 GC 线程的数量:

  • -XX:ParallelGCThreads=n:设置垃圾回收器在并行阶段使用的线程数。
  • -XX:ConcGCThreads=n:控制垃圾回收器在并发阶段使用的线程数。

默认的 GC 线程数是多少?

如果你没有使用上述两个 JVM 参数显式设置 GC 线程数,那么默认的 GC 线程数将根据服务器/容器中的 CPU 数量推导得出。

  • -XX:ParallelGCThreads 默认值:在 Linux/x86 机器上,它是根据以下公式推导得出的:
if (num of processors <=8) {
   return num of processors;
} else {
  return 8+(num of processors-8)*(5/8);
}

因此,如果你的 JVM 运行在一台有 32 个处理器的服务器上,那么 ParallelGCThread 的值将是:23(即 8 + (32 – 8)*(5/8))。

  • -XX:ConcGCThreads 默认值:它是根据以下公式推导得出的:
max((ParallelGCThreads+2)/4, 1)

因此,如果你的 JVM 运行在一台有 32 个处理器的服务器上,那么:

  • ParallelGCThread 的值将是:23(即 8 + (32 – 8)*(5/8))。
  • ConcGCThreads 的值将是:6(即 max(25/4, 1))。

JVM 会意外出现过多的 GC 线程吗?

你的 JVM 可能会在你不知情的情况下意外出现过多的 GC 线程。这通常是因为默认的 GC 线程数是根据服务器或容器中的 CPU 数量自动确定的。

例如,在一台有 128 个 CPU 的机器上,JVM 可能会为垃圾回收的并行阶段分配大约 80 个线程,为并发阶段分配大约 20 个线程,总共大约 100 个 GC 线程。

如果你在这台 128 个 CPU 的机器上运行多个 JVM,每个 JVM 可能都会出现大约 100 个 GC 线程。这会导致过度的资源使用,因为所有这些线程都在争夺相同的 CPU 资源。在容器化环境中,这个问题尤为明显,因为多个应用程序共享相同的 CPU 核心。 它会导致 JVM 分配比实际需要更多的 GC 线程,从而降低整体性能。

为什么过多的 GC 线程是个问题?

虽然 GC 线程对于高效的内存管理至关重要,但过多的 GC 线程可能会给你的 Java 应用程序带来显著的性能挑战。例如:

  1. 增加上下文切换:当 GC 线程数过高时,操作系统必须频繁地在这之间切换。这会导致上下文切换开销增加,更多的 CPU 周期被用于管理线程,而不是执行应用程序的代码。因此,你的应用程序可能会显著变慢。
  2. CPU 开销:每个 GC 线程都会消耗 CPU 资源。如果同时有过多的线程处于活动状态,它们会争夺 CPU 时间,为应用程序的主要任务留下的处理能力就会减少。这种竞争可能会降低应用程序的性能,尤其是在 CPU 资源有限的环境中。
  3. 内存争用:由于有过多的 GC 线程,可能会增加对内存资源的争用。多个线程同时尝试访问和修改内存可能会导致锁争用,这会进一步减慢应用程序的速度,并可能导致性能瓶颈。
  4. 增加的 GC 暂停时间和较低的吞吐量:当有过多的 GC 线程处于活动状态时,垃圾回收过程可能会变得效率低下,导致较长的 GC 暂停时间,应用程序会暂时停止。这些延长的暂停可能会导致应用程序出现明显的延迟或卡顿。此外,由于更多的时间用于垃圾回收而不是处理请求,应用程序的整体吞吐量可能会降低,每秒处理的事务或请求数量减少,影响其在负载下扩展和执行的能力。
  5. 更高的延迟:由于过多的线程导致的增加的 GC 活动可能会导致响应用户请求或处理任务的延迟增加。对于需要低延迟的应用程序(如实时系统或高频交易平台),即使是轻微的延迟也可能产生严重后果。
  6. 收益递减:在某个点之后,增加更多的 GC 线程并不能提高性能。相反,它会导致收益递减,管理这些线程的开销超过了更快的垃圾回收带来的好处。这可能会导致应用程序性能下降,而不是预期的优化。

为什么过少的 GC 线程也是个问题?

虽然过多的 GC 线程可能会导致性能问题,但过少的 GC 线程对于你的 Java 应用程序来说同样是个问题。原因如下:

  1. 更长的垃圾回收时间:由于可用的线程较少,垃圾回收过程可能需要显著更长的时间才能完成。由于线程数量有限,回收内存所需的时间增加,导致 GC 暂停时间延长。
  2. 增加的应用程序延迟:更长的垃圾回收时间会导致应用程序延迟增加,尤其是对于需要低延迟操作的应用程序。用户可能会体验到延迟,因为应用程序在等待垃圾回收完成时变得无响应。
  3. 吞吐量降低:较少的 GC 线程意味着垃圾回收器不能高效地工作,导致整体吞吐量降低。你的应用程序可能会每秒处理更少的请求或事务,影响其在负载下扩展的能力。
  4. CPU 利用率低效:由于 GC 线程过少,在垃圾回收期间 CPU 核心可能无法充分利用。这可能会导致资源利用低效,因为一些核心处于空闲状态,而其他核心则负担过重。
  5. 增加 OutOfMemoryErrors 和内存泄漏的风险:如果由于线程过少,垃圾回收器无法跟上内存分配的速度,它可能无法及时回收内存。这会增加应用程序内存不足的风险,导致 OutOfMemoryErrors 和潜在的崩溃。此外,不足的 GC 线程可能会加剧内存泄漏,通过减慢垃圾回收过程,允许更多的未使用对象在内存中积累。随着时间的推移,这可能会导致过多的内存使用,并进一步降低应用程序的性能。

优化 GC 线程数的办法

如果由于 GC 线程过多或过少,你的应用程序出现了性能问题,可以考虑使用上述提到的 JVM 参数手动设置 GC 线程数:

  • -XX:ParallelGCThreads=n
  • -XX:ConcGCThreads=n

在将这些更改应用到生产环境之前,分析你的应用程序的 GC 行为至关重要。首先收集和分析 GC 日志,使用工具进行分析。此分析将帮助你确定当前的线程数是否会导致性能瓶颈。根据这些见解,你可以对 GC 线程数进行明智的调整,而不会引入新的问题。

  • 注意:始终先在受控环境中测试更改,以确认它们确实提高了性能,然后再将其推广到生产环境中。

总结

平衡 GC 线程的数量是确保 Java 应用程序顺利运行的关键。通过仔细监控和调整这些设置,你可以避免潜在的性能问题,保持应用程序高效运行。