内容


IBM JVM for Linux JIT 诊断简介

Comments

简介

“一次编写,到处运行”(WORA)的原则只有在将纯 Java™ 的字节码从一个平台的某个特定版本的 Java 虚拟机(JVM)移植到另外一个不同平台上完全相同版本的 JVM 上时才适用。

然而,有时这种迁移过程并不是无缝的。可以对这个迁移过程产生影响的一个因素是不同供应商的优化技术的内部实现之间存在差异。

在将一个 Java 程序从使用 Sun JDK 的平台迁移到使用 IBM JDK 的平台上时,重要的是要了解在这两个供应商的 JVM 中使用的优化技术之间的差异,这些差异可能会对程序产生影响,以及如何通过调节 IBM JVM 中可用的优化机制来获得更好的性能。

本文着重介绍对于在 JIT 中碰到的问题的诊断,在从 Sun HotSpot JVM 迁移到 IBM 基于 JIT 的 JVM 时可能会碰到这些问题。

虽然本文中介绍的大部分内容对于 IBM JVM 1.3.1 和 1.4.2 都是普遍适用的,但是尤其适用于 IBM JVM 1.3.1 和 JVM 1.4.2 for Linux,包括 POWER 和 PowerPC 架构上的 Linux。

在 Sun 的 JVM 1.3.1 中,JIT 和 HotSpot 编译器都已经包含其中了,可以使用 -server 或 -hotspot 选项(默认为客户机 HotSpot VM)来调用,使用 -classic 选项调用 JIT,-hotspot 选项是隐含的默认值。在 Sun JDK 1.4.1 及之后的版本中,只能使用 HotSpot 了。IBM JVM 1.3.1 和 1.4.2 使用了 JIT 和 MMI 的组合,这是默认的运行模式,可以实现与 HotSpot 相同的功能。

有关 IBM JVM 的更详细信息,请参阅 IBM developer kits - diagnosis documentation:http://www.ibm.com/developerworks/java/jdk/diagnosis/。

Sun Java HotSpot VM

Java HotSpot VM 是 Sun Microsystems 的 JVM 实现。虽然 Java HotSpot VM 与 Java II Platform VM 实现了相同的规范,并且运行相同的字节代码,但是它已经重新进行了设计,以利用一些新技术,例如自适应式优化,从而提高 JVM 的性能。Java Hotspot 中并没有包含 JIT 编译器插件,而是对程序中出现最多的方法(称为“热点”)进行优化、内联并执行其他优化选项。这意味着在 Java 字节码中首先传递的内容被解释为不存在本地代码编译器(例如 JIT)的字节码。如果代码是作为程序中的热点出现的,那么 HotSpot 编译器就会将字节码编译为本地代码,然后将其保存在高速缓存中,并同时进行其他方面的优化。与 JIT(只能自行使用,没有采用自适应式优化机制) 相比,选择编译方法的优点是字节码编译成本地代码之后,在那些可以从优化机制中获益的领域中,生成高度优化的本地代码可以节省很多时间,同时可以不用理会那些在解释模式中运行的代码。

IBM JVM JIT 和 MMI

用来提高 Java 程序性能的工具是 Just-In-Time(JIT)编译器。JIT 是一个代码生成器,它将 Java 字节码转换成宿主平台的本地代码。Java 程序使用 JIT 调用时的运行速度通常都比使用解释程序执行字节码时的速度更快。

当 JVM 启动时,会有很多方法被加载到 JVM 中并执行。

如果 JIT 被禁用了,那么 JVM 启动会很快,但是在大部分情况中,运行程序的速度都会与解释字节码的速度一样慢。如果 JIT 被启用了,但是并没有使用 JIT 的一些自适应式优化机制,就会试图在启动时对所有方法进行编译。对于诸如 applet 之类的小程序来说,当启动 JVM 的时间长于运行程序的时间时,就可能会成为问题。

IBM JVM Mixed Mode Interpreter(MMI)与 JIT 紧密地结合在一起,它可以减轻这个问题。它可以用于与 HotSpot VM 相同的目的,将编译方法延伸到 JVM 的生命期之外。除了其他特性之外,MMI 还可以计算一个特定的方法被执行了多少次。为了达到平衡的性能,MMI 对于 IBM JVM 支持的每种平台都有一个默认的上限计数器,这是经过仔细选择的,并经过了广泛的测试和研究。

