bigywater.tech
Java · 301

Template Interpreter:解释器如何执行字节码

解释器不是一个巨大的 switch,HotSpot 是在启动时为每个字节码生成一小段机器码。这篇把这些机器码打印出来,反汇编一个,再量解释执行有多慢、什么时候被编译。

275
JDK 21 解释器 codelet 数(每个字节码一个,共 81K 机器码)
8 个指令字
iadd 在 AArch64 上的 codelet 长度(2 次弹栈 + add + 分发)
4.383 vs 0.299 ns
同一循环每次迭代:-Xint 对比只用 C1

codelet

实测 · 本机执行输出
  JDK 21: 代码总量 81K,codelet 总数 275,平均 301;有字节码编号的 codelet 239 个(0–238),其中 fast_* / nofast_* 的「改写后字节码」33 个
  JDK 21 部分字节码 codelet 的机器码大小(字节): {'nop': 56, 'iconst_1': 64, 'iload_0': 64, 'iload': 152, 'iadd': 32, 'idiv': 48, 'iinc': 128, 'ifeq': 616, 'if_icmplt': 624, 'goto': 600, 'getfield': 520, 'fast_igetfield': 64, 'putfield': 1152, 'new': 384, 'invokestatic': 640, 'invokevirtual': 1160, 'invokeinterface': 2592, 'ireturn': 784, 'aastore': 1008, 'athrow': 16}
  最小的 5 个: [('lookupswitch', 16), ('athrow', 16), ('istore_0', 24), ('istore_1', 24), ('istore_2', 24)] ;最大的 5 个: [('return_register_finalizer', 840), ('aastore', 1008), ('putfield', 1152), ('invokevirtual', 1160), ('invokeinterface', 2592)]
  大小分布: {'0-32': 60, '33-64': 75, '65-128': 44, '129-700': 47, '701-5000': 13}
  JDK 27: 代码总量 85K,codelet 总数 277,平均 316;有字节码编号的 codelet 239 个(0–238),其中 fast_* / nofast_* 的「改写后字节码」33 个
  JDK 27 同样几个 codelet 的大小: {'iconst_1': 64, 'iload_0': 64, 'iadd': 32, 'iinc': 128, 'if_icmplt': 624, 'goto': 600, 'invokevirtual': 1264, 'ireturn': 952}

反汇编 iadd

