[讨论] HotSpot 解释器是怎样执行bytecode 的

cheney_love 2014-07-06
一段简单的代码:
public class Tiger {
	public static void main(String args[]){
		Tiger tiger = new Tiger();
	}
}

main 方法对应的bytecode
    省略...
    Code:
      stack=2, locals=2, args_size=1
         0: new           #1                  // class Tiger
         3: dup           
         4: invokespecial #16                 // Method "<init>":()V
         7: astore_1      
         8: return        
      LineNumberTable:
        line 5: 0
        line 6: 8
      LocalVariableTable:
   省略...


在圈子里面看了几篇R大回复的帖子:
java_main的汇编入口在哪里
Java 字节码如何执行的
对方法的执行过程有了一定的了解。
在不使用-Xcomp的情况下,虚拟机是使用默认的解释器进行代码的执行工作。
方法的调用方式:
StubRoutines::call_stub()(
        (address)&link,
        // (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
        result_val_address,          // see NOTE above (compiler problem)
        result_type,
        method(),
        entry_point,
        args->parameters(),
        args->size_of_parameters(),
        CHECK
      );

call_stub() 是在jvm初始化的时候在StubGenerator::generate_call_stub()中生成的。
entry_point : 指向的是解释器的方法入口处理函数。(这个入口函数是哪个函数?)
method() : 是要执行的方法。
方法对应的 bytecode 应该是存在methodoop的_code 字段里面。

我现在弄不明白的地方是。
1.在执行call_stub() 后,bytecode 是怎么一个一个被解释然后执行的呢,对应的代码在哪里呢。比如这个new 指令,是在哪里被解析出来然后执行的呢,在TemplateTable 有_new() 这个方法,它是在什么地方被调用的呢?
2.有帖子说是interp_masm_sparc.cpp的dispatch_next,但是我debug的时候,这个方法是在call_stub() 之前就执行完了。