每执行一次方法,该方法在 MMI 中的上限计数器就减 1。当一个方法的上限计数器达到零(0)时,就可以使用 JIT 将这个方法编译成本地代码了。因此,与 HotSpot VM 一样,高频率使用的方法 ——“热点”—— 都会在启动 JVM 之后由 JIT 进行编译,而低频度使用的方法则会在之后进行编译,或者可能在整个 JVM 进程的生命周期中都不会被编译。

IBM JVM 1.3.1 和 1.4.2 还为改进某些 Java 程序的启动时间而提供了一个非标准的选项 -Xquickstart。-Xquickstart 选项会导致使用优化选项的子集来运行 JIT;也就是说,快速编译的方法。这个选项适用于短期运行的程序,特别是那些执行时间不集中在少量的“热点”方法中的程序。如果 -Xquickstart 选项用在长时间运行的程序上,而这些程序又包含一些热点方法,那么这个选项就可能会导致性能的降低。

与其他非标准的 -X 选项一样,-Xquickstart 选项的实现和存在都可能不加通知就会修改。

可以减小 MMI 上限计数器,从而加速 JIT 编译一个方法的过程;或者增大它以达到相反的效果。这是通过使用一个环境变量 IBM_MIXED_MODE_THRESHOLD 实现的,与其他环境变量一样,需要在运行 JVM 进程的 shell 实例中进行设置。自然,这个变量是在启动 JVM 进程之前设置的,对于在 JVM 中执行的所有方法都是有效的。

将 IBM_MIXED_MODE_THRESHOLD 设置为 0 可以禁用 MMI,这样,所有的方法在首次加载到 JVM 中时就可以立即使用 JIT 进行编译。

IBM JVM 运行时模式如下:

  • MMI 和 JIT 都启用

    这是默认的 IBM JVM 设置。

  • MMI 禁用,JIT 启用

    所有的方法都是在首次运行之前编译的,即 JVM 的启动时间可能会很慢,但是后来的性能会很好。

  • MMI 和 JIT 都禁用

    JVM 是一个纯解释系统。所有的代码都不会进行编译。禁用 JIT 同时还会自动禁用 MMI,并将 IBM_MIXED_MODE_THRESHOLD 设置为 0,这样可以有效地禁用 MMI,但是不会禁用 JIT。

MMI 是一个非常有效的解释器,利用宿主平台上的程序集代码来达到最优的优化。虽然 JIT 并不是 JVM 的一个集成部分,但却以一个共享库(libjitc)的形式提供了,它与 MMI 紧密地结合在一起,JIT 和 MMI 是 IBM JVM 中紧密结合的两种技术。

在 Java HotSpot VM 或经典的 JIT 模式中使用的是 Sun JVM 1.3.1,Sun 的 JIT 实现并不与 IBM 的 JIT 完全相同;Sun HotSpot JVM 1.4.1 中根本没有 JIT。当将 Java 程序从 Sun JVM 1.3.1 或 Sun JVM 1.4.1 迁移到 IBM JVM 上时,在一些非常罕见的情况中可能会出现问题,问题的范围从性能的下降,到代码会产生不正确的结果,以及 JVM 的挂起、崩溃和出现一些异常。

JIT 诊断

本节将介绍在问题发生时用来调试和诊断 IBM JVM 的 JIT 和 MMI 的技术。上一节简要介绍了 JIT,它是 IBM JVM 的一个基本部分。

在任何给定的时刻,JVM 进程包含一些可执行文件和一些使用 JIT 编译的代码,它们被动态链接到 JVM 中的 MMI 方法的封装程序上。JIT 编译的本地机器代码被放置到 JVM 本地数据内存段中,这样就可以增加 JVM 进程的本地内存占用,并且 MMI 封装程序被修改为指向编译后的代码。

因此,JIT 对于 Java 程序的执行流程会产生很大的影响。

在将程序从一个平台上迁移到另外一个平台上碰到的问题如下:

  • 死锁挂起
  • 一直产生不正确的结果
  • 结果不一致
  • 不正常结束
  • 无限循环
  • 内存泄漏

对问题原因要考虑的第一件事情是 JIT。

