要观察jit循环展开优化,需先配置hsdis动态库并验证加载成功,再用-xx:compilecommand等参数锁定目标方法编译,最后通过汇编中边界检查消失、迭代体变宽、循环变量步进增大三个信号识别展开。

要真正看到 JIT 编译器对循环做的展开优化,光靠写个 for 循环再跑一下是没用的。必须让目标方法被 JIT 实际编译,并把生成的机器码清晰、干净地打出来——而 hsdis 就是完成这一步的关键桥梁。
先让 hsdis 正常加载,否则汇编根本不会出现
PrintAssembly 默认不工作,不是配置错了,而是缺了 hsdis 动态库。JDK 官方发行版(Oracle/OpenJDK)都不自带它。
- Linux:下载 hsdis-amd64.so(或 aarch64.so),放进
$JAVA_HOME/jre/lib/amd64/server/ - macOS:对应 hsdis-amd64.dylib,路径为
$JAVA_HOME/jre/lib/server/ - Windows:用 hsdis-amd64.dll,放在
%JAVA_HOME%\jre\bin\server\
验证是否成功:运行 java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -version。如果看到 Loaded disassembler from …,说明加载成功;若报 Could not load hsdis-xxx,就是文件名、路径或架构不匹配。
只看目标方法,避免日志被淹没
JIT 编译是按方法触发的,默认阈值是方法调用 10000 次(Client VM)或更复杂条件(Tiered)。你不锁定,输出里全是类初始化、getter/setter 等无关汇编。
- 加
-XX:CompileCommand=compileonly,*YourClass.yourMethod,强制只编译你关心的方法 - 配合
-Xcomp(全编译模式)或-XX:TieredStopAtLevel=1(跳过 C1,直上 C2)加快触发 - 重定向输出:
2> asm.log,再用grep -A10 -B2 "yourMethod\|add\|mov\|jmp"快速定位循环体
从汇编里识别循环展开的三个关键信号
真正的循环展开不是“多复制几遍指令”那么简单,它会消除控制开销、合并访存、暴露并行性。重点看这三处:
-
边界检查消失:没有
cmp %rax,%rdx; jl Lxx这类数组长度比较和跳转 → JIT 已证明索引安全,删掉了冗余检查 -
迭代体变宽、变直:原本一个
mov + add + inc + cmp + jne的五指令循环体,变成连续多个mov + add(比如 4 组),中间无跳转 → 展开因子为 4 -
循环变量操作减少:
add $4,%i替代inc %i,且循环计数次数变为原长 ÷ 展开因子 → 说明 JIT 把多次迭代合并成单次大步进
对照内联 + 热点确认,排除干扰
如果方法太小(比如只调用另一个方法),JIT 可能选择内联而非展开;如果没达到热点阈值,就还在解释执行,压根没汇编。
- 加
-XX:+PrintInlining,确认你的方法是否被内联 —— 若显示 inlined (hot),那看到的汇编其实是外层方法展开后的结果 - 加
-XX:+PrintCompilation,观察控制台是否打出类似 12345 123 b Test::sum (27 bytes) → 表示该方法已被 C2 编译 - 确保循环体足够“热”:在 main 中循环调用目标方法至少 10 万次,或用
-XX:CompileThreshold=100临时降低阈值(仅测试)










