在 第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()函数时用到,所以我们需要在这里强调一下。

推荐阅读:

第1篇-关于JVM运行时,开篇说的简单些

第2篇-JVM虚拟机这样来调用Java主类的main()方法

第3篇-CallStub新栈帧的创建

第4篇-JVM终于开始调用Java主类的main()方法啦

第5篇-调用Java方法后弹出栈帧及处理返回结果

如果有问题可直接评论留言或加作者微信mazhimazh

关注公众号,有HotSpot源码剖析系列文章!

  

第6篇-Java方法新栈帧的创建的更多相关文章

  1. 第3篇-CallStub新栈帧的创建

    在前一篇文章 第2篇-JVM虚拟机这样来调用Java主类的main()方法  中我们介绍了在call_helper()函数中通过函数指针的方式调用了一个函数,如下: StubRoutines::cal ...

  2. Java虚拟机之栈帧

    写在前面的话:Java虚拟机是一门学问,是众多Java大神们的杰作,由于我个人水平有限,精力有限,不能保证所有的东西都是正确的,这里内容都是经过深思熟虑的,部分引用原著的内容,讲的已经很好了,不在累述 ...

  3. 详细解析Java虚拟机的栈帧结构

    欢迎关注微信公众号:万猫学社,每周一分享Java技术干货. 什么是栈帧? 正如大家所了解的,Java虚拟机的内存区域被划分为程序计数器.虚拟机栈.本地方法栈.堆和方法区.(什么?你还不知道,赶紧去看看 ...

  4. C语言的函数调用过程(栈帧的创建与销毁)

    从汇编的角度解析函数调用过程 看看下面这个简单函数的调用过程: int Add(int x,int y) { ; sum = x + y; return sum; } int main () { ; ...

  5. 第7篇-为Java方法创建栈帧

    在 第6篇-Java方法新栈帧的创建 介绍过局部变量表的创建,创建完成后的栈帧状态如下图所示. 各个寄存器的状态如下所示. // %rax寄存器中存储的是返回地址 rax: return addres ...

  6. 第30篇-main()方法的执行

    在第7篇详细介绍过为Java方法创建的栈帧,如下图所示. 调用完generate_fixed_frame()函数后一些寄存器中保存的值如下: rbx:Method* ecx:invocation co ...

  7. 第5篇-调用Java方法后弹出栈帧及处理返回结果

    在前一篇 第4篇-JVM终于开始调用Java主类的main()方法啦 介绍了通过callq调用entry point,不过我们并没有看完generate_call_stub()函数的实现.接下来在ge ...

  8. 第48篇-native方法调用解释执行的Java方法

    举一个native方法调用解释执行的Java方法的实例,如下: public class TestJNI { static { System.load("/media/mazhi/sourc ...

  9. generate_fixed_frame()方法生成Java方法栈帧

    在从generate_normal_entry()函数调用generate_fixed_frame()函数时的栈与寄存器的状态如下: 栈的状态如下图所示. 各个寄存器的状态如下所示. rax: ret ...

随机推荐

  1. keycloak~管理平台的查询bug与自定rest中文检索

    对于keycloak来说,它的管理平台在它的源码中的admin-client中,它会定义相关的rest接口规范:在我们使用keycloak管理平台时,其中有一个组的查询,在我们查询中文组时,它是不支持 ...

  2. 使用Linux Deploy将闲置的安卓手机改造简易服务器

    本文将介绍我在自己闲置的小米4手机安装CentOS系统的过程.手机配置信息:MIUI 9开发板(方便ROOT).Android 6.架构 ARMv7(arm32) 准备工作 1.手机必须ROOT!!! ...

  3. 暑假自学java第一天

    今天通过网上的学习资料安装了Java的环境和java的程序开发工具包(JDK) 还安装了eclipse ,英语不太好,所以不太会用这个软件,网上搜了教程,还是出现了问题:unnamed package ...

  4. 前端 | Vue 路由返回恢复页面状态

    需求场景:首页搜索内容,点击跳转至详情页,页面后退返回主页,保留搜索结果. 方案:路由参数:路由守卫 需求描述 在使用 Vue 开发前端的时候遇到一个场景:在首页进行一些数据搜索,点击搜索结果进入详情 ...

  5. Vector ArrayList LinkedList

    三者都实现了List接口! Vector与ArrayList:采用顺序存储的方式,但是Vector是线程安全的,ArrayList是线程不安全的,按需使用: 当存储空间不足的时候,ArrayList默 ...

  6. 如何用Redis统计独立用户访问量

    拼多多有数亿的用户,那么对于某个网页,怎么使用Redis来统计一个网站的用户访问数呢? 使用Hash 哈希是Redis的一种基础数据结构,Redis底层维护的是一个开散列,会把不同的key映射到哈希表 ...

  7. 关于easyswoole实现websocket聊天室的步骤解析

    在去年,我们公司内部实现了一个聊天室系统,实现了一个即时在线聊天室功能,可以进行群组,私聊,发图片,文字,语音等功能,那么,这个聊天室是怎么实现的呢?后端又是怎么实现的呢? 后端框架 在后端框架上,我 ...

  8. 基于Vue/React项目的移动端适配方案

    本文的目标是通过下文介绍的适配方案,使用vue或react开发移动端及H5的时候,不需要再关心移动设备的大小,只需要按照固定设计稿的px值布局,提升开发效率. 下文给出了本人分别使用create-re ...

  9. mongodb的基本命令与常规操作

    1. 查看当前数据库的版本号:db.version()2. 查看当前所在数据库:db 默认是test数据库3. 查看当前数据库的连接地址:db.getMongo()4. 查看所有数据库:show da ...

  10. FreeRTOS消息队列

    FreeRTOS 的一个重要的通信机制----消息队列,消息队列在实际项目中应用较多. 一.消息队列的作用及概念: 消息队列就是通过 RTOS 内核提供的服务,任务或中断服务子程序可以将一个消息(注意 ...