尽快在判断问题原因时,确定 JIT 是否是问题的根源非常重要,这是由于 3 个原因:

  1. 问题可能是由于 JIT 在 JVM 中给定的活动角色而引起的。
  2. JIT 调试与其他类型的问题判断技术有很大的不同。
  3. JIT 调试过程可能会非常耗费时间,而且非常复杂,通常需要高级的专门技术。

确定是否是 JIT 问题

在某些情况下,从问题的特性可以很清楚地看出就是 JIT 的问题。例如,在 JVM 终止时带有 Javadump(在 Linux 上,Javadump 的文件名的格式为 javacore.YYYYMMDD.HHMMSS.PID.txt )或 Linux core 文件的情况下,从 Javadump 中的跟踪信息或 gdb 对 Java 可执行文件和 core 文件的输出信息中,可以很清楚地判断出 JIT 就是产生问题的原因。在某些情况下,会直接显示导致 JVM 进程死亡的信号是在 libjitc.so 中接收到的。在另外一些情况下,在 JVM 进程的代码中,但是在该进程的已编译代码之外,会产生崩溃或挂起,这可以说明问题是由于 JIT 编译的代码产生的。

然而,在大部分情况下,并没有清晰的迹象表明 JIT 是否是问题的源头。因此,给定 JIT 的重要性后,在问题判断过程中的第一个步骤应该是禁用 JIT,除非这显然不是一个与 JIT 相关的问题。即使在有迹象表明 JIT 就是问题的原因的情况下,最好也通过禁用 JIT 进行一下验证,并重新运行一下禁用了 JIT 的程序。

要禁用 JIT,首先请检查一下当前环境变量 JAVA_COMPILER 的设置,然后将其设置为 NONE。例如,对于 Bourne Again Shell (bash)或 Korn shell(ksh),设置如下:

export JAVA_COMPILER=NONE

对于 csh,设置如下:

setenv JAVA_COMPILER NONE

另外一种禁用 JIT 的方法是向 java 命令传递 -D 参数,将 java.compiler 设置为 NONE,从而覆盖默认的环境变量置:

java -Djava.compiler=NONE <myapp>

JIT 默认是被启用的。要验证 JIT 是否被启用了,可以使用 java 命令的 -version 选项:

java -version

如果没有启用 JIT,就会显示一个包含如下内容的消息:

JIT disabled

如果启用了 JIT,就会显示一个包含如下内容的消息:

JIT enabled: jitc

如果指定了 JAVA_COMPILER="" -Djava.compiler="",那就禁用了 JIT。如果 JAVA_COMPILER 没有设置,如下:

unset JAVA_COMPILER

那么 JIT 编译器就启用了。

再次运行一下程序,看一下禁用 JIT 之后问题是否重现。如果问题可以重现,那么这就不是一个与 JIT 有关的问题。如果在禁用 JIT 之后问题就不存在了,那么这就可能是一个与 JIT 有关的问题。在禁用 JIT 之后问题就不再出现的现象并不意味着 JIT 编译器就是问题的原因。例如,高度线程化且时间相关的程序中的方法在编译后和解释时的运行速度可能会不同,因此 Java 代码中的逻辑错误只会在编译代码之后才会出现。

要启用 JIT,请将 JAVA_COMPILER 设置为 jitc,或者使用下面的命令行来切换 JIT 编译器:

java -Djava.compiler=jitc <myapp>

或者取消 JAVA_COMPILER 环境变量的设置,或者从传递给 java 命令的选项中删除 -Djava.compiler 选项。

处理与 JIT 有关的问题

通常,与 JIT 有关的问题会将一个服务调用授权给一个适当的 IBM 支持组件。JIT 诊断可能是一个非常耗时的过程,需要高级的专门技术。通常,没有定义好的算法来解决与 JIT 有关的问题,因为 JIT 具有先天的复杂性和动态特性,因此最好是在 JIT 调试过程中有一个有经验的服务代表进行指导。然而,在与 IBM 的服务部门进行联系之前,最好能确定问题是真正与 JIT 有关的。因为事实是,程序在使用 JAVA_COMPILER=NONE 时运行得很好,并不能明确地说明 JIT 就是问题的根源。在有些基准测试的情况下,完全禁用了 JIT 的版本(JAVA_COMPILER=NONE)的速度会比标准的启用了 JIT 的运行速度慢 8 倍多。在生产环境中这是不可接受的。因此,最好是具有一个产品系统的克隆副本,然后在这个系统上进行测试和调试。IBM 服务代表可以提供丰富的有关 JIT 调试的文档,以及与特定的 JIT 问题有关的说明。

