[讨论] [HotSpot VM] 如何判断一个方法当前的执行上下文或调用链是在一个循环或递归调用中?

ZHH2009 2014-11-06
我使用了JVMTI的Method Entry/Exit事件回调API来记录方法的调用日志,
后来发现如果当前被调用的方法所在的执行上下文或调用链是在一个循环或递归调用中,
那么HotSpot VM会很慢,如果循环次数很大,或递归很深,
那么就会产生大量无用的日志。

我想让HotSpot VM达到这样的效果:
当前被调用的方法能够知道自己所在的执行上下文或调用链是否在一个循环或递归调用中,
并且HotSpot可以配置一个参数,比如参数值是3,循环次数是1万,
那么只有前3次会触发JVMTI事件回调,并且HotSpot能告知总的循环次数是多少。

当退出循环或递归调用时,
上次在循环或递归调用中所涉及的所有方法的JVMTI事件回调重新开启。

想做这件事的目的是想让机器能模拟人们看代码的习惯,
比如在调试一个while循环的代码时,大多数情况下只会遍历两次,
然后就跳到下一个断点了。

HotSpot VM虽然提供了调试相关的API,
但是在调试模式运行时每次循环迭代还是会触发JVMTI回调事件。


想到的一想思路:
Javac会把循环编译成goto和各类if字节码,
然后在goto和各类if字节码对应的汇编中加一些拦截代码,
这种方法要修改超多地方,
比如templateTable_x86_32.cpp、interpreterRuntime.cpp、jvmti实现……

另一种办法是修改Javac的代码,
在涉及循环的地方注入一些代码,把一些信息放到当前线程的ThreadLocal里,
这种方法也同样要修改大量代码。

递归调用的识别,在HotSpot VM里可以从当前线程的栈帧中做一些启发式的判断,
如果递归调用都是调用自身,那么就比较容易判断,
如果中间又隔了多个其他方法再调到自身,这就不好判断了。
RednaxelaFX 2014-11-07
HotSpot VM里也可以方便的存一些thread local的信息。最简单的办法就是把你要的信息挂在JavaThread结构上。

循环其实超级好拦截。默认情况下HotSpot VM的解释器要记录方法的回边计数器(循环次数)。搜一下UseLoopCounter就可以找到解释器里的所有处理循环的地方。在附近找个地方加上你的拦截代码放到thread local的状态里就好了。

递归也是类似,可以做一个thread local的HashTable<Method*, int>,然后在解释器的方法入口的地方检查这个表里有没有这个Method*,没的话加进去,有的话计数器加1,然后返回的时候…你懂的。
ZHH2009 2014-11-07
RednaxelaFX 写道

循环其实超级好拦截。默认情况下HotSpot VM的解释器要记录方法的回边计数器(循环次数)。搜一下UseLoopCounter就可以找到解释器里的所有处理循环的地方。在附近找个地方加上你的拦截代码放到thread local的状态里就好了。


是的,我前面提到的templateTable_x86_32.cpp,
还有goto、各类if字节码对应的汇编就是在TemplateTable::branch里,
UseLoopCounter的逻辑基本上也在里面。

RednaxelaFX 写道

递归也是类似,可以做一个thread local的HashTable<Method*, int>,然后在解释器的方法入口的地方检查这个表里有没有这个Method*,没的话加进去,有的话计数器加1,然后返回的时候…你懂的。


我之前的方案为了不修改HotSpot的代码,
基于JVMTI的agent为了避免重复解析类和方法,
也用了thread local保存上一次的类和方法的缓存数据(通过Get/SetThreadLocalStorage)
这个方案太晚了,因为是在触发JVMTI事件后才进行的。

就像你说的,放在解释器的方法入口处会更好,
我还没做完,目前的方案基本上就跟你提到的一样。

我还想怎么通过JVMTI/JNI直接就实现了,
因为通过JVMTI也是能知道栈帧的,
只是我老想不明白怎么通过JVMTI来反转控制HotSpot让它别触发JVMTI事件了,
或许真的做不到,实在不行就继续修改HotSpot的代码……

呃,我已经改了不少HotSpot的代码,每次升级JDK新版本都得自己合并
ZHH2009 2014-11-07
如果在JVMTI中通过当前线程的栈帧变化来推测是循环还是递归,
可能稍微有点难度,不过循环和递归时栈帧的push/pop都是有规律的。

然后在线程的thread local保存一个标志,
HotSpot再通过这个标志的true/false值来判断是否调用notify_method_entry()、notify_method_exit(),这样改的代码就少点。

