欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 建筑 > JVM常见概念之不怎么常见的一些陷阱

JVM常见概念之不怎么常见的一些陷阱

2025/5/17 9:20:31 来源:https://blog.csdn.net/nanxiaotao/article/details/146421242  浏览:    关键词:JVM常见概念之不怎么常见的一些陷阱

问题

JIT 编译的最佳部分是什么?如果 JIT 决定编译该方法,它会编译其中的所有内容吗?我是否应该使用真实数据来预热方法?JIT 编译器有哪些技巧可以优化其编译时间?

基础知识

JIT编译器针对方法进行工作:一旦某个方法被视为热代码,运行时系统就会要求 JIT编译器生成该方法的优化版本。因此,JIT 会编译整个方法并将其交给运行时系统。

但事实是,允许推测编译/去优化的运行时系统允许JIT使用关于其行为的一系列假设来编译方法。我们之前在隐式空值检查中见过它。这次,我们将研究有关冷代码的更常见的一些东西。

考虑一下只有使用 flag = true 才能有效调用的这个方法:

void m(boolean flag) {if (flag) {// do stuff A} else {// do stuff B}
}

即使从分析中不知道 flag ,智能 JIT 编译器也可以使用分支分析来确定“B”分支永远不会被采用,并将其编译为:

void m() {if (condition) {// do stuff A} else {// Assume this branch is never taken.<trap to runtime system: uncommon branch is taken>}
}

因此,实际上从未编译过分支 B 中的实际代码。通过避免处理永远不需要的代码,这节省了编译时间,通常还提高了代码密度。

请注意,这与基于分支频率的代码布局不同。在这种情况下,当其中一个分支频率恰好为零时,我们可以完全跳过编译其主体。当且仅当采用该分支时,生成的代码才会陷入运行时系统,表明违反了编译先决条件,并且 JIT 将在新条件下重新生成方法主体,这次编译现在并不罕见的分支。

实验

源码

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class ColdCodeBench {@Param({"onlyA", "onlyB", "swap"})String test;boolean condition;@Setup(Level.Iteration)public void setup() {switch (test) {case "onlyA":condition = true;break;case "onlyB":condition = false;break;case "swap":condition = !condition;break;}}int v = 1;int a = 1;int b = 1;@Benchmark@CompilerControl(CompilerControl.Mode.DONT_INLINE)public void test() {if (condition) {v *= a;} else {v *= b;}}
}

在这个测试中,我们要么只采用分支 A,要么只采用分支 B,或者我们在每次迭代时在它们之间进行切换。

此测试的目的是以简单的方式演示生成的代码。 在这个简单的测试中, 所有版本的性能大致相同。实际上,冷分支可能需要大量代码,尤其是在内联之后,并且对编译时间和生成的代码密度的性能影响将是巨大的。

毫不奇怪,与 “基于频率的代码布局” 一致,我们可以看到“onlyA”和“onlyB”测试都立即布局了第一个分支。但随后,发生了一件奇怪的事情:根本没有第二个分支代码!相反,有一个所谓的“不常见陷阱”的调用!这是对运行时的通知,我们已不满足编译条件,现在采用这个“不常见”分支。

# "onlyA"9.54%   ...3cc: movzbl 0x18(%rsi),%r10d  ; load and test $condition0.21%   ...3d1: test   %r10d,%r10d╭ ...3d4: je     ...3f6│                                  ; if true, then...0.90% │ ...3d6: mov    0x10(%rsi),%r10d  ; ...load $a...7.81% │ ...3da: imul   0xc(%rsi),%r10d   ; ...multiply by $v...17.33% │ ...3df: mov    %r10d,0xc(%rsi)   ; ...store to $v...8.16% │ ...3e3: add    $0x20,%rsp        ; ...and return.0.60% │ ...3e7: pop    %rbp0.18% │ ...3e8: cmp    0x340(%r15),%rsp0.02% │ ...3ef: ja     ...40810.51% │ ...3f5: retq│                                  ; if false, then...↘ ...3f6: mov    %rsi,%rbp...3f9: mov    %r10d,(%rsp)...3fd: mov    $0xffffff45,%esi...402: nop...403: callq  <runtime>         ; - (reexecute) o.o.CCB::test@4 (line 73);   {runtime_call UncommonTrapBlob}# "onlyB"10.21%   ...acc: movzbl 0x18(%rsi),%r10d  ; load and test $condition0.25%   ...ad1: test   %r10d,%r10d╭ ...ad4: jne    ...af6│                                  ; if false, then...0.29% │ ...ad6: mov    0x14(%rsi),%r10d  ; ...load $b...8.78% │ ...ada: imul   0xc(%rsi),%r10d   ; ...multiply by $v...18.87% │ ...adf: mov    %r10d,0xc(%rsi)   ; ...store $v...9.74% │ ...ae3: add    $0x20,%rsp        ; ...and return.0.24% │ ...ae7: pop    %rbp0.27% │ ...ae8: cmp    0x340(%r15),%rsp│ ...aef: ja     ...b089.76% │ ...af5: retq│                                  ; if true, then...↘ ...af6: mov    %rsi,%rbp...af9: mov    %r10d,(%rsp)...afd: mov    $0xffffff45,%esi...b02: nop...b03: callq  <runtime>         ; - (reexecute) o.o.CCB::test@4 (line 73);   {runtime_call UncommonTrapBlob}

当最终采用该“冷”分支时,JVM 将重新编译该方法。它将在 -XX:+PrintCompilation 日志中显示如下:

# Warmup Iteration   1: ...// Profiled version is compiled with C1 (+MDO)351  476       3       org.openjdk.ColdCodeBench::test (37 bytes)// C2 version is installed352  477       4       org.openjdk.ColdCodeBench::test (37 bytes)// Profiled version is declared dead352  476       3       org.openjdk.ColdCodeBench::test (37 bytes)   made not entrant# Warmup Iteration   2: ...// Deopt! C2 version is declared dead1361  477       4       org.openjdk.ColdCodeBench::test (37 bytes)   made not entrant// Re-profiling version is compiled with C1 (+counters)1363  498       2       org.openjdk.ColdCodeBench::test (37 bytes)// New C2 version is installed1364  499       4       org.openjdk.ColdCodeBench::test (37 bytes)// Re-profiling version is declared dead1364  498       2       org.openjdk.ColdCodeBench::test (37 bytes)   made not entrant

在“swap”情况下,最终结果清晰可见。两个分支都经过编译:

  4.25%    ...f2c: mov    0xc(%rsi),%r11d  ; load $v6.23%    ...f30: movzbl 0x18(%rsi),%r10d ; load and test $condition0.04%    ...f35: test   %r10d,%r10d╭  ...f38: je     ...f45│                                  ; if false, then0.02% │  ...f3a: imul   0x10(%rsi),%r11d ; ...multiply by $a...13.33% │  ...f3f: mov    %r11d,0xc(%rsi)  ; ...store $v3.82% │╭ ...f43: jmp    ...f4e││                                 ; if true, then0.02% ↘│ ...f45: imul   0x14(%rsi),%r11d ; ...multiply by $b...18.70%  │ ...f4a: mov    %r11d,0xc(%rsi)  ; ...store $v6.12%  ↘ ...f4e: add    $0x10,%rsp...f52: pop    %rbp0.08%    ...f53: cmp    0x340(%r15),%rsp...f5a: ja     ...f6110.81%    ...f60: retq

总结

高级JIT 编译器可以只编译方法的实际活动部分。这简化了生成的代码和 JIT 编译器开销。另一方面,这会使预热变得复杂:为了避免突然重新编译,您需要使用与稍后运行的配置文件类似的配置文件进行预热,以便编译所有路径。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词