本节并不是对 JIT 诊断的一份详尽的指南,而是简要介绍了可以改善 JIT 调试的基本技术,从而实现以下目标:

  1. 在联系 IBM 的技术支持之前,确认问题是真正与 JIT 有关的。
  2. 寻找一个解决方案用于两个目的:
    • 提供一个暂时的解决方案,它可以在生产环境中提供可以接受的性能,同时让 IBM 修正问题的根源。
    • 帮助 IBM 进行问题分析。
  3. 获取 IBM 技术支持判断 JIT 问题所需要的信息。

如果您不能在一段可以接受的时间内解决问题,或者如果问题对于生产来说是非常关键的,或者您早已有一个简单的测试用例可以反复重现这个问题,那就请立即与 IBM 的技术服务部门联系。

在着手进行 JIT 调试之前,首先需要确保问题是在打过最新补丁的 JVM(例如 JVM 1.3.1)上发生的,或者是在一个应用了最新的修正包的更新版本的 JVM(例如 JVM 1.4.2)上发生的,它可以为应用程序提供与之后版本的 JVM 的兼容性。

没有必要花费太多的努力在 JIT 调试上,这可能会耗费大量的时间(取决于特定情况的复杂性),最终可能是由于 JIT 的一个小问题,而这个问题可能已经在相同版本的 JVM 的一个最新的修正包中解决了。JIT 一直处于不断的更新之中,您所碰到的问题很可能早已在最新的修正包中解决了。

其次,如果问题是在完全禁用 JIT(JAVA_COMPILER=NONE)的多处理器系统上发生的,那么看一下这个问题是否也会在禁用 JIT 的单处理器平台上存在,这是非常有用的。如果这个问题不会在禁用 JIT 的单处理器上发生,那么这就可能说明 JIT 不是问题的根源。如果这个问题即使在启用了 JIT 的单处理器系统上也不会出现,那么这个问题很可能与 JIT 无关,而是与应用程序中的时间相关逻辑有关。在使用 IBM 的技术服务请求时,需要将这些信息提供给 IBM 的技术服务部门。

要了解 Linux 系统上有多少处理器以及这些处理器有哪些特性,请执行下面的命令:

 cat  /proc/cpuinfo

JIT 调试有 3 个主要步骤。采用这三个步骤,才能获得一个性能可接受的可用解决方案,这对于 IBM 技术支持部门分析问题的根源也是非常重要的:

  1. 隔离产生问题的包/类/方法,在对代码进行优化或使用 JIT 编译为机器代码时,跳过 JIT 编译对代码进行编译的步骤。

    目标是尽可能缩小导致 JIT 问题发生的方法的范围。

    隔离 JIT 问题,从而缩小方法的范围,最好能将问题定位在一个方法上。如果只调用了一个方法,就很容易给出一个不会对应用程序性能产生很大影响的解决方案,这可以让 IBM 的技术支持部门更精确地分析在进行 JIT 编译时导致问题产生的 Java 代码。

  2. 隔离 JIT 优化选项,将 JIT 优化选项限制为尽可能少,而且在对上面确定的方法进行错误优化的 JIT 优化层次中尽可能具体,对这些方法禁用这些优化选项。

    JIT 的优化选项越具体(这可以缩小范围),禁用这种优化选项产生的性能影响就越小,这样就可能会很容易得到一个解决方案。

  3. 这样,JIT 调试过程就会确定应用程序可能产生问题的方法的最小集合,并为产生问题的 JIT 优化选项在 JIT 优化层次中尽可能深入地定位(缩小优化选项的范围)。结合使用这两种方法,即对所涉及的方法的已经确定的有限集合禁用一些特定的优化选项(或限制优化选项的范围);或者只使用一种方法(如果只涉及一个方法的代码),就可以产生一个解决方案,其性能影响可能会很小,通常可以忽略不计。

