调试HotSpot的JIT编译的注意事项

0.前言

通常,当您编译 Java 程序时,它首先由 Java 编译器编译为字节码。 但是,该字节码尚未优化。 在 HotSpot(OpenJDK 的 JVM)中,这在运行时发生,并由 JIT(即时编译器)完成。 这种处理方式允许 JIT 最大限度地利用代码运行的条件,例如硬件,甚至在某种程度上利用输入到程序中的特定数据。

在尝试了解 Java 程序的性能特征时,了解 JIT 编译器的作用非常重要。 在这篇博文中,我将展示几种用于调试 JIT 编译器正在执行的操作的技术。 这不是一篇针对初学者的帖子。 不过,它可能会给初学者一些他们可能想了解的东西的启发。 如果您想更多地了解某个主题,则必须自己进行研究。

1.调试环境搭建

首先,让我们建立一个小型测试项目,我们可以轻松修改该项目来测试不同的 Java 代码片段。 我们的目标是能够触发特定 Java 代码片段的 JIT 编译,以便我们可以使用 HotSpot 的一些选项来调试编译。

我定义了一个“payload”方法,它将保存我们希望 JIT 编译的代码。 然后,我们只需多次调用该方法即可触发此有效负载的 JIT 编译。 这是代码:

public class TestJIT {
    public static void main(String[] args) {
        for (int i = 0; i < 20_000; i++) {
            payload();
        }
    }

    public static void payload() {
        // test code here
    }
}

第 4 层 JIT 编译由 C2 JIT 编译器完成,这是最高/最优化的层,在 x64 上执行 10K 次调用后发生。 该数字可以在

./src/hotspot/cpu/x86/c2_globals.hpp

文件中找到,即

CompileThreshold 1

。它也是一个 VM 标志,因此也可以从命令行配置阈值。 为了安全起见,我在这里调用有效负载 20K 次,因为调用计数器不一定 100% 准确。

我只是用 javac 将程序编译为字节码。 然后,在运行它时,需要传递一些重要的标志。 第一:

-XX:CompileCommand=dontinline,TestJIT::payload

。 该标志禁用有效负载的内联。 这样做对于获得有效负载方法的独立编译非常重要,这需要能够独立于 main 中的循环来检查该特定方法的编译。

我们需要传递的另一个重要标志是

-Xbatch

。 JIT 编译默认在后台线程中完成。 这意味着您的代码可以在编译发生时继续运行,但在我们的例子中,这也意味着代码可能在编译完成之前完成运行,这意味着我们将无法调试编译。

-Xbatch

使得请求编译的线程在编译发生时停止。 FWIW,-Xbatch是

-XX:-BackgroundCompilation

的别名,即关闭后台编译2;

如果您使用这 2 个标志(以及任何其他所需的标志)运行测试程序,则输出还不是很有趣:

java -cp classes -XX:CompileCommand=dontinline,TestJIT::payload
-Xbatch TestJIT CompileCommand: dontinline TestJIT.payload bool dontinline = true

接下来,让我们看看如何开始使用此测试设置从有效负载方法的编译中获取有趣的信息。

2.获取已编译方法的程序集

JIT 编译器输出机器代码,可以说,这是一种难以阅读的格式。 幸运的是,这个机器代码可以被反汇编成更易读的格式,其中每个 CPU 指令都用助记符表示。 为此,需要一个名为 hsdis 的 HotSpot 反汇编器插件。 您可以通过 OpenJDK 构建说明。 我建议使用基于 capstone 的 hsdis,因为它最容易构建,也是我正在使用的。 我已将 hsdis 库文件放在 PATH 中,HotSpot 可以自动加载它。

现在我们只需使用几个附加的 VM 标志再次运行该程序即可打印出有效负载方法的程序集。 我使用

-XX:CompileCommand=print,TestJIT::payload

打印出程序集,并且还添加了

-XX:-TieredCompilation

,它会禁用 HotSpot C1 编译器的编译。 后一个标志用于减少输出量。 在本例中,我只对 C2 JIT 生成的程序集感兴趣,这是更优化的版本。 相反,如果您只对 C1 生成的程序集感兴趣,则可以使用

-XX:TieredStopAtLevel=3

来禁用第 4 层编译,即 C2。 最后,我更喜欢 intel 汇编语法,因此我还传递

