第6篇-Java方法新栈帧的创建
在 第2篇-JVM虚拟机这样来调用Java主类的main()方法 介绍JavaCalls::call_helper()函数的实现时提到过如下一句代码:
address entry_point = method->from_interpreted_entry();
这个参数会做为实参传递给StubRoutines::call_stub()函数指针指向的“函数”,然后在 第4篇-JVM终于开始调用Java主类的main()方法啦 介绍到通过callq指令调用entry_point,那么这个entry_point到底是什么呢?这一篇我们将详细介绍。
首先看from_interpreted_entry()函数实现,如下:
源代码位置:/openjdk/hotspot/src/share/vm/oops/method.hpp volatile address from_interpreted_entry() const{ return (address)OrderAccess::load_ptr_acquire(&_from_interpreted_entry); }
_from_interpreted_entry只是Method类中定义的一个属性,如上方法直接返回了这个属性的值。那么这个属性是何时赋值的?其实是在方法连接(也就是在类的生命周期中的类连接阶段会进行方法连接)时会设置。方法连接时会调用如下方法:
// Called when the method_holder is getting linked. // Setup entrypoints so the method // is ready to be called from interpreter, // compiler, and vtables. void Method::link_method(methodHandle h_method, TRAPS) { // ... address entry = Interpreter::entry_for_method(h_method); // Sets both _i2i_entry and _from_interpreted_entry set_interpreter_entry(entry); // ... }
首先调用Interpreter::entry_for_method()函数根据特定方法类型获取到方法的入口,得到入口entry后会调用set_interpreter_entry()函数将值保存到对应属性上。set_interpreter_entry()函数的实现非常简单,如下:
void set_interpreter_entry(address entry) { _i2i_entry = entry; _from_interpreted_entry = entry; }
可以看到为_from_interpreted_entry属性设置了entry值。
下面看一下entry_for_method()函数的实现,如下:
static address entry_for_method(methodHandle m) { return entry_for_kind(method_kind(m)); }
首先通过method_kind()函数拿到方法对应的类型,然后调用entry_for_kind()函数根据方法类型获取方法对应的入口entry_point。调用的entry_for_kind()函数的实现如下:
static address entry_for_kind(MethodKind k){ return _entry_table[k]; }
这里直接返回了_entry_table数组中对应方法类型的entry_point地址。
这里涉及到Java方法的类型MethodKind,由于要通过entry_point进入Java世界,执行Java方法相关的逻辑,所以entry_point中一定会为对应的Java方法建立新的栈帧,但是不同方法的栈帧其实是有差别的,如Java普通方法、Java同步方法、有native关键字的Java方法等,所以就把所有的方法进行了归类,不同类型获取到不同的entry_point入口。到底有哪些类型,我们可以看一下MethodKind这个枚举类中定义出的枚举常量:
enum MethodKind { zerolocals, // 普通的方法 zerolocals_synchronized, // 普通的同步方法 native, // native方法 native_synchronized, // native同步方法 ... }
当然还有其它一些类型,不过最主要的就是如上枚举类中定义出的4种类型方法。
为了能尽快找到某个Java方法对应的entry_point入口,把这种对应关系保存到了_entry_table中,所以entry_for_kind()函数才能快速的获取到方法对应的entry_point入口。 给数组中元素赋值专门有个方法:
void AbstractInterpreter::set_entry_for_kind(AbstractInterpreter::MethodKind kind, address entry) { _entry_table[kind] = entry; }
那么何时会调用set_entry_for_kind()函数呢,答案就在TemplateInterpreterGenerator::generate_all()函数中,generate_all()函数会调用generate_method_entry()函数生成每种Java方法的entry_point,每生成一个对应方法类型的entry_point就保存到_entry_table中。
下面详细介绍一下generate_all()函数的实现逻辑,在HotSpot启动时就会调用这个函数生成各种Java方法的entry_point。调用栈如下:
TemplateInterpreterGenerator::generate_all() templateInterpreter.cpp InterpreterGenerator::InterpreterGenerator() templateInterpreter_x86_64.cpp TemplateInterpreter::initialize() templateInterpreter.cpp interpreter_init() interpreter.cpp init_globals() init.cpp Threads::create_vm() thread.cpp JNI_CreateJavaVM() jni.cpp InitializeJVM() java.c JavaMain() java.c start_thread() pthread_create.c
调用的generate_all()函数将生成一系列HotSpot运行过程中所执行的一些公共代码的入口和所有字节码的InterpreterCodelet,一些非常重要的入口实现逻辑会在后面详细介绍,这里只看普通的、没有native关键字修饰的Java方法生成入口的逻辑。generate_all()函数中有如下实现:
#define method_entry(kind) \ { \ CodeletMark cm(_masm, "method entry point (kind = " #kind ")"); \ Interpreter::_entry_table[Interpreter::kind] = generate_method_entry(Interpreter::kind); \ } method_entry(zerolocals)
其中method_entry是一个宏,扩展后如上的method_entry(zerolocals)语句变为如下的形式:
Interpreter::_entry_table[Interpreter::zerolocals] = generate_method_entry(Interpreter::zerolocals);
_entry_table变量定义在AbstractInterpreter类中,如下:
static address _entry_table[number_of_method_entries];
number_of_method_entries表示方法类型的总数,使用方法类型做为数组下标就可以获取对应的方法入口。调用generate_method_entry()函数为各种类型的方法生成对应的方法入口。generate_method_entry()函数的实现如下:
address AbstractInterpreterGenerator::generate_method_entry(AbstractInterpreter::MethodKind kind) { bool synchronized = false; address entry_point = NULL; InterpreterGenerator* ig_this = (InterpreterGenerator*)this; // 根据方法类型kind生成不同的入口 switch (kind) { // 表示普通方法类型 case Interpreter::zerolocals : break; // 表示普通的、同步方法类型 case Interpreter::zerolocals_synchronized: synchronized = true; break; // ... } if (entry_point) { return entry_point; } return ig_this->generate_normal_entry(synchronized); }
zerolocals表示正常的Java方法调用,包括Java程序的main()方法,对于zerolocals来说,会调用ig_this->generate_normal_entry()函数生成入口。generate_normal_entry()函数会为执行的方法生成堆栈,而堆栈由局部变量表(用来存储传入的参数和被调用方法的局部变量)、Java方法栈帧数据和操作数栈这三大部分组成,所以entry_point例程(其实就是一段机器指令片段,英文名为stub)会创建这3部分来辅助Java方法的执行。
我们还是回到开篇介绍的知识点,通过callq指令调用entry_point例程。此时的栈帧状态在 第4篇-JVM终于开始调用Java主类的main()方法啦 中介绍过,为了大家阅读的方便,这里再次给出:
注意,在执行callq指令时,会将函数的返回地址存储到栈顶,所以上图中会压入return address一项。
CallStub()函数在通过callq指令调用generate_normal_entry()函数生成的entry_point时,有几个寄存器中存储着重要的值,如下:
rbx -> Method* r13 -> sender sp rsi -> entry point
下面就是分析generate_normal_entry()函数的实现逻辑了,这是调用Java方法的最重要的部分。函数的重要实现逻辑如下:
address InterpreterGenerator::generate_normal_entry(bool synchronized) { // ... // entry_point函数的代码入口地址 address entry_point = __ pc(); // 当前rbx中存储的是指向Method的指针,通过Method*找到ConstMethod* const Address constMethod(rbx, Method::const_offset()); // 通过Method*找到AccessFlags const Address access_flags(rbx, Method::access_flags_offset()); // 通过ConstMethod*得到parameter的大小 const Address size_of_parameters(rdx,ConstMethod::size_of_parameters_offset()); // 通过ConstMethod*得到local变量的大小 const Address size_of_locals(rdx, ConstMethod::size_of_locals_offset()); // 上面已经说明了获取各种方法元数据的计算方式, // 但并没有执行计算,下面会生成对应的汇编来执行计算 // 计算ConstMethod*,保存在rdx里面 __ movptr(rdx, constMethod); // 计算parameter大小,保存在rcx里面 __ load_unsigned_short(rcx, size_of_parameters); // rbx:保存基址;rcx:保存循环变量;rdx:保存目标地址;rax:保存返回地址(下面用到) // 此时的各个寄存器中的值如下: // rbx: Method* // rcx: size of parameters // r13: sender_sp (could differ from sp+wordSize
// if we were called via c2i ) 即调用者的栈顶地址 // 计算local变量的大小,保存到rdx __ load_unsigned_short(rdx, size_of_locals); // 由于局部变量表用来存储传入的参数和被调用方法的局部变量, // 所以rdx减去rcx后就是被调用方法的局部变量可使用的大小 __ subl(rdx, rcx); // ... // 返回地址是在CallStub中保存的,如果不弹出堆栈到rax,中间 // 会有个return address使的局部变量表不是连续的, // 这会导致其中的局部变量计算方式不一致,所以暂时将返 // 回地址存储到rax中 __ pop(rax); // 计算第1个参数的地址:当前栈顶地址 + 变量大小 * 8 - 一个字大小 // 注意,因为地址保存在低地址上,而堆栈是向低地址扩展的,所以只 // 需加n-1个变量大小就可以得到第1个参数的地址 __ lea(r14, Address(rsp, rcx, Address::times_8, -wordSize)); // 把函数的局部变量设置为0,也就是做初始化,防止之前遗留下的值影响 // rdx:被调用方法的局部变量可使用的大小 { Label exit, loop; __ testl(rdx, rdx);
// 如果rdx<=0,不做任何操作 __ jcc(Assembler::lessEqual, exit); __ bind(loop);
// 初始化局部变量 __ push((int) NULL_WORD); __ decrementl(rdx); __ jcc(Assembler::greater, loop); __ bind(exit); } // 生成固定桢 generate_fixed_frame(false); // ... 省略统计及栈溢出等逻辑,后面会详细介绍 // 如果是同步方法时,还需要执行lock_method()函数,所以 // 会影响到栈帧布局 if (synchronized) { // Allocate monitor and lock method lock_method(); } // 跳转到目标Java方法的第一条字节码指令,并执行其对应的机器指令 __ dispatch_next(vtos); // ... 省略统计相关逻辑,后面会详细介绍 return entry_point; }
这个函数的实现看起来比较多,但其实逻辑实现比较简单,就是根据被调用方法的实际情况创建出对应的局部变量表,然后就是2个非常重要的函数generate_fixed_frame()和dispatch_next()函数了,这2个函数我们后面再详细介绍。
在调用generate_fixed_frame()函数之前,栈的状态变为了下图所示的状态。
与前一个图对比一下,可以看到多了一些local variable 1 ... local variable n等slot,这些slot与argument word 1 ... argument word n共同构成了被调用的Java方法的局部变量表,也就是图中紫色的部分。其实local variable 1 ... local variable n等slot属于被调用的Java方法栈帧的一部分,而argument word 1 ... argument word n却属于CallStub()函数栈帧的一部分,这2部分共同构成局部变量表,专业术语叫栈帧重叠。
另外还能看出来,%r14指向了局部变量表的第1个参数,而CallStub()函数的return address被保存到了%rax中,另外%rbx中依然存储着Method*。这些寄存器中保存的值将在调用generate_fixed_frame()函数时用到,所以我们需要在这里强调一下。
推荐阅读:
第2篇-JVM虚拟机这样来调用Java主类的main()方法
如果有问题可直接评论留言或加作者微信mazhimazh
关注公众号,有HotSpot源码剖析系列文章!