寻找解决方案时,在付出的努力上应该有一个平衡。在 JIT 调试过程中,在某一个特定的平衡点上可以找到一个性能可接受的解决方案。如果目标只是找到一个性能可接受的快速解决方案,那么调试努力就可以到此处结束了。

然而,如果已经使用了 IBM 的技术支持的服务请求,假设没有将所涉及的方法和 JIT 优化选项的范围定位到最窄的范围,就可能会使问题的解决方案变得复杂。在这种情况下,提供一个不包含所有者机密源代码的简单测试用例,使 IBM 技术支持部门可以重现这个问题,这会是最好的方法。采用一个可以一直重现问题的简单测试用例通常都是个好主意,因为这可以显著地加速解决问题的过程。

在寻找错误的 JIT 优化机制之前,应该先定位在进行 JIT 优化时引起问题的一个方法或一组有限的方法集合。

快速解决方案过程如下:

 1.1 设置 JITC_COMPILEOPT=NBUILTIN:NCHA:NMMI2JIT                
 			     /* if NBUILTIN:NCHA:NMMI2JIT passes */     
    1.1.1 设置 JITC_COMPILEOPT=NMMI2JIT...禁用动态热点 JITC 编译
    1.1.2 设置 JITC_COMPILEOPT=NBUILTIN...禁用内置的预编译的方法      
    1.1.3 设置 JITC_COMPILEOPT=NCHA...禁用类相互关系的分析   
1.2 设置 JITC_COMPILEOPT=NINLINING...禁用内联方法  
1.3 设置 JITC_COMPILEOPT=NQOPTIMIZE...禁用中间码优化    
			   /* if NQOPTIMIZE passes then try the options below */      
	1.3.1 设置 JITC_COMPILEOPT=NQCOPYPROPA...禁用拷贝传播 
	1.3.2 设置 JITC_COMPILEOPT=NQCOMMONING...禁用高级通用措施 
	1.3.3 设置 JITC_COMPILEOPT=NQDEADSTORE...禁用死锁存储文件删除功能 
1.4 设置 JITC_COMPILEOPT=NPRIVATIZE...禁用线程安全对象的本地化 
	     /* New options for JVM 1.4.0 and later */  
1.5 设置 JITC_COMPILEOPT=NOTHER  
1.6 设置 JITC_COMPILEOPT=GLOBAL

我们可以将上面的过程看作是一段伪代码。将同一个 JITC_COMPILEOPT 变量逐一设置为不同的值,每次设置之后重新运行应用程序,从而确认问题是否依然存在。只有在上一个“if”条件满足时,您才需要执行缩进的操作,然后执行同一个缩进级别上的操作,直到有一个选项能允许程序毫无问题地成功运行为止。

例如,如果 JITC_COMPILEOPT=NALL 的设置不能解决问题,您就要继续执行后面的过程,否则就继续执行 1.1,然后如果通过了,就执行深一层的步骤,否则就直接跳到同一层的下一个步骤,直到问题得到解决或已经尝试完所有的选项为止。

实际上 JIT 优化选项比上面的步骤中列出的要多。例如,设置 JITC_COMPILEOPT=NALL 可以禁用除基本优化之外的大部分优化选项,包括大部分其他优化选项。也就是说,这是进行其他优化的祖先;设置 JITC_COMPILEOPT=NALL 意味着所有其他 JIT 特有的优化组件也被禁用了。

要查看 JVM 1.4.2 中 JIT 优化选项的完整列表,请设置 JITC_COMPILEOPT=TITLE(all) ,并执行一个简单的 Java 代码,例如经典的“Hello World”。

在大部分 shell 特有通用字符中,例如:

" ' ` $ & | ; ( ) * ? \ > <

需要在前面加上转义字符“\”。