-XX:PrintAssemblyOptions=intel

。 最后一个标志是诊断标志,因此我们还必须在 PrintAssemblyOptions 标志之前传递

-XX:+UnlockDiagnosticVMOptions

。 将所有这些放在一起,我们运行程序如下:

java `
  -cp classes `
  -Xbatch `
  -XX:-TieredCompilation `
  -XX:CompileCommand=dontinline,TestJIT::payload `
  -XX:CompileCommand=print,TestJIT::payload `
  -XX:+UnlockDiagnosticVMOptions `
  -XX:PrintAssemblyOptions=intel `
  TestJIT

请注意,我使用的是 powershell。 这些刻度在 powershell 中相当于 unix shell 中的 \。 我已将此命令放入脚本文件中,以便更轻松地编辑和重新运行。

生成的输出是特定于架构的。 我使用的是 x64 机器,它提供了 x64 程序集。 如果您使用的是 AArch64 机器,例如 Apple 的 M1,则输出会有所不同。 我上面的测试程序得到的输出如下:

CompileCommand: dontinline TestJIT.payload bool dontinline = true
CompileCommand: print TestJIT.payload bool print = true

============================= C2-compiled nmethod ==============================
----------------------------------- Assembly -----------------------------------

Compiled method (c2)      54   13             TestJIT::payload (1 bytes)
 total in heap  [0x000001c6bacb0a90,0x000001c6bacb0ca0] = 528
 relocation     [0x000001c6bacb0bd8,0x000001c6bacb0be8] = 16
 main code      [0x000001c6bacb0c00,0x000001c6bacb0c50] = 80
 stub code      [0x000001c6bacb0c50,0x000001c6bacb0c68] = 24
 oops           [0x000001c6bacb0c68,0x000001c6bacb0c70] = 8
 scopes data    [0x000001c6bacb0c70,0x000001c6bacb0c78] = 8
 scopes pcs     [0x000001c6bacb0c78,0x000001c6bacb0c98] = 32
 dependencies   [0x000001c6bacb0c98,0x000001c6bacb0ca0] = 8

[Disassembly]
--------------------------------------------------------------------------------
[Constant Pool (empty)]

--------------------------------------------------------------------------------

[Verified Entry Point]
  # {method} {0x000001c6d88002e8} 'payload' '()V' in 'TestJIT'
  #           [sp+0x20]  (sp of caller)
  0x000001c6bacb0c00:   sub             rsp, 0x18
  0x000001c6bacb0c07:   mov             qword ptr [rsp + 0x10], rbp
  0x000001c6bacb0c0c:   cmp             dword ptr [r15 + 0x20], 0
  0x000001c6bacb0c14:   jne             0x1c6bacb0c43
  0x000001c6bacb0c1a:   add             rsp, 0x10
  0x000001c6bacb0c1e:   pop             rbp
  0x000001c6bacb0c1f:   cmp             rsp, qword ptr [r15 + 0x378]
                                                            ;   {poll_return}
  0x000001c6bacb0c26:   ja              0x1c6bacb0c2d
  0x000001c6bacb0c2c:   ret
  0x000001c6bacb0c2d:   movabs          r10, 0x1c6bacb0c1f  ;   {internal_word}
  0x000001c6bacb0c37:   mov             qword ptr [r15 + 0x390], r10
  0x000001c6bacb0c3e:   jmp             0x1c6bac7ad80       ;   {runtime_call SafepointBlob}
  0x000001c6bacb0c43:   call            0x1c6bac586e0       ;   {runtime_call StubRoutines (2)}
  0x000001c6bacb0c48:   jmp             0x1c6bacb0c1a
  0x000001c6bacb0c4d:   hlt
  0x000001c6bacb0c4e:   hlt
  0x000001c6bacb0c4f:   hlt
[Exception Handler]
  0x000001c6bacb0c50:   jmp             0x1c6baca3f00       ;   {no_reloc}
[Deopt Handler Code]
  0x000001c6bacb0c55:   call            0x1c6bacb0c5a
  0x000001c6bacb0c5a:   sub             qword ptr [rsp], 5
  0x000001c6bacb0c5f:   jmp             0x1c6bac7a020       ;   {runtime_call DeoptimizationBlob}
  0x000001c6bacb0c64:   hlt
  0x000001c6bacb0c65:   hlt
  0x000001c6bacb0c66:   hlt
  0x000001c6bacb0c67:   hlt
