JVM常用概念之即时常量

问题

JVM的优化器对程序可以利用的常量值做了什么工作?

基础知识

常量的优化是最便捷的,常量已经在编译时进行了处理,在应用运行时不需要做如何操作,那对于变量、常量变量、静态变量、常量静态变量编译和运行时会有什么区别呢?

java 复制代码
class M {
  final int x;
  M(int x) { this.x = x; }
}

M m1 = new M(1337);
M m2 = new M(8080);

void work(M m) {
  return m.x; // what to compile in here, 1337 or 8080?
}

实验

源码

java 复制代码
在这里插入代码片@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class JustInTimeConstants {

    static final long x_static_final = Long.getLong("divisor", 1000);
    static       long x_static       = Long.getLong("divisor", 1000);
           final long x_inst_final   = Long.getLong("divisor", 1000);
                 long x_inst         = Long.getLong("divisor", 1000);

    @Benchmark public long _static_final() { return 1000 / x_static_final; }
    @Benchmark public long _static()       { return 1000 / x_static;       }
    @Benchmark public long _inst_final()   { return 1000 / x_inst_final;   }
    @Benchmark public long _inst()         { return 1000 / x_inst;         }

}

运行结果

bash 复制代码
Benchmark                          Mode  Cnt  Score   Error  Units
JustInTimeConstants._inst          avgt   15  9.670 ± 0.014  ns/op
JustInTimeConstants._inst_final    avgt   15  9.690 ± 0.036  ns/op
JustInTimeConstants._static        avgt   15  9.705 ± 0.015  ns/op
JustInTimeConstants._static_final  avgt   15  1.899 ± 0.001  ns/op

通过-prof perfasm进行进一步分析,结果如下:

bash 复制代码
# JustInTimeConstants._inst / _inst_final hottest loop
0.21%            ↗  mov    0x40(%rsp),%r10
0.02%            │  mov    0x18(%r10),%r10    ; get field x_inst / x_inst_final
                 |  ...
0.13%            │  idiv   %r10               ; ldiv
76.59%   95.38%  │  mov    0x38(%rsp),%rsi    ; prepare and consume the value (JMH infra)
0.40%            │  mov    %rax,%rdx
0.10%            │  callq  CONSUME
                 |  ...
1.51%            │  test   %r11d,%r11d        ; call @Benchmark again
                 ╰  je     BACK

如上述执行结果所示,大部分时间成本都花在执行实际的整数除法上。

bash 复制代码
# JustInTimeConstants._static hottest loop
0.04%            ↗  movabs $0x7826385f0,%r10  ; native mirror for JustInTimeConstants.class
0.02%            │  mov    0x70(%r10),%r10    ; get static x_static
                 |  ...
0.02%            │  idiv   %r10               ;*ldiv
72.78%   95.51%  |  mov    0x38(%rsp),%rsi    ; prepare and consume the value (JMH infra)
0.38%            │  mov    %rax,%rdx
0.04%    0.06%   │  data16 xchg %ax,%ax
         0.02%   │  callq  CONSUME
                 |  ...
0.13%            │  test   %r11d,%r11d        ; call @Benchmark again
                 ╰  je     BACK

如上述执行结果所示,static修饰的字段,它从静态字段所在的本地类镜像中读取静态字段。由于运行时知道我们正在处理的类(静态字段访问是静态解析的!),我们将常量指针内联到镜像,并通过其预定义的偏移量访问该字段。但是,由于我们不知道字段的值是什么------实际上有人可能在代码生成后更改了它------我们仍然执行相同的整数除法。

bash 复制代码
# JustInTimeConstants._static_final hottest loop
1.36%    1.40%   ↗  mov    %r8,(%rsp)
7.73%    7.40%   │  mov    0x8(%rsp),%rdx       ; <--- slot holding the "long" constant "1"
0.45%    0.51%   │  mov    0x38(%rsp),%rsi      ; prepare and consume the value (JMH infra)
3.59%    3.24%   │  nop
1.44%    0.54%   │  callq  CONSUME
                 | ...
3.46%    2.37%   │  test   %r10d,%r10d          ; call @Benchmark again
                 ╰  je     BACK

如上述_static_final的汇编执行结果,JIT 编译器确切地知道它正在处理的值,因此它可以积极地对其进行优化。在这里,循环计算只是重用了保存预先计算的值"1000 / 1000"的槽,即"1"。因此,性能可以通过编译器通过static final进行常量折叠的能力来解释。

总结

在上述示例中,字节码编译器(例如 javac)不知道static final字段的值是什么,因为该字段是用运行时值初始化的。当 JIT 编译发生时,类已成功初始化,并且值已存在,可以使用!这实际上是即时常量。这允许开发非常高效但运行时可调整的代码:事实上,整个示例被认为是基于预处理器的断言的替代品。而在C++领域,因为编译是完全提前的,因此如果你想让关键代码依赖于运行时选项。

这个示例的一个重要部分是解释器/分层编译。类初始化器通常是冷代码,因为它们只执行一次。但更重要的是处理类初始化的延迟部分,当我们想在第一次访问字段时加载和初始化类时。解释器或基线 JIT 编译器(例如 Hotspot 中的 C1)为我们运行它。当优化 JIT 编译器(例如 Hotspot 中的 C2)为相同方法运行时,重新编译的方法所需的类通常已完全初始化,并且它们的static final已完全已知。

相关推荐
芒克芒克13 小时前
深入浅出JVM的运行时数据区
java·开发语言·jvm·面试
月明长歌14 小时前
JavaThread类详解核心属性、常用方法与实践
java·开发语言·jvm
kaico201814 小时前
JVM的垃圾回收
开发语言·jvm
zfj32114 小时前
java垃圾收集 minorgc majargc fullgc
java·开发语言·jvm·gc·垃圾收集器
烟沙九洲15 小时前
JVM 堆内存分代
java·jvm
独自破碎E16 小时前
JVM由哪些部分组成?
jvm
曹轲恒1 天前
JVM之垃圾回收算法(GC)
jvm·算法
xiaolyuh1232 天前
ThreadLocalMap 中弱引用被 GC 后的行为机制解析
java·jvm·redis
这周也會开心2 天前
JVM-垃圾回收算法
jvm·算法
代码or搬砖2 天前
JVM垃圾回收算法
jvm·算法