实测 · 本机执行输出
  iadd(32 字节 = 8 个指令字):
     ldr	w0, [x20], #8
     ldr	w1, [x20], #8
     add	w0, w1, w0
     ldrb	w8, [x22, #1]!
     add	w9, w8, #1024
     ldr	x9, [x21, w9, uxtw #3]
     br	x9
     nop
  iload_0(64 字节 = 16 个指令字):
     str	x0, [x20, #-8]!
     b	#36
     str	s0, [x20, #-8]!
     b	#28
     str	d0, [x20, #-16]!
     b	#20
     str	xzr, [x20, #-8]!
     str	x0, [x20, #-8]!
     b	#8
     str	x0, [x20, #-8]!
     ldr	x0, [x24]
     ldrb	w8, [x22, #1]!
     add	w9, w8, #1024
     ldr	x9, [x21, w9, uxtw #3]
     br	x9
     nop

解释执行的速度

实测 · 本机执行输出
  loop(int) 的字节码: 0:lconst_0 | 1:lstore_1 | 2:iconst_0 | 3:istore_3 | 4:iload_3 | 5:iload_0 | 6:if_icmpge     20 | 9:lload_1 | 10:iload_3 | 11:i2l | 12:ladd | 13:lstore_1 | 14:iinc          3, 1 | 17:goto          4 | 20:lload_1 | 21:lreturn
  每次迭代执行的字节码数(从循环头 4 到 goto 17,按 javap 计数): 10
  -Xint(只用解释器)                       每次迭代 4.383 ns;折合每个字节码 0.44 ns
  只用 C1(-XX:TieredStopAtLevel=1)     每次迭代 0.299 ns;折合每个字节码 0.03 ns
  默认分层编译(C1 + C2)                    每次迭代 0.307 ns;折合每个字节码 0.03 ns

调用计数的阈值

实测 · 本机执行输出
  JVM 选项: 默认(Tier3InvocationThreshold=200,Tier3MinInvocationThreshold=100,Tier3CompileThreshold=2000)
    调用    50 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   150 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   199 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   200 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   201 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   210 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   250 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   300 次 → 28 6 3 Main::hot (6 bytes)
    调用   500 次 → 19 6 3 Main::hot (6 bytes)
    调用  1000 次 → 17 6 3 Main::hot (6 bytes)
    调用  1001 次 → 27 6 3 Main::hot (6 bytes)
    调用  1100 次 → 24 6 3 Main::hot (6 bytes)
    调用  1500 次 → 28 6 3 Main::hot (6 bytes)
    调用  5000 次 → 20 6 3 Main::hot (6 bytes) ; 21 7 4 Main::hot (6 bytes) ; 21 6 3 Main::hot (6 bytes) made not entrant
    调用 20000 次 → 20 6 3 Main::hot (6 bytes) ; 20 7 4 Main::hot (6 bytes) ; 21 6 3 Main::hot (6 bytes) made not entrant
  JVM 选项: ['-XX:Tier3InvocationThreshold=1000', '-XX:Tier3MinInvocationThreshold=1000']
    调用    50 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   150 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   199 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   200 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   201 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   210 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   250 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   300 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用   500 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用  1000 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用  1001 次 → 没有出现在 PrintCompilation 里(没有被编译)
    调用  1100 次 → 21 2 3 Main::hot (6 bytes)
    调用  1500 次 → 26 2 3 Main::hot (6 bytes)
    调用  5000 次 → 17 2 3 Main::hot (6 bytes)
    调用 20000 次 → 24 2 3 Main::hot (6 bytes) ; 24 3 4 Main::hot (6 bytes) ; 24 2 3 Main::hot (6 bytes) made not entrant

回边与 OSR

实测 · 本机执行输出
    循环    10000 次 → 没有 Main::main 的编译记录
    循环    40000 次 → 没有 Main::main 的编译记录
    循环    60000 次 → 没有 Main::main 的编译记录
    循环    60500 次 → 27 6 % 3 Main::main @ 12 (60 bytes)
    循环    62000 次 → 27 6 % 3 Main::main @ 12 (60 bytes)
    循环    65000 次 → 27 6 % 3 Main::main @ 12 (60 bytes)
    循环    70000 次 → 26 6 % 3 Main::main @ 12 (60 bytes)
    循环   200000 次 → 22 6 % 3 Main::main @ 12 (60 bytes) ; 22 7 3 Main::main (60 bytes) ; 22 8 % 4 Main::main @ 12 (60 bytes) ; 23 6 % 3 Main::main @ 12 (60 bytes) made not entrant
    循环  2000000 次 → 22 6 % 3 Main::main @ 12 (60 bytes) ; 22 7 3 Main::main (60 bytes) ; 23 8 % 4 Main::main @ 12 (60 bytes) ; 24 6 % 3 Main::main @ 12 (60 bytes) made not entrant ; 25 8 % 4 Main::main @ 12 (60 bytes) made not entrant

通知频率

实测 · 本机执行输出
   {'Tier0InvokeNotifyFreqLog': 7, 'Tier0BackedgeNotifyFreqLog': 10, 'Tier3InvocationThreshold': 200, 'Tier3MinInvocationThreshold': 100, 'Tier3BackEdgeThreshold': 60000}
  调用计数每 128 次通知一次:大于 Tier3InvocationThreshold=200 的第一个计数点是 256;大于 1000 的第一个计数点是 1024
  回边计数每 1024 次通知一次:大于 Tier3BackEdgeThreshold=60000 的第一个计数点是 60416

分步演示

模板解释器
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12 与 27,Apple Silicon AArch64;反汇编用 Homebrew llvm 22 的 llvm-mc
    • 没有验证:x86 上 codelet 的指令序列、栈顶缓存状态(tos)的完整含义、不同 GC 屏障对解释器 codelet 的影响、codelet 大小随运行变化的原因(putfield 在两次运行里相差 8 字节,没有收录)、解释器与 C1 之间的 profile 数据结构(MDO)、-XX:+PrintBytecodeHistogram 等 debug 选项(发布版没有)
    京ICP备2026067502号-1京公网安备11010502063487号