--------------------------------------------------------------------------------
[/Disassembly]

需要关注的是输出的 [Disassemble] 部分。 尽管我们的 [payload] 方法是空的,但仍然有相当多的 JIT 生成的代码。 我们当然是在虚拟机中运行,并且需要一些额外的代码才能使其工作。 如果我们只对 [payload] 方法的内容生成的程序集感兴趣,那么上面的大部分内容都是无关紧要的。 不过,我将过一遍它,以便我们知道哪些部分通常可以忽略:

0x000001c6bacb0c00:   sub             rsp, 0x18
0x000001c6bacb0c07:   mov             qword ptr [rsp + 0x10], rbp

设置堆栈框架。 在线程的堆栈上分配一点内存,并将 [rbp] 寄存器的内容保存到堆栈中。

0x000001c6bacb0c0c:   cmp             dword ptr [r15 + 0x20], 0
0x000001c6bacb0c14:   jne             0x1c6bacb0c43

[NMethod]进入屏障。 “nmethod”是 HotSpot 中编译的 Java 方法的名称。 需要 nmethod 进入屏障才能使某些 GC 工作。 我现在不会讨论这个。

0x000001c6bacb0c1a:   add             rsp, 0x10
0x000001c6bacb0c1e:   pop             rbp

清理框架

0x000001c6bacb0c1f:   cmp             rsp, qword ptr [r15 + 0x378]
                                                            ;   {poll_return}
0x000001c6bacb0c26:   ja              0x1c6bacb0c2d

安全点调查。 虚拟机需要线程偶尔轮询安全点。 在安全点,当前线程的 JVM 状态是完全已知且可恢复的。 这是代码中虚拟机可能想要检查线程以执行各种虚拟机操作的点。

0x000001c6bacb0c2c:   ret

返回说明

我现在将忽略其余部分。 重要的是,[payload] 方法内容的代码将主要在此块中找到,位于 nmethod 入口屏障和帧清理之间。 尝试查找片段的生成代码时,一个好的策略是查找 nmethod 入口障碍并从那里向前工作,或者查找返回指令并从那里向后工作。

让我们修改我们的 [payload] 方法,看看生成的程序集会发生什么。 我将更改我的有效负载方法,将两个数字相加并返回结果:

public static int payload(int a, int b) {
    return a + b;
}

我只是在 [main] 方法中使用一些虚拟参数来调用它。 这些值并不重要,因为我们已经禁用了 [payload] 方法的内联,因此 JIT 编译器将无法“查看”参数的实际值,并且必须假设它们可以是任何值。 出于类似的原因,丢弃 [main] 方法中的返回值是安全的:

for (int i = 0; i < 20_000; i++) {
    payload(1, 2);
}

使用 javac 重新编译,并使用上面的标志重新运行程序,得到以下汇编代码:

# {method} {0x0000016af6c002f0} 'payload' '(II)I' in 'TestJIT'
  # parm0:    rdx       = int
  # parm1:    r8        = int
  #           [sp+0x20]  (sp of caller)
  0x0000016ad9230c00:   sub             rsp, 0x18
  0x0000016ad9230c07:   mov             qword ptr [rsp + 0x10], rbp
  0x0000016ad9230c0c:   cmp             dword ptr [r15 + 0x20], 0
  0x0000016ad9230c14:   jne             0x16ad9230c47
  0x0000016ad9230c1a:   lea             eax, [rdx + r8]
  0x0000016ad9230c1e:   add             rsp, 0x10
  0x0000016ad9230c22:   pop             rbp
  0x0000016ad9230c23:   cmp             rsp, qword ptr [r15 + 0x378]
                                                            ;   {poll_return}
  0x0000016ad9230c2a:   ja              0x16ad9230c31
  0x0000016ad9230c30:   ret

我刚刚在这里复制了相关位,从方法入口到返回指令。 请注意前几行,它告诉我们参数正在传递到哪些寄存器。 HotSpot JIT 使用 Java 方法的自定义调用约定。 因此,这可能与例如使用的调用约定不同。 C:

   # parm0: rdx = int
   # parm1: r8 = int

