[讨论] 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() 进行创建,这个过程也可以调试到。 再次谢谢大家的指点。 |