第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 ... 
随机推荐
- keycloak~管理平台的查询bug与自定rest中文检索
			对于keycloak来说,它的管理平台在它的源码中的admin-client中,它会定义相关的rest接口规范:在我们使用keycloak管理平台时,其中有一个组的查询,在我们查询中文组时,它是不支持 ... 
- 使用Linux Deploy将闲置的安卓手机改造简易服务器
			本文将介绍我在自己闲置的小米4手机安装CentOS系统的过程.手机配置信息:MIUI 9开发板(方便ROOT).Android 6.架构 ARMv7(arm32) 准备工作 1.手机必须ROOT!!! ... 
- 暑假自学java第一天
			今天通过网上的学习资料安装了Java的环境和java的程序开发工具包(JDK) 还安装了eclipse ,英语不太好,所以不太会用这个软件,网上搜了教程,还是出现了问题:unnamed package ... 
- 前端 | Vue 路由返回恢复页面状态
			需求场景:首页搜索内容,点击跳转至详情页,页面后退返回主页,保留搜索结果. 方案:路由参数:路由守卫 需求描述 在使用 Vue 开发前端的时候遇到一个场景:在首页进行一些数据搜索,点击搜索结果进入详情 ... 
- Vector ArrayList LinkedList
			三者都实现了List接口! Vector与ArrayList:采用顺序存储的方式,但是Vector是线程安全的,ArrayList是线程不安全的,按需使用: 当存储空间不足的时候,ArrayList默 ... 
- 如何用Redis统计独立用户访问量
			拼多多有数亿的用户,那么对于某个网页,怎么使用Redis来统计一个网站的用户访问数呢? 使用Hash 哈希是Redis的一种基础数据结构,Redis底层维护的是一个开散列,会把不同的key映射到哈希表 ... 
- 关于easyswoole实现websocket聊天室的步骤解析
			在去年,我们公司内部实现了一个聊天室系统,实现了一个即时在线聊天室功能,可以进行群组,私聊,发图片,文字,语音等功能,那么,这个聊天室是怎么实现的呢?后端又是怎么实现的呢? 后端框架 在后端框架上,我 ... 
- 基于Vue/React项目的移动端适配方案
			本文的目标是通过下文介绍的适配方案,使用vue或react开发移动端及H5的时候,不需要再关心移动设备的大小,只需要按照固定设计稿的px值布局,提升开发效率. 下文给出了本人分别使用create-re ... 
- mongodb的基本命令与常规操作
			1. 查看当前数据库的版本号:db.version()2. 查看当前所在数据库:db 默认是test数据库3. 查看当前数据库的连接地址:db.getMongo()4. 查看所有数据库:show da ... 
- FreeRTOS消息队列
			FreeRTOS 的一个重要的通信机制----消息队列,消息队列在实际项目中应用较多. 一.消息队列的作用及概念: 消息队列就是通过 RTOS 内核提供的服务,任务或中断服务子程序可以将一个消息(注意 ... 