代码的汇编 return a + b; 可以在nmethod入口屏障和帧清理之间找到:

   0x0000016ad9230c1a:lea eax,[rdx + r8]

一种巧妙的方法,在一条指令中将两个值相加,并将结果存储在 eax 寄存器中,该寄存器是 Java 编译调用约定中返回整数值的寄存器。

这应该让您对开始分析 JIT 编译器为特定 Java 代码片段生成的代码所需的内容有一个基本的了解。

这里需要注意的是,该程序集是通过发布版本生成的,即您可以从 jdk.java.net 下载的版本。 HotSpot 还有“fastdebug”和“slowdebug”版本。 使用调试版本可以在打印程序集时打印更详细的信息。 如果您能够获得快速调试构建,我建议在打印程序集时至少使用它。 获得快速调试构建的最直接方法是自己构建 JDK。 如果您按照构建指南中的步骤进行操作,这相对容易。 对于 fastdebug 构建,只需确保使用 --with-debug-level=fastdebug 配置构建即可。

3. 打印内联痕迹

我们可以从 JIT 编译器获得的下一个有用信息是内联跟踪。 此跟踪指示编译的 Java 代码调用的方法是否内联。 内联是一项重要的优化,它允许进行其他优化,因此在尝试了解 Java 代码片段的性能时,了解哪些方法是内联的非常重要。 虽然这些信息在技术上也存在于程序集中,但内联跟踪提供了更好的高级概述。

首先,让我们修改有效负载方法以调用另一个方法:

public static int yetAnotherMethod() {
    return 42;
}

public static int otherMethod() {
    return yetAnotherMethod();
}

public static int payload() {
    return otherMethod() + 1;
}

我们可以使用另一个 CompileCommand 选项为此编译生成内联跟踪。 我不使用

-XX:CompileCommand=print,TestJIT::payload

来打印程序集,而是将该标志中的打印选项更改为 PrintInlined,这将为我们提供内联跟踪:

-XX:CompileCommand=PrintInlined,TestJIT ::payload

。 我得到的输出很简单:

@ 0   TestJIT::otherMethod (4 bytes)   inline (hot)
  @ 0   TestJIT::yetAnotherMethod (3 bytes)   inline (hot)

不幸的是,内联跟踪的格式并没有真正记录下来。 理解它的最佳信息来源是 CompileTask::print_inlined_inner 中的源代码。 内联跟踪中的每一行将指示内联成功的方法或内联失败的方法。 目前成功和失败之间的区别并没有很好地表明(我希望将来能改变这一点),我们必须解释行末尾的消息来决定内联是成功还是失败。 在这种情况下,消息是内联的(热),从中我们可以确定内联成功。 该行还列出了内联方法的名称,以及方法调用在调用者方法中的字节码位置。 在这种情况下,有效负载中对 otherMethod 的调用位于 bci(字节码索引)0,otherMethod 中的 YetAnotherMethod 方法调用也位于 bci 0。行的缩进表示发生的内联的“级别”。 由于 YetAnotherMethod 方法是通过 otherMethod 传递内联的,因此它的行缩进了 2 个额外的空格。

让我们看看如果使用

-XX:CompileCommand=dontinline,TestJIT::otherMethod

标志禁用 otherMethod 的内联会发生什么。 现在内联跟踪如下所示:

CompileCommand 不允许使用 @ 0 TestJIT::otherMethod(4 字节)
这应该涵盖内联跟踪的基础知识。

调试配置文件污染的一些注意事项:例如在调用虚拟方法时会发生分析。 JVM 将记录接收者的类型,如果它始终是两种类型之一,则 C2 JIT 可以使用内联缓存来内联方法调用。 配置文件附加到特定的字节码,这意味着当我们在一些高度共享的代码中有一个虚拟方法调用站点并且看到许多不同的接收器类型时,C2 可能无法内联此类方法调用。 这有时称为配置文件污染:呼叫的配置文件被许多不同的接收器类型“污染”。

内联跟踪可用于诊断配置文件污染。 受污染的方法将在 t 中显示为虚拟调用他内联跟踪。 然而,为了获得准确的分析(分析也发生在较低层),再次打开分层编译非常重要。 因此,我们需要使用

-XX:+TieredCompilation

(将 - 替换为 +),而不是

-XX:-TieredCompilation

