第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源码剖析系列文章!

第6篇-Java方法新栈帧的创建的更多相关文章
- 第3篇-CallStub新栈帧的创建
在前一篇文章 第2篇-JVM虚拟机这样来调用Java主类的main()方法 中我们介绍了在call_helper()函数中通过函数指针的方式调用了一个函数,如下: StubRoutines::cal ...
- Java虚拟机之栈帧
写在前面的话:Java虚拟机是一门学问,是众多Java大神们的杰作,由于我个人水平有限,精力有限,不能保证所有的东西都是正确的,这里内容都是经过深思熟虑的,部分引用原著的内容,讲的已经很好了,不在累述 ...
- 详细解析Java虚拟机的栈帧结构
欢迎关注微信公众号:万猫学社,每周一分享Java技术干货. 什么是栈帧? 正如大家所了解的,Java虚拟机的内存区域被划分为程序计数器.虚拟机栈.本地方法栈.堆和方法区.(什么?你还不知道,赶紧去看看 ...
- C语言的函数调用过程(栈帧的创建与销毁)
从汇编的角度解析函数调用过程 看看下面这个简单函数的调用过程: int Add(int x,int y) { ; sum = x + y; return sum; } int main () { ; ...
- 第7篇-为Java方法创建栈帧
在 第6篇-Java方法新栈帧的创建 介绍过局部变量表的创建,创建完成后的栈帧状态如下图所示. 各个寄存器的状态如下所示. // %rax寄存器中存储的是返回地址 rax: return addres ...
- 第30篇-main()方法的执行
在第7篇详细介绍过为Java方法创建的栈帧,如下图所示. 调用完generate_fixed_frame()函数后一些寄存器中保存的值如下: rbx:Method* ecx:invocation co ...
- 第5篇-调用Java方法后弹出栈帧及处理返回结果
在前一篇 第4篇-JVM终于开始调用Java主类的main()方法啦 介绍了通过callq调用entry point,不过我们并没有看完generate_call_stub()函数的实现.接下来在ge ...
- 第48篇-native方法调用解释执行的Java方法
举一个native方法调用解释执行的Java方法的实例,如下: public class TestJNI { static { System.load("/media/mazhi/sourc ...
- generate_fixed_frame()方法生成Java方法栈帧
在从generate_normal_entry()函数调用generate_fixed_frame()函数时的栈与寄存器的状态如下: 栈的状态如下图所示. 各个寄存器的状态如下所示. rax: ret ...
随机推荐
- js 对json数据进行检索 插件 linq.js
有时界面需要很多数据.但是多次访问服务器效率很低,所以需要检索json数据,最好是像sql查询语句那种 linq.js 插件 LINQ,语言集成查询(Language Integrated Query ...
- TCP 的 Keepalive 和 HTTP 的 Keep-Alive 是一个东西吗?
大家好,我是小林. TCP 的 Keepalive 和 HTTP 的 Keep-Alive 是一个东西吗? 这是个好问题,应该有不少人都会搞混,因为这两个东西看上去太像了,很容易误以为是同一个东西. ...
- HDU 4292 Food 多源多汇入门题
Food 有F种食物和D种饮料,每种食物或饮料只能供有限次,且每个人只享用一种食物和一种饮料.现在有n个人,每个人都有自己喜欢的食物种类列表和饮料种类列表,问最多能使几个人同时享用到自己喜欢的食物和饮 ...
- Docker:docker部署redis
docker镜像库拉取镜像 # 下载镜像 docker pull redis:4.0 查看镜像 # 查看下载镜像 docker images 启动镜像 # 启动镜像 docker run --na ...
- gitolite安装及配置
1.客户端(1)使用ssh-keygen命令在当前用户根目录下生成密钥(2)将该密钥远程拷贝到gitserver服务器上:scp ~/.ssh/id_rsa.pub user@ip:/tmp/neu. ...
- php+redis实现全页缓存系统
php redis 实现全页缓存系统之前的一个项目说的一个功能,需要在后台预先存入某个页面信息放到数据库,比如app的注册协议,用户协议,这种.然后在写成一个php页面,app在调用接口的时候访问这个 ...
- charles证书导入系统信任区(Android7.0以上)
打开charles,跟着下图来,下载好charles的证书 后缀是pem的格式,挺方便的了,burp的证书是der的,还需要再进一步转化成pem,这里就不再多说, 利用openssl来计算出文件名 加 ...
- Flask(9)- 蓝图的基本使用
前言 在前面的例子中,所有的页面处理逻辑都是放在同一个文件中,随着业务代码的增加,将所有代码都放在单个程序文件中是非常不合适的 不仅会让阅读代码变得困难,而且会给后期维护带来麻烦 Flask 中使用蓝 ...
- C语言字符串处理库函数大全(转)
一.string.h中字符串处理函数 在头文件<string.h>中定义了两组字符串函数.第一组函数的名字以str开头:第二组函数的名字以mem开头. 只有函数memmove对重叠对象间的 ...
- MySQL 那些常见的错误设计规范
依托于互联网的发达,我们可以随时随地利用一些等车或坐地铁的碎片时间学习以及了解资讯.同时发达的互联网也方便人们能够快速分享自己的知识,与相同爱好和需求的朋友们一起共同讨论. 但是过于方便的分享也让知识 ...