您还可以简单地将整个字符串放到单引号(')中,但是需要在变量中真正的单引号字符前面加上一个反斜线(\')。

JVM 1.4.x JIT 选项及其内部层次关系的详细列表可以打印出来。这个列表可以用来将问题的范围限制在引起问题的一个优化选项(或者一个优化选项的小集合)上。

如果您需要将当前的设置打印到标准输出上,JITC_COMPILEOPT=TITLE 选项也会非常有用。这可以帮助确保您考虑的正在设置的选项实际上都正被 JIT 编译器接受和识别。

要减少错误发生所需要的时间,您可能会希望如下的设置:IBM_MIXED_MODE_THRESHOLD=0。这样会禁用 MMI,并且每个方法都是在第一次碰到时进行编译,这样,问题就可能早些浮现出来。当然,禁用 MMI 会强制所有的方法都在首次碰到时进行编译,这样就会编译很多不是热点方法且只会被调用很少次数的方法。由于采用这种方法所引起的编译负载可能会实际增加程序运行到出现问题的地方所需要的时间。您可能会希望试验大量的值来找到一个可以快速引起问题的值。

设置 JITC_COMPILEOPT 变量来禁用特定的 JIT 优化选项,而不用指定要禁用哪个包、类或方法,这意味着会对应用程序的所有代码都禁用这些优化选项,从而会在整个程序的范围产生性能的影响。这就是为什么上面这个过程只是给出一个快速解决方案的建议。主要的压力应该在于隔离在进行 JIT 编译时引起问题的方法或方法集。

如何发现失败的方法?

设置 JITC_COMPILEOPT=COMPILING,并再次运行您的程序。在设置这个选项之后,JIT 编译器会在对方法进行编译时为每个方法打印两条消息。例如:

Compiling {java/lang/MyClass}{MyMethod}
0x0BF025C0 {java/lang/MyClass}{MyMethod}

第一条消息 Compiling {java/lang/MyClass}{MyMethod} 说明 JIT 编译器已经开始编译这个方法了。第二条消息说明编译过程已经完成了,十六进制的地址表示每次调用这个方法时,所执行的机器代码的起始偏移量。

如果您看到如下格式的一条消息,其中“F”表示这个方法没有被编译成功。

Compiling   F   {package/class}{method}

如果您怀疑会失败的方法的名字右边有一个“X”,那么这个“X”就表示发生了一次 MMI2JIT 转换,其中在检测到循环时对这个方法进行了编译,在一次调用这个方法时,对该方法的执行方式从解释(MMI)模式切换到了编译(JIT)模式。例如:

Compiling {java/util/MyClasss}{MyMethod} X

有三种可能的情景可以查找除程序输出之外的最后一条消息:

  1. 0x012345678 {package/class}{method}:崩溃是在执行一个已编译的方法(不一定是这条消息列出的方法)时发生的 。
  2. 该程序继续运行,但是性能降低了,或者需要花费更多时间来判断崩溃之前哪一条是最后的消息了。
  3. Compiling {package/class}{method}:崩溃是在编译过程中发生的。

在第一种情况下,可能的原因是 JIT 编译器会为一个方法产生已编译的代码,这个已编译的代码会产生一个错误。您也许可以通过检查错误发生处的地址来判断是哪个方法导致了这个错误的发生。

如果您可以获得一个错误指令地址,请遵循以下步骤来隔离失效的方法:

  1. 将 COMPILING 选项修改为 COMPILING(verbose)。这将会导致在输出结果中包含每个方法已编译的代码的起始地址和结束地址。如果您已经将 COMPILING 选项的范围缩小到一个类/方法规范中,请使用 COMPILING(verbose){class-spec}{method-spec}。
  2. 再次运行这个程序,但是要将标准输出重定向到一个文件中,例如:
    java -options classname >compiling.log
  3. 当错误发生时,使用 gdb 调试器来寻找错误地址,这取决于您的操作系统(您需要重复这次调用过程,因为两次调用时失败的地址可能会不同)。
    • 在产生 core 文件的目录中启动 gdb 调试器:
      gdb java core
    • 如果 gdb 调试器可以读取 core 文件,它就可以分析出失效发生的地址(例如,Segmentation fault in function_name at 0x10001234)。
    • 要获得一个堆栈跟踪信息,可以使用 bt 命令:
      (gdb) bt
    • 然后使用 quit 命令退出 gdb。

    确保 core 文件的大小(这可以在 ulimit -c 命令的结果中看到)没有被设置为 0 或设置得太小,因为在前一种情况下,不会产生 core 文件,而在后一种情况下,core 文件可能会被截断。

  4. compiling.log 文件中查找一个函数的开始地址和结束地址,它是十六进制的形式,其间就包含着错误地址。注意开始的十六进制地址通常是以升序排列的,但这并不是必须的。
  5. 如果您找到一个函数地址范围包含了失败的指令地址,那么这个范围的消息就会说明可能引入错误的方法。

    在第二种情况下,错误指令是未知的,因此需要使用其他技术来缩小出错类/方法对的范围:

    • 如果在程序崩溃之前打印(或者写到 javacore.txt)了 Java 堆栈跟踪,就检查它。在堆栈跟踪信息顶部列出的方法就是主要的候选者。
    • 如果您可以访问源代码,请在所怀疑的 Java 方法中插入一些指示性的代码,注意哪些怀疑的代码没有启动,或者没有完成。在有些情况下,在所怀疑的方法中添加一个 System.out.println() 语句可能会导致问题的消失,因为字节码和最终为该方法编译后的代码可能会发生很多变化,从而改变对该方法所应用的优化流程。在这种情况下,把出现 println() 的方法看作被怀疑的方法。如果您在很多怀疑的方法中都插入一个 println() 调用,则尽量删除 println() 调用,直到您确定哪一个防止了失败的出现;包含 println() 调用的方法很可能就是值得怀疑的地方。
    • 使用 libjitc.so 的调试版本 —— libjitc_g.so。

调试版本的 JIT 库包含对 C 库函数中的 assert() 函数的调用。这些调用被放到 JIT 编译器的代码中,用来断言所需要的条件为真;当一个 assert 调用失败时,就说明 JIT 编译器代码碰到了非预期的情况。通常一个断言失效都是在编译一个失败方法时发生的,然而在使用非调试版本的 JIT 库时,失败情况可能要到编译方法中的一个特定指令在以后实际执行时才会出现。当使用调试版本的 JIT 库时,您的程序会更可能在打印问题方法的 Compiling 消息之后失败。

调试版本的可执行程序和共享库包含在 JDK 中,但没有包含在 JRE 中。

要使用调试版本的 JIT 库,请遵循以下步骤:

  1. 检查在 java 命令的 -Dsun.boot.library.path 选项中所指向的一个目录中存在所需要的调试库。检查存在共享库 libjitc_g.so。
  2. 在调用 java 命令之前,设置环境变量:JAVA_COMPILER=jitcg。

如果您看到下面的消息:

Warning: JIT compiler "jitcg" not found. Will use interpreter


请检查所指定的库路径(-Dsun.boot.library.path)是正确的,所指定的调试库在这个路径中的一个目录中的确存在。

当您运行这个版本的 JVM 时,如果碰到断言失败的情况,执行过程就会停止,同时会显示一条断言失败消息。

在第三种情况下,或者在方法编译失败时,您会看到:

Compiling   F   {package/class}{method}


您可以跳过有问题的方法,不对其使用 JIT 进行编译。

您可以使用 JITC_COMPILEOPT 变量的 SKIP 选项禁用对失败方法进行 JIT 编译。

例如:

JITC_COMPILEOPT=SKIP{failingPackage/failingClass}{failingMethod}
跳过对特定类中的特定方法进行 JIT 编译的过程
JITC_COMPILEOPT=SKIP{failingPackage/failingClass}{*}
跳过对一个类的所有方法进行 JIT 编译的过程
JITC_COMPILEOPT=SKIP{failingPackage/*}{*}
这可以跳过对失败包中所有类的所有方法进行 JIT 编译的过程

如果上面其他查找失效方法的技术都失败了,那么这种相同的跳过一些方法的技术可以应用到更广泛的范围,作为一种强制方法来隔离失败的方法,或者一个失败方法的集合。

JITC_COMPILEOPT 变量在 Linux 以及其他平台上允许使用多个以点号(.)分隔开的选项,以及在正则表达式中使用通配符。例如:

JITC_COMPILEOPT=COMPILING:SKIP{com/class1/*}{*}{com /class2/*}{*}
跳过 JIT 对 firm.empdata 和 ourfirm.orderentry 包中所有类的所有方法的编译,按照“COMPILING”选项的定义打印 JIT 编译的信息。
JITC_COMPILEOPT=SKIP{myClass?2}{myfunc?b}
跳过名为 myClass?2 的任何类中的名为 myfunc?b 的所有方法,其中 ? 是任意的单个字符。
JITC_COMPILEOPT=SKIP{[ab]?[de]*}{*}
跳过第一个字母为“a”或“b”,第三个字母为“d”或“e”的所有类中的所有方法。

布尔 OR 和 NOT 操作分别是 | 和 ^ 操作符。注意这些字符在某些命令 shell 中具有特殊的意义,需要使用转义字符或引号。

JITC_COMPILEOPT=SKIP{java*|sun*}{*}
跳过以 java 或 sun 开始的所有类的所有方法。
JITC_COMPILEOPT=SKIP{^[js]*}{*}
跳过除了以“j”和“s”开头的所有类的所有方法。

在这个过程的最后,您应该可以提炼一组范围有限的方法集合,或者理想地说,甚至是提炼出一个方法,这样在为这个方法或这个范围很小的方法集合禁用所有的 JIT 优化选项时可以解决问题。

如何寻找有问题的 JIT 优化选项?

在上一节的最后,您应该早已可以确定跳过所选择的方法、类或包防止了问题的出现。在本节中,您将找到有问题的 JIT 优化选项,在对上一节中确定的方法集合或单个方法执行时,这些优化选项就会产生问题。如果您遵循 3.2 节中介绍的快速解决方案的步骤,通过寻找一个高级的优化选项集合进行禁用从而绕过问题来寻找一个解决方案,那么您可能早已确定了一个高级的优化选项集合。首先,您可以将 SKIP 选项修改为 NALL 选项,从而判断基本的编译是否是问题的根源,或者是否是由于优化选项而引起的问题。NALL 可以禁用大部分优化选项,但是没有禁用基本的编译。

如果您的 SKIP 选项包含了多个类规范/方法规范对,那么您就可能会希望一次对一个集合从 SKIP 切换到 NALL 上。尽管这会使得解决问题需要花费很多时间,但会给您一个目标更明确的解决方案集,在将来您请求技术支持时,使 IBM 更容易修正 JIT 的问题,在您等待修正程序的问题时,可以为每个类都提供最优的性能。

例如,如果您将失败的方法范围限定到:

SKIP{MyPackage/MyClass}{MyMethod}
{MyPackage2/MyClass2}{MyMethod2}

那么您就可以使用 SKIP 和 NALL 选项,如下所示:

export JITC_COMPILEOPT=NALL{MyPackage/MyClass}{MyMethod}:
SKIP {MyPackage2/MyClass2}{MyMethod2}

注意上面这行只是为了格式化文档的需要。在设置 JITC_COMPILEOPT 时应该没有空格和换行。

再次运行一下这个程序。如果它可以成功运行,那么您需要提炼优化选项。您可能需要遵循 3.2 中的步骤来找到一个快速解决方案,它会逐一禁用大部分高级的优化选项。这可以通过深入特定的优化层次进行深入的提炼。

如果这个程序不能运行,那么就可能存在本文中讨论的其他原因。您可以通过对所选择的方法跳过整个 JIT 编译过程来寻找一种解决方案。

现在,您可以联系 IBM 的技术服务部门了。

合并结果

调试过程的最终结果应该是对一组范围很小的方法禁用一组范围很小的优化选项。在最简单的情况下,只对一个方法执行了一个有问题的优化选项。

例如,如果您在对 MyPackage/MyClass 的 myMethod 方法进行内联时确定了有问题的行,那么最终的解决方案形式如下(Korn Shell):

export JITC_COMPILEOPT=NINLINING{MyPackage/MyClass}{myMethod}

针对 Linux 的当前 IBM JVM 产品

在撰写本文时,Linux 上的 JDK 和 JRE 可以从 IBM Linux 开发者工具集 下载,地址为 http://www.ibm.com/developerworks/java/jdk/linux140。

结束语

本文简要介绍了 IBM JVM 中使用的优化技术:JIT 和 MMI。

本文中介绍的简要 JIT 诊断过程可以帮助:

  • 确认 JIT 是问题的根源。
  • 将系统快速恢复到生产可接受的性能。
  • 在将 Java 程序从非基于 JIT 的 JVM 迁移到 IBM JVM 的过程中,如果碰到与 JIT 有关的问题,请搜集 IBM 修正问题的根源所需要的信息。

相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=163246
ArticleTitle=IBM JVM for Linux JIT 诊断简介
publish-date=03012005