。 然而,这也将使内联跟踪包含来自多个编译的跟踪。 为了能够区分它们,我们可以使用

-XX:CompileCommand=PrintCompilation,TestJIT::payload

,这将在特定编译的内联跟踪开始时输出一些有关编译的信息,使跟踪更容易区分( 例如,“2591 308 b 4 AbsMapProfiling::payload (139 bytes)”,后跟该编译的内联跟踪)。 对于最相关的信息,您可能想要查看该方法的最后一次编译的跟踪。

4. 仔细看看编译命令

现在您可能已经注意到

-XX:CompileCommand=…

选项有多么有用。 该命令用于控制每个方法的编译器设置。 要获取有关 CompileCommand 标志的更多信息,我们可以使用:

java -XX:CompileCommand=help

这将输出标准的 Java 帮助消息,但如果在控制台输出中向上滚动到该消息上方,您还应该找到 CompileCommand 帮助消息。 这描述了该标志的语法,并列出了可以与该标志结合使用的所有选项。

./src/hotspot/share/compiler/compilerDirectives.hpp

文件中还列出了许多标志。

除了在命令行上指定这些编译命令之外,还可以通过 json 文件指定它们,这提供了更多的灵活性。 该选项适用于哪个编译器。 有关详细信息,请参阅 JEP

如果您查看 compilerDirectives 文件,您可能会注意到某些选项仅在非产品版本中可用。

5. 追踪逃跑的对象

在本节中,我将使用 HotSpot 的“fastdebug”版本。 如果您想继续,您将无法使用发布版本。

当使用 -prof gc 运行 JMH 基准测试时,您可能遇到过这种情况。 你有一个像这样的基准:

Runnable dummy = () -> {};

static class Scope implements AutoCloseable {
    final List<Runnable> resources = new ArrayList<>();

    void addCloseAction(Runnable runnable) {
        resources.add(runnable);
    }

    @Override
    public void close() {
        for (Runnable r : resources) {
            r.run();
        }
    }
}
@Benchmark
public void testMethod() throws InterruptedException {
    try (Scope scope = new Scope()) {
        scope.addCloseAction(dummy);
    }
}

当使用 -prof gc 运行它时,您会看到一些分配发生:

MyBenchmark.testMethod:gc.alloc.rate.norm avgt 50 96.000 ± 0.001 B/op

每个操作分配 96 个字节。 但是,HotSpot 具有逃逸分析,可以消除分配,并且据我们所知,没有分配的对象从基准方法中逃逸。 那么,为什么我们仍然看到分配呢? 为了调查这一点,我们将使用 TraceEscapeAnalysis 编译命令(该命令在发布版本中不可用)。 该命令打印逃逸分析算法的痕迹,我们可以用它来追踪哪些对象逃逸以及为什么。

让我们首先修改有效负载以包含基准测试中的代码:

 public void payload() {
    try (Scope scope = new Scope()) {
        scope.addCloseAction(dummy);
    }
}

Runnable dummy = () -> {};

static class Scope implements AutoCloseable {
    final List<Runnable> resources = new ArrayList<>();

    void addCloseAction(Runnable runnable) {
        resources.add(runnable);
    }

    @Override
    public void close() {
        for (Runnable r : resources) {
            r.run();
        }
    }
}

请注意,我已将有效负载方法转换为实例方法,为了使该方法有效,我只需创建 TestJIT 类的实例并在其上调用有效负载方法:

TestJIT recv = new TestJIT();
for (int i = 0; i < 20_000; i++) {
    recv.payload();
}

为了获取转义分析跟踪,我在基本命令中将 PrintInlined 选项更改为 TraceEscapeAnalysis:

-XX:CompileCommand=TraceEscapeAnalysis,TestJIT::payload

。 我还将输出通过管道传输到文件 … > EA.txt,因为它很长。

我得到的输出很长,我不会在这里完整包含它。 需要寻找的重要内容是这样的行:

+++++ Initial worklist for virtual void TestJIT.payload() (ea_inv=0)

注意最后的 ea_inv=0 。 逃逸分析可以运行多次迭代。 然而,为了找到逃逸的对象,只有最后一次迭代是相关的。 所以,我只是搜索 ea_inv,找到最后一次迭代,即 ea_inv=1,并删除之前的其余跟踪。

