调试方法
俗话说磨刀不误砍柴工,在研究即时编译器前了解调试方法和准备好调试工具是有必要的,了解了它们,可以从外部更直观地了解编译器的内部情况。
编译日志
简单观察编译行为可以使用-XX:+PrintCompilation参数实现,如代码清单7-7所示,它会输出所有编译过的方法:
代码清单7-7 -XX:+PrintCompilation输出
时间戳 编译ID 属性 编译级别 方法名(方法大小) 退优化
30631 3958 n 0 java.lang.Class::getDeclaringClass0 (native)
30632 3959 b 3 MemNode::main (19 bytes)
30634 3960 b 4 MemNode::main (19 bytes)
30637 3959 3 MemNode::main (19 bytes) made not entrant
30638 3961 % b 4 MemNode::main @ 2 (19 bytes)
时间戳表示编译完成的时间,与该时间相对的是JVM启动时间。属性字符有多种:%表示栈上替换(方法后面的@2表示发生栈上替换的字节码索引);s表示编译同步方法;!表示方法存在异常处理器;b表示阻塞模式下发生的编译;n表示封装native方法所发生的编译。编译级别即分层编译的等级。方法大小表示Java字节码大小而非编译产出的机器代码大小。
如果发生退优化,需要撤销之前编译过的方法,这时候尾部会标注made not entrant(方法取消进入),或made zombie(僵尸代码)。产生made not entrant的原因可能是编译器的乐观假设被打破,或者发生了分层编译。如代码清单7-7所示,MemNode::main方法首先经过3级的C1编译,后续又经过4级的C2编译,此时C1产生的机器代码就会被标注为取消进入,但是方法仍然保留在CodeCache,直到该方法不被虚拟机及服务线程使用,也不被其他方法调用时,再将方法标注为made zombie。
编译神谕
编译神谕是指-XX:CompileCommand=subcommand,命令,通过它可以使用户控制虚拟机中即时编译器的行为。subcommand表示子命令,每个子命令都有特定的行为。
break:在编译器和生成的机器代码中打断点。
print:输出方法的汇编表示。
exclude:不编译和内联某个方法。
inline:总是内联某个方法。
dontinline:不内联某个方法。
compileonly:只编译。
log:用日志记录编译过程。
用于指定方法,可以使用package/Class.method形式,也可以使用package.Class::method形式。方法名和类名可以使用星号(*)模糊匹配。
可视化工具
本节介绍3个主要的编译器的可视化工具。
1. c1visualizer
前文提到,中间表示是编译器的灵魂,为了了解编译器的工作机制,可以使用-XX:+PrintIR输出C1的HIR,使用-XX:+PrintIRWithLIR输出C1的LIR,但是这些选项是以文本形式输出的,而C1的中间表示是图IR,文本表示很难直观地表达它的结构,所以c1visualizer应运而生。
c1visualizer可以可视化地输出C1编译器的HIR和LIR,还能可视化LIR寄存器分配阶段的值的存活范围,如图7-7所示。
2. idealgraphvisualizer
idealgraphvisualizer是C2的中间表示的可视化工具,它可以帮助理解C2理想图的构造过程。
可以使用-XX:PrintIdealGraphLevel=配合-XX:PrintIdealGraphFile=ideal.xml输出理想图的文本形式供idealgraphvisualizer分析。-XX:PrintIdealGraphLevel的可选值是0~4,值越大,输出的过程越详细,如图7-8所示。
idealgraphvisualizer还支持自定义过滤器以过滤理想图中的部分节点,同时支持Graal IR的可视化。
3. JITWatch
JITWatch可以方便地映射源码、字节码和JIT生成的机器代码,还可以支持可视化Code Cache、nmethod、编译时间线等,如图7-9所示。