这个方案的难点就是如何通过栈帧变化来精确推测是循环还是递归。
ZHH2009 2014-11-07
我试了UseLoopCounter的方案,不太靠谱。
比如,假如有下面的java代码:
    public static void main(String[] args) {
        int count = 5;
        for(int i = 0; i < count; i++) {
            run1();
        }
        
        count = 2;
        for(int i = 0; i < count; i++) {
            run2();
        }
    }

HotSpot VM中的实现逻辑会把第一个for循环后得到的backedge_counter延续到第二个for循环,

第二个for循环的backedge_counter计数不是从0开始。

这样就会导致run2方法的JVMTI事件也不触发了,
因为在执行第二个for循环前backedge_counter的值肯定已经超过5了
(HotSpot VM内部每次加8,这个细节要注意)

这个方案可能还得改其他代码才行,比如backedge_counter在什么时候要重新置0?
ZHH2009 2014-11-07
另外,我起初把backedge_counter的逻辑代码放在InterpreterGenerator::generate_normal_entry,像这样:
  {
    const Address backedge_counter  (rax,
                  MethodCounters::backedge_counter_offset() +
                  InvocationCounter::counter_offset());
    Label skipJVMTI;
	 
    __ movptr(rax, Address(rbx, Method::method_counters_offset()));
    __ movl(rax, backedge_counter);               // load backedge counter
    __ andl(rax, InvocationCounter::count_mask_value);  // mask out the status bits
	  __ cmpl(rax, 3);
	  __ jcc(Assembler::greater, skipJVMTI);
    // jvmti support
    __ notify_method_entry();

    __ bind(skipJVMTI);
  }


后来发现错了,与TemplateTable::branch的代码相比:
      // increment counter
      __ movptr(rcx, Address(rcx, Method::method_counters_offset()));
      __ movl(rax, Address(rcx, be_offset));        // load backedge counter
      __ incrementl(rax, InvocationCounter::count_increment); // increment counter
      __ movl(Address(rcx, be_offset), rax);        // store counter

      __ movl(rax, Address(rcx, inv_offset));    // load invocation counter

      __ andl(rax, InvocationCounter::count_mask_value);     // and the status bits
      __ addl(rax, Address(rcx, be_offset));        // add both counters


为什么前后两段代码的rbx和rcx不是同一个Method *
原来前者对应run1,后者才是main
RednaxelaFX 2014-11-08
我说的参考UseLoopCounter是说只要搜索这个参数在解释器里出现的位置,就可以知道哪里是循环回边,于是把你要加的逻辑加在那些位置就可以。不是叫你直接用method backedge counter啊。那个counter每个方法只有一个,所有循环都加在一起了,不适合你的场景用。

不过既然你提到“热点”,你可以参考的模型是JKU做的基于HotSpot C1的trace-based编译器:http://www.ssw.jku.at/Research/Papers/Haeubl11/
他们的东西在每个循环都有计数器,而且也有信息记录在thread local的数据结构里。

他们改写为trace anchor的地方你都会感兴趣
概念很简单:trace-based compilation发现热的执行路径(trace)时把它编译掉。而你在发现热的路径时,如果该路径包含JVMTI事件的话,禁用掉这些事件
ZHH2009 2015-03-12
最近才又重新拾起这个问题,

Trace-based Compilation论文中提到
引用

Our trace-based JIT compiler is integrated into Oracle’s Java Hot-
Spot VM, using the early access version b134 of the upcoming
JDK 7 [21].


这个不是OpenJDK吧,我下了OpenJDK 7 b134的代码没看到对应的实现,
http://hg.openjdk.java.net/jdk7/jdk7/hotspot/rev/447e6faab4a8
jdk-8u也没有。
RednaxelaFX 2015-03-14
ZHH2009 写道
最近才又重新拾起这个问题,

Trace-based Compilation论文中提到
引用
Our trace-based JIT compiler is integrated into Oracle’s Java Hot-
Spot VM, using the early access version b134 of the upcoming
JDK 7 [21].


这个不是OpenJDK吧,我下了OpenJDK 7 b134的代码没看到对应的实现,
http://hg.openjdk.java.net/jdk7/jdk7/hotspot/rev/447e6faab4a8
jdk-8u也没有。

不是产品中的。论文这句话的意思不是说整合到了产品里,而是他们做的东西放在了OpenJDK 7 build 134上测试。
ZHH2009 2015-03-14
意思是这东西并没有开源出来?还是已经放到商业般的HotSpot VM中了?

看Figure 2. System overview里,新增了三处,修改了三处,
看样子也不是那么容易实现的。
Global site tag (gtag.js) - Google Analytics