跟踪的其余部分应分为两部分:初始工作列表,其开始由上面的文本行标记,然后是实际计算跟踪,由以下 l 标记内:

+++++ Calculating escape states and scalar replaceability

下一步是在初始工作列表中找到我们的对象分配。 对象分配由 C2 中的 Allocate 节点表示,因此我们可以查找字符串“Allocate ===”。 请务必仅查看初始工作清单中的分配。 我展示的测试程序应该有 2 个:

JavaObject(9) NoEscape(NoEscape) [ [ 37 ]]     25  Allocate  === 5 6 7 8 1 (23 21 22 1 1 10 1 1 1 ) [[ 26 27 28 35 36 37 ]]  rawptr:NotNull ( int:>=0, java/lang/Object:NotNull *, bool, top, bool ) TestJIT::payload @ bci:0 (line 12) !jvms: TestJIT::payload @ bci:0 (line 12)
JavaObject(10) NoEscape(NoEscape) [ [ 102 ]]     90  Allocate  === 39 36 63 8 1 (88 87 22 1 1 10 1 1 1 42 1 42 ) [[ 91 92 93 100 101 102 ]]  rawptr:NotNull ( int:>=0, java/lang/Object:NotNull *, bool, top, bool ) TestJIT$Scope::<init> @ bci:5 (line 20) TestJIT::payload @ bci:4 (line 12) !jvms: TestJIT$Scope::<init> @ bci:5 (line 20) TestJIT::payload @ bci:4 (line 12)

这些分配一开始是非逃逸的,但在接下来的逃逸分析中,有时会发现它们发生了逃逸。 在 Allocate 节点的调试字符串中,我们可以看到代码中发生分配的位置:TestJIT::payload @ bci:0(第 12 行)和 TestJIT$Scope:: @ bci:5(第 20 行) 。 因此,我们有 2 次分配,一次在第 12 行的有效负载方法中,一次在第 20 行的 Scope 构造函数中。这些是:

