我该如何确定Hotspot JVM为什么会重新编译已经进行了JIT编译的代码?

17

我正在尝试为一个对延迟敏感的Java应用程序编写热身例程,以优化否则会因为动态类加载和JIT(主要原因)而减慢的前几个事务。

我面临的问题是,即使我的热身代码加载了所有的类并通过多次调用它们进行了练习(至少100次-XX:CompileThreshold),但稍后当实际用户登录时,这些相同的函数仍然被标记为“非入口”,需要重新编译,这会导致延迟问题。

JVM标志如下(我只添加了-XX:+PrintCompilation -verbose:class tp troubleshoot,其他标志是遗留的):

-Xms5g -Xmx5g -server -XX:+AggressiveHeap -XX:+UseFastAccessorMethods -XX:+PrintGCDetails -XX:CompileThreshold = 100 -XX:-CITime -XX:-PrintGC -XX:-PrintGCTimeStamps -XX:+PrintCompilation -verbose:class

#Warmup happens here
  12893 2351       my.test.application.hotSpot (355 bytes)
#Real user logs on here
 149755 2351      made not entrant  my.test.application.hotSpot (355 bytes)
 151913 2837       my.test.application.hotSpot (355 bytes)
 152079 2351      made zombie  my.test.application.hotSpot (355 bytes)

热身后不会发生类加载(尽管我可以看到之前有类加载,所以标记有效)。

似乎该函数获得了一个新的ID(2351与2837),这意味着JVM认为它是“不同的”。

那么我该如何确定JVM为什么要重新编译这个函数呢?

我猜这归结于如何确定为什么ID发生了变化?标准是什么?

我尝试将尽可能多的方法和类标记为私有,但没有用。

这是JRE 1.6.0_45-b06。

有关如何故障排除或获取更多信息的任何提示都将不胜感激!:)


最近我们有一个关于Hotspot JVM的讲座,Oracle的人过来给我们讲解了相关内容。他们说JVM中有两个编译阶段:第一个是在代码运行2000次(默认值)后进行的编译,另一个则是在代码运行10000次后对代码进行更多优化的编译阶段...我不是专家,但也许这就是为什么代码会被重新编译并标记旧的编译代码无效的原因...? - fast-reflexes
@fast-reflexes 听起来像是“分层编译”。据我所知,默认情况下它是未启用的。 - ArtemGr
如果你关心延迟问题,立即停用Java 6。Java 8 至少快40-50%。 - kittylyst
@user268744 确保您的新平台在裸机上运行。虚拟化运行将导致非常相似的性能特征,就调度不确定性而言 - 但由于它们发生在(虚拟化的)操作系统以下,大多数工具都无法检测到它们,这是一个额外的问题。 - kittylyst
显示剩余3条评论
1个回答

14

为了后人留存,一旦我阅读了热点JVM的一些源代码,这就变得相当简单了。

以下标志将指出导致函数被取消优化并重新编译的确切源代码行:

-XX:+TraceDeoptimization -XX:+WizardMode -XX:+PrintNativeNMethods -XX:+PrintDependencies -XX:+DebugDeoptimization -XX:+LogEvents

通常它是这样的一个if语句。

void function (Object object){
    if ( object == null ){
        // do some uncommon cleanup or initialization
    }
    do_stuff();
}

假设我的预热代码从未触发if语句。

我曾经认为整个函数将一次性编译,但是当JIT C2编译器决定为该函数生成本机代码时,它不会为if语句生成任何代码,因为这条代码路径从未被执行过。

它只会生成一个条件分支,在C2编译器线程中生成陷阱和异常处理程序。我认为这是因为本地代码缓存相对较小,所以JVM编写者不希望用可能无用的代码来填充它。

无论如何,如果语句为真(即对象为空),则函数将立即且无条件地触发此异常处理并重新编译(导致冻结/延迟约为几毫秒)。

当然,我的预热代码不会以与生产完全相同的方式调用每个函数,我敢说在任何复杂的产品中,这几乎是不可能实现的,而且对于维护来说也是一场噩梦。

这意味着为了有效地预热Java应用程序,需要由预热代码调用代码中的每个if语句。

因此,我们将简单地放弃“预热”Java代码的想法,因为它不像某些人所认为的那么简单。

出于以下原因,我们将重新编写部分应用程序以支持运行数周/数月:

  • 更易维护(我们不需要在预热期间模拟生产并保持其更新)
  • JIT不会根据我们的塑料模拟完成,而是根据实际生产行为(即使用JIT进行设计,而不是与之对抗)

从长远来看,客户可能会支付重写C/C++等语言的费用,以获得一致的低延迟,但这是另一天的事情。

编辑:让我补充一下,更新到Hotspot JVM的新版本或“调整”Hotspot JVM参数永远无法解决此问题。它们都是幌子。事实是,Hotspot JVM从未为可预测的低延迟编写,这个缺点无法在Java用户空间内解决。


放弃“热身”的想法可能是有史以来最好的主意。请注意,如果热身涉及到每个条件分支,则可能会给HotSpot优化器留下关于条件结果可能性的错误印象。这也可能导致性能不佳。顺便说一句,升级到比Java 6更高版本的JVM也可能有所帮助,并且比将应用程序重写为C / C ++更便宜。 - Holger
@Holger,升级到较新的JVM也没有帮助,因为Java在这个方面基本上是有缺陷的。它从来没有被编写用于可预测的低延迟。无论升级还是Java“调优”,都无法解决这个缺点。 - user268744
3
这是不正确的。没有“根本性缺陷”。只是这个特定的JVM并不是为可预测的低延迟而设计的(而提供此功能的专门JVM通常很昂贵)。但主要问题似乎是你正在关注反优化事件,而不是你声明的目标。如果你想要可预测的延迟,可以简单地关闭JIT编译器。但我宁愿通过将最新的JVM和最新的硬件结合起来,使最坏情况低于所需阈值,从而更加关注低延迟 - Holger
2
即使您使用HotSpot的dev树,它也永远不会为软实时程序创建一个体面的环境 - 它从未被设计为这样做。如果您需要软实时约束条件,则应使用考虑到这些因素的东西,特别是GC方面的东西。有一些JVM确实可以做到这一点,因此我不完全确定为什么有人想要与HotSpot斗争以实现这一点..显然,整个事情与Java语言毫无关系.. - Voo
你必须拥有一个 debug 版本的 JVM 才能使用这些标志!只是让你知道 :) - jocull

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接