谢谢
nijiaben 2014-07-06
每条字节码都对应一段汇编代码,可参考TemplateInterpreterGenerator::generate_and_dispatch方法,顾名思义就是生成当前指令的汇编代码以及跳转,
下一个字节码指针(byecode pointer)存在r13寄存器里,当要这条指令本身就要跳转(这个在指令实现里(一般在templateTable_x86_32/64.cpp里实现)就做了),或者执行完这条指令自动切换到下一条(就在上面这个方法里做,根据指令的大小,自动递增r13里的值,能在下一次环节自动跳转过去执行

至于怎么找到这条字节码的汇编代码段,就依靠Interpreter的一个映射表Interpreter::_active_table了,在上面的跳转代码里其实会用到它,这个表可能是_normal_table也可能是safept_table,取决于是否要进入安全点(比如要进行stw性质的gc的时候),对于_normal_table的设置在TemplateInterpreterGenerator::set_entry_points这个函数里
cheney_love 2014-07-06
nijiaben 写道
每条字节码都对应一段汇编代码,可参考TemplateInterpreterGenerator::generate_and_dispatch方法,顾名思义就是生成当前指令的汇编代码以及跳转,
下一个字节码指针(byecode pointer)存在r13寄存器里,当要这条指令本身就要跳转(这个在指令实现里(一般在templateTable_x86_32/64.cpp里实现)就做了),或者执行完这条指令自动切换到下一条(就在上面这个方法里做,根据指令的大小,自动递增r13里的值,能在下一次环节自动跳转过去执行

至于怎么找到这条字节码的汇编代码段,就依靠Interpreter的一个映射表Interpreter::_active_table了,在上面的跳转代码里其实会用到它,这个表可能是_normal_table也可能是safept_table,取决于是否要进入安全点(比如要进行stw性质的gc的时候),对于_normal_table的设置在TemplateInterpreterGenerator::set_entry_points这个函数里

谢谢您的回复,请问一下,generate_and_dispatch是被谁调用的呢,好像在call_stub 之前就执行完了。call_stub 和 generate_and_dispatch 是怎么串起来的呢。
谢谢
nijiaben 2014-07-06
cheney_love 写道

谢谢您的回复,请问一下,generate_and_dispatch是被谁调用的呢,好像在call_stub 之前就执行完了。call_stub 和 generate_and_dispatch 是怎么串起来的呢。
谢谢

generate_and_dispatch在启动过程中就会执行,可理解为为每种字节码指令分别创建一个方法,在运行期根据对应的指令在执行到对应的方法里。

call_stub是在真正的方法调用过程中,传到call_stub中的entry_point对于正常的方法调用而言,一般都是zerolocals或者zerolocals_synchronized对应的entry入口,具体是哪个是根据你的方法类型而言的,比如是否方法加了同步,是否是native方法等,entry创建过程请看TemplateInterpreterGenerator::generate_all方法,每种方法类型的entry生成在AbstractInterpreterGenerator::generate_method_entry这个方法里,其实也就是生成一段汇编代码,将这段汇编的开始地址设置为对应的entry入口,找到入口地址之后就开始执行一系列汇编指令,最终会看到调用到InterpreterMacroAssembler::dispatch_next分发到方法里的第一条字节码指令,而执行这条指令的入口依赖于Interpreter::_active_table,在指令执行过程中依次寻找下一条指令进行分发处理
ZHH2009 2014-07-09
这类问题确实有好几贴了,
要完全说清解释器是怎样执行bytecode的,我预想可能需要超长的篇幅,

不过最好还是先自己把代码跑起来,然后慢慢debug去分析代码。

理解这问题,需要先有很好的汇编语言基础,
然后抓住HotSpot中的一些实现关键点:
1.call_stub和method_entry_point(zerolocals、zerolocals_synchronized等)
都是用汇编实现的;
2.每一个bytecode也都是由一段汇编代码来实现;

1和2中的汇编代码都是在初始化HotSpot阶段就生成的,
你可以简单认为它们就是一些内置的方法,
call_stub调用method_entry_point,
然后在method_entry_point里调用new,new再调用dup(用跳转更合适)……

这时可以动用GDB或VS从call_stub开始调试每条汇编,
call_stub如何衔接到method_entry_point再到new,
这里就是考验你的汇编能力的时候了,
有些寄存器是关键点: esp, ebp, ebx, esi……
好比ebx里放的是main(String args[])这个java方法在HotSpot中的method指针,
通过ebx就能知道method的其他字段(例如ConstMethod),
而通过esi能遍历ConstMethod中的bytecode……

不同的HotSpot版本的实现会存在一些差异,
上面只是提了其中的一些关键点。
cheney_love 2014-07-10
谢谢大家的回复,我再仔细看看
ZHH2009 2014-07-13
我的OpenJDK-Research上面有一些相关的研究笔记可能会对你有一些帮助
call_stub、method_entry_point_zerolocals都有 https://github.com/codefollower/OpenJDK-Research/tree/master/hotspot/my-docs/interpreter

以后会慢慢补充。
cheney_love 2014-07-15
ZHH2009 写道
我的OpenJDK-Research上面有一些相关的研究笔记可能会对你有一些帮助
call_stub、method_entry_point_zerolocals都有 https://github.com/codefollower/OpenJDK-Research/tree/master/hotspot/my-docs/interpreter

以后会慢慢补充。

谢谢您的研究笔记,内容很丰富,已经fork,慢慢学习,我现在还有个疑问,就是解析器在解析new 指令的时候,class 文件的解析是在哪里完成的呢。是在new 指令对应的那段汇编里面吗。现在正在看《深入理解计算机系统》恶补一下汇编。  谢谢
ZHH2009 2014-07-15
在研究HotSpot的代码时,可以按顺序围绕下面5个大问题去探索:
1. HotSpot内部的各个模块是如何初始化的?
2. 类的装载、链接、初始化在HotSpot中是如何完成的?
3. HotSpot如何执行java方法中的字节码?
4. HotSpot如何实现GC?
5. HotSpot如何实现JIT编译器?

你问的其实是第3个问题,前面说了要回答好这个问题是需要很多篇幅的,
回答它之前必须讲清楚前两个问题,
比如在第一个问题中不讲清楚解释器模块、stub模块如何初始化你就不懂call_stub和字节码对应的的汇编是如何生成的;

同样需要在第二个问题中理清类的装载、链接,你才知道class 文件的解析是如何做的,
才知道原始的字节码放在哪里?

所以,真想要研究HotSpot,就得循序渐进一步步来,
回答好前面3个问题就足够写一本600页以上的书了。

class 文件的解析在src\share\vm\classfile\classFileParser.cpp实现,
入口是: ClassFileParser::parseClassFile
cheney_love 2014-07-17
ZHH2009 写道
在研究HotSpot的代码时,可以按顺序围绕下面5个大问题去探索:
1. HotSpot内部的各个模块是如何初始化的?
2. 类的装载、链接、初始化在HotSpot中是如何完成的?
3. HotSpot如何执行java方法中的字节码?
4. HotSpot如何实现GC?
5. HotSpot如何实现JIT编译器?

1.今天经过不断调试,基本弄明白了new 指令中load class文件的地方了。
templateTable_x86_64.cpp 中的TemplateTable::_new()方法。
创建对象分两种方式:快速分配和慢速分配。
快速分配是针对class 文件已经被解析过的情况,这种情况下,直接从常量池取,然后分配对象空间就可以了。该过程直接通过生成的汇编就完成了,所以调试不到。
如果class 文件是第一次加载,那么就直接进入慢速分配。对应的代码:
  // slow case
  __ bind(slow_case);
  __ get_constant_pool(c_rarg1);
  __ get_unsigned_2_byte_index_at_bcp(c_rarg2, 1);
  call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::_new), c_rarg1, c_rarg2);
  __ verify_oop(rax);
首先解析class 文件,然后调用InterpreterRuntime::_new() 对对象进行初始化,这个过程可以调试到。

2. TemplateTable::newarray() 没有快速分配和慢速分配之说,所以每次都调用InterpreterRuntime::newarray() 进行创建,这个过程也可以调试到。


再次谢谢大家的指点。
Global site tag (gtag.js) - Google Analytics