try (Scope scope = new Scope()) {

和:

final List<Runnable> resources = new ArrayList<>();

正是我们使用 new 运算符的地方!

现在,让我们尝试找出这些对象逃逸的原因。 我将首先在“计算转义状态…”消息中搜索 JavaObject(9)。 我发现这个:

JavaObject(9) NoEscape(NoEscape) -> NoEscape(GlobalEscape) propagated from: LocalVar(28) ...
JavaObject(9) NoEscape(GlobalEscape) NSR -> ArgEscape(GlobalEscape) propagated from: LocalVar(28) ...

我们可以在这里看到 JavaObject(9) 的状态已更新为 ArgEscape(GlobalEscape),这使得它不可进行标量替换。 这也由“NSR”(不可标量替换)表示。 为了找到原因,我们必须沿着日志中的状态更新链追溯到根。 在此消息中,我们可以看到状态是从 LocalVar(28) 传播的。 当我搜索时,我发现了这个:

LocalVar(28) NoEscape(NoEscape) -> NoEscape(GlobalEscape) propagated from: LocalVar(41) ...

即 LocalVar(28) 的状态本身是从 LocalVar(41) 传播的。 如果我继续沿着链条返回,我最终会得到:

LocalVar(41) ArgEscape(ArgEscape) -> ArgEscape(GlobalEscape) escapes as arg to: 1020  CallStaticJava  === 414 407 408 8 1 (42 1 1 417 1 ) [[ 1021 1022 1023 ]] # Static  TestJIT$Scope::close void ( TestJIT$Scope (java/lang/AutoCloseable):NotNull * ) TestJIT::payload @ bci:25 (line 12) !jvms: TestJIT::payload @ bci:25 (line 12)

即我们的对象通过对 TestJIT$Scope::close 的外线调用进行转义! 而且,如果我们对 JavaObject(10)(我们的其他分配)遵循相同的过程,我们最终会得到相同的调用。 因此,两个对象都通过此调用逃逸。

我想说的是,跟踪日志的过程有点乏味。 读者们很幸运,我最近编写了一个脚本,可以解析跟踪并报告有关转义对象的信息。 你可以在这里找到它。 如果我在跟踪上调用该脚本:java .\TraceEAParser.java EA.Txt,我会得到以下内容:

Escaping allocations:

JavaObject(9) allocation in: TestJIT::payload @ bci:0 (line 12)
  -> LocalVar(28)
  -> LocalVar(41)
  Reason: EscapesAsArg[callNode=1020  CallStaticJava  === 414 407 408 8 1 (42 1 1 417 1 ) [[ 1021 1022 1023 ]] # Static  TestJIT$Scope::close void ( TestJIT$Scope (java/lang/AutoCloseable):NotNull * ) TestJIT::payload @ bci:25 (line 12) !jvms: TestJIT::payload @ bci:25 (line 12)]


JavaObject(10) allocation in: TestJIT$Scope::<init> @ bci:5 (line 20)
  -> Field(19)
  -> JavaObject(9)
  -> LocalVar(28)
  -> LocalVar(41)
  Reason: EscapesAsArg[callNode=1020  CallStaticJava  === 414 407 408 8 1 (42 1 1 417 1 ) [[ 1021 1022 1023 ]] # Static  TestJIT$Scope::close void ( TestJIT$Scope (java/lang/AutoCloseable):NotNull * ) TestJIT::payload @ bci:25 (line 12) !jvms: TestJIT::payload @ bci:25 (line 12)]

对转义分配的一个很好的总结。 同样,我们可以在这里看到有 2 个转义分配,它们都是通过调用 TestJIT$Scope::close 来转义的。

现在,由于这个异常调用出现在此处,我们可以生成一个内联跟踪,如第 3 节所示。如果我们在跟踪中查找 TestJIT$Scope::close,我们会发现:

@ 25   TestJIT$Scope::close (39 bytes)   already compiled into a medium method

不幸的是,TestJIT$Scope::close 没有被内联,这使得我们的对象逃逸。 实际上,我们刚刚诊断出 JDK-8267532,截至撰写本文时还没有解决方案。 但是,我们现在至少知道了物体逃逸的原因。 在其他情况下,这可能是一个更有用的工具,可以追踪有问题的代码片段,以便进行现场修复。 但是,不幸的是,在处理性能时,彩虹尽头并不总是有一桶金。

6. 使用本机调试器调试编译

最后,让我们打开最终的逃生舱口:在编译期间直接调试源代码。 当其他方法都达不到要求时,使用调试器单步调试源代码是检查 JIT 编译器正在执行的操作的终极工具。 为此,我们需要一个“slowdebug”构建(以与 fastdebug 构建类似的方式获得)。 这是必要的,这样虚拟机实际上可以使用本机调试工具进行调试,而不会出现太多问题。

为了设置虚拟机的调试,我首先通过在 JDK 构建系统中运行 make vscode-project 来设置 VSCode 项目。 这应该会生成一个 ./build//jdk.code-workspace 文件,我可以通过 File -> Open Workspace from File… 在 VSCode 中打开该文件。

接下来,我将这两行代码添加到测试程序中 main 方法的开头:

System.out.println("pid: " + ProcessHandle.current().pid());
System.in.read();

我打印出当前进程 ID,然后通过从标准输入读取来“等待”。

接下来,我添加

-XX:CompileCommand=BreakAtCompile,TestJIT::payload,true

命令行标志,这将在编译有效负载方法期间停止本机调试器。 如果我运行该程序,我将在控制台上看到打印的 pid。 此时我可以通过 VSCode 附加调试器。 我转到“运行和调试”选项卡,然后单击窗口左上角的小齿轮。 这应该打开一个包含多个运行/调试配置的 .json 文件。 右下角应该有一个“添加配置…”,通过它我可以为我想要使用的调试器添加配置。 我使用的是 C/C++:(Windows) Attach 配置,它来自 C/C++ VSCode 插件。 只需要添加运行配置一次,之后我只需单击左上角的绿色小箭头并选择“(Windows) Attach”运行配置即可。

使用打印的 pid 连接本机调试器,并在测试程序控制台中按“enter”键使其继续执行后,VSCode 窗口会弹出,并将我带入 HotSpot 编译器代码的某个位置。 此时我可以在编译器代码中设置其他断点,并开始调试有效负载方法的编译。

这应该让您了解如何使用本机调试器调试编译。

结论
这就是我目前所拥有的调试 HotSpot 的 JIT 编译器一切信息。 希望我使用的一些调试技术能给您带来一些启发,让您自己调试 HotSpot 的 JIT 编译器。

  • 20
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值