JDK7 应用程序在正常运行一段时间后变慢

JDK7 Application is getting slow after some Uptime

我们使用 Hibernate、Spring 等多个库在 JBoss 上部署了一个大型 JDK7 应用程序。服务器初始启动后,应用程序 运行 按预期运行,但在正常运行一段时间后变得非常慢。

使用分析器,我们发现每次应用程序的某些方面都在变慢,但并不总是相同的方面。在一个 运行 中,hibernate flush 可能会变慢,而在另一个 运行 中,可能是来自 Spring 的一些 DI 代码。

那里发生了什么事?

JDK7 中有一个关于 CodeCache 内存区域的错误,这给我们带来了非常非常严重的打击。

说明

基本上 Java 启动并使用即时编译 (JIT) 在运行时只编译字节码的所需部分。这使 JVM 能够在执行期间反编译和重新编译某些代码片段。如果 JVM 确定某个代码片段的初始编译不是最优的,就会发生这种情况。 Oracle 在 JDK 7 中引入了一个名为 tiered compilation 的特性,它允许 VM 做到这一点。

JVM中编译后的代码存放在CodeCache内存区。在 JDK6 之前,默认情况下该区域将被填满,一旦达到 100%,JIT 将停止编译并将错误打印到控制台,但是应用程序将 运行 相同和以前一样:所有已经编译的都将保持编译状态,所有未编译的都将在解释模式下执行(大约慢 100 倍)

此选项名为 CodeCacheFlushing,自 JDK7u4 起默认启用。这个想法是,一旦 CodeCache 已满,编译代码中最少使用的部分就会从内存中清除,以便为其他代码片段腾出空间。这将使 JDK6-default-behaviour(停止全部编译)过时。它还允许使用更小的 CodeCache 区域(在 JDK7 中,如果启用分层编译,CodeCache 为 48M default/96M)。

bug来了。在 JDK7 中,一旦 CodeCache 变满,JIT 就会停止。接下来是 CodeCache 区域的刷新。而已。刷新完成后应该重新启用 JIT,但这并没有发生。此外,没有警告打印到控制台。更糟的是:在禁用 JIT 之前,大约有一半已编译的代码被丢弃。

与 JDK6 相比,在 JDK6 中所有快速的东西都将保持快速并且只解释新代码,在 JDK7 中你实际上丢失了已经编译和优化的代码!您的应用程序中所有表现良好的突然部分将停止这样做。应用程序的哪些部分变慢了,这让分析器几乎不可能跟踪那个错误:有时用于刷新的休眠代码变慢,其他时候,它是 spring DI 代码或你自己的应用代码。

你受影响了吗?

您可以使用分析器(JProfiler/YourKit) 或JConsole(JVisualVM 不会)来监控CodeCache 内存区域的内存消耗。通常,CodeCache 数量 committed 将非常接近 used 数量(例如,committed 为 23mb,已用为 22mb)。当您的应用程序运行时,committedused 会上升,直到 committed 达到 max。届时 used 将急剧下降至 max 的 1/2 - 2/3。之后,used 将不再增长。这就是错误会攻击你的地方。在 JConsole 中,它将如下所示:

为什么是我而不是其他人?

很有可能,您正在使用 JBoss。 Oracle 很快发现有些东西不应该,默认情况下禁用 tiered compilation - 但 Red Hat 以其无限的智慧决定,它知道得更好并重新启用它。基本上我们的 webapp 在 Weblogic 上运行良好,只有 JBoss 受到影响,因为没有分层编译(在 weblogic 中未启用),CodeCache 的增长非常小,即使经过数周的操作,我们也从未真正达到 48mb 的阈值。

我能做什么?

首先,确定这个错误是否会影响您。其次,让虫子更难伤害你。如果你禁用 CodeCacheFlushing 至少打这个错误不会让事情变得比以前更糟。停止 tiered compilation 会降低 bug 攻击你的可能性,就像增加可用的 CodeCache-Memory 的数量一样。

您可以随时尝试切换到 JDK8,这似乎不受影响,而且如果 CodeCache 运行 已满,您也可以在软件中实施监控以警告您。

TL;DR

  • 在JDK7中从不启用分层编译(默认禁用,在JBoss中启用)
  • in JBoss 7 always set PRESERVE_JAVA_OPTS=true in standalone.conf
  • 始终 禁用 CodeCacheFlushing (-XX:-UseCodeCacheFlushing)
  • 始终 将足够的内存装入 CodeCache (-XX:ReservedCodeCacheSize=xxM)。