[Inside HotSpot] Java的方法调用
1. 方法调用模块入口
Java所有的方法调用都会经过JavaCalls模块。该模块又细分为call_virtual调用虚函数,call_static调用静态函数等。虚函数调用会根据对象类型进行方法决议,所以需要获取对象引用再查找实际要调用的方法;而静态方法调用直接查找要调用的方法即可。不管怎样,这些方法都是先找到要调用的方法methodHandle,然后传给JavaCalls::call_helper()做实际的调用。
2. 寻找调用方法
现在我们知道了methodHandle表示实际要调用的方法,methodHandle里面有一个指向当前线程的指针,还有一个指向Method
类的指针,Method
位于hotspot\share\oops\method.hpp
,各种各样的数据比如方法的访问标志,内联标志,用于编译优化的计数等都落地于此。它的每个属性的意义都是肉眼可见的重要:
_constMethod
指向方法中一些常量数据,比如常量池,max_local,max_stack,返回类型,参数个数,编译-解释适配器...这些参数的重要性不言而喻。
_method_data
存放一些计数信息和Profiling信息,比如方法重编译了多少次,非逃逸参数有多少个,回边有多少,有多少循环和基本块。这些参数会影响后面的编译器优化。
_method_counters
大量编译优化相关的计数:
- 解释器调用次数
- 解释执行时由于异常而终止的次数
- 方法调用次数(method里面有多少方法调用)
- 回边个数
- 该方法曾经过的分层编译的最高层级
- 热点方法计数
_access_flag
flag | 值 | 说明 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否为public |
ACC_PRIVATE | 0x0002 | 方法是否为private |
ACC_PROTECTED | 0x0004 | 方法是否为protected |
ACC_STATIC | 0x0008 | 方法是否为static |
ACC_FINAL | 0x0010 | 方法是否不可重写 |
ACC_SYNCHRONIZED | 0x0020 | 是否存在方法锁 |
ACC_BRIDGE | 0x0040 | 该方法是否由编译器生成 |
ACC_VARARGS | 0x0080 | 是否存在可变参数 |
ACC_NATIVE | 0x0100 | 是否为native方法 |
ACC_ABSTRACT | 0x0400 | 是否为抽象方法 |
ACC_STRICT | 0x0800 | 是否启用严格浮点模式 |
ACC_SYNTHETIC | 0x1000 | 是否是源代码里面不存在的合成方法 |
_vtable_index
flag | 值 | 说明 |
---|---|---|
itable_index_max | -10 | 首个itable索引 |
pending_itable_index | -9 | itable将会被赋值 |
invalid_vtable_index | -4 | 无效虚表index |
garbage_vtable_index | -3 | 还没有初始化vtable的方法,垃圾值 |
nonvirtual_vtable_index | -2 | 不需要虚函数派发,比如static函数就是这种 |
_flags
这个_flag不同于前面的_access_flag,它是表示这个方法具有什么特征,比如是否强制内联,是否有@CallerSentitive注解,是否是有@HotSpotIntrinsicCandidate注解等
_intrinsic_id
固有方法(intrinsic method)在虚拟机中表示一些众所周知的方法,针对它们可以做特设处理,生成独特的代码例程,虚拟机发现一个方法是固有方法就不会走逐行解释字节码这条路径而是跳到独特的代码例程上面,所有的固有方法都定义在hotspot\share\classfile\vmSymbols.hpp
中,有兴趣的可以去看看。
_compiled_invocation_count
编译后的方法叫nmethod,这个就是用来计数编译后的nmethod调用了多少次,如果该方法是解释执行就为0。
_code
指向编译后的本地代码。
_from_interpreter_entry
解释器入口,这个非常重要。之前提到JavaCalls::call得到methodHandle传给call_helper做实际调用,call_helper会使用这个入口进入解释器的世界。
_from_compiled_entry
如果该方法已经经过了编译,那么就会使用该入口执行编译后的代码。
虚拟机是解释编译混合执行的模型,一个方法可能A时刻是解释模式,B时刻是编译模式,这就要求两个入口都能进入正确的地方。hotspot使用一个适配器完成解释编译模式的切换:
之所以要加一个适配器是因为编译产出的本地代码用寄存器存放参数,解释器用栈存放参数,适配器可以消除这些不同,同时正确设置入口点。
3. 建立栈帧
前面说道找到methodHandle后传给call_helper做调用。其实,严格来说,call_helper还没有做方法调用,它只是检查了下方法是否需要进行编译,验证了参数等等,最终它是调用函数指针_call_stub_entry
,把方法调用这件事又转交给了_call_stub_entry。
// hotspot\share\runtime\javaCalls.cpp
void JavaCalls::call_helper(JavaValue* result, const methodHandle& method, JavaCallArguments* args, TRAPS) {
...
// 调用函数指针_call_stub_entry,把实际的函数调用工作转交给它。
{ JavaCallWrapper link(method, receiver, result, CHECK);
{ HandleMark hm(thread); // HandleMark used by HandleMarkCleaner
StubRoutines::call_stub()(
(address)&link,
result_val_address,
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
);
result = link.result();
if (oop_result_flag) {
thread->set_vm_result((oop) result->get_jobject());
}
}
}
}
_call_stub_entry由generate_call_stub()生成,当调用Java方法前需要建立栈帧,该栈帧就是于此建立的。
另外StubRoutines::call_stub()()
是将_call_stub_entry强制类型转换为指针然后执行,调试的时候不能对应源码。如果使用Microsoft Visual Studio系列编译器,点击菜单栏调试->窗口->反汇编
:
然后在反汇编窗口STEP INTO进入call
:
在右方可以看到generate_call_stub()生成的机器码(的汇编表示)了。由于generate_call_stub太多,这里就不逐行对照,请自行对应源码和反汇编窗口的输出,generate_call_stub里面是用汇编形式写的机器码生成,全部贴出来既无必要也没意思,所以用注释代替了,只保留最重要的逻辑:
// hotspot\cpu\x86\stubGenerator_x86_32.cpp
address generate_call_stub(address& return_address) {
// 保存重要的参数比如解释器入口点,Java方法返回地址等
// 将Java方法的参数压入栈
// 调用Java方法
__ movptr(rbx, method); // 将Method*指针存放到rbx
__ movptr(rax, entry_point); // 将解释器入口存放到rax
__ mov(rsi, rsp); // 将当前栈顶存放到rsi
__ call(rax); // 进入解释器入口!
// 处理Java方法返回值
// 弹出Java参数
// 返回
return start;
}
它首先建立了一个栈帧,这个栈帧里面保存了一些重要的数据,再把Java方法的参数压入栈,当这一步完成,栈帧变成了这个样子:
4. Java方法调用
当栈帧建立完毕就可以调用Java方法了。重复一次,Java方法调用使用如下代码:
// 调用Java方法
__ movptr(rbx, method); // 将Method*指针存放到rbx
__ movptr(rax, entry_point); // 将解释器入口存放到rax
__ mov(rsi, rsp); // 将当前栈顶存放到rsi
__ call(rax); // 进入解释器入口!
前面三句将重要的数据放入寄存器,然后call rax
相当于call entry_point
,这个entry_point即解释器入口点,最终的方法执行过程其实是在这里面的,_call_stub_entry只是一个桩代码(Stub code),创建了栈帧,处理调用返回,实际的调用还是要跳到解释器里面的。
桩代码的意义有很多,常见的就是它是一个符合要求的签名的函数,但是函数现在还没有完全实现,那就留一个桩占位。比如一个系统需要读取外部温度:
void work(){
float temperature = readTemperatureFromSensor();
if(temperature>40.0){
...
}
}
float readTemperatureFromSensor(){
return 42.0f;
}
这个读温度的函数比较复杂,涉及传感器的硬件编程,现阶段我们只想完成外部即work的逻辑,那么就将readTemperatureFromSensor()做为一个stub,写一个假的实现,后面再补全。
回到主题,虚拟机_call_stub_entry桩代码的意思是它不完成具体任务(方法调用),只是做一些辅助工作(建立栈帧),而是跳到(call rax)解释器入口完成具体任务,虚拟机中还有很多这样的模式,其它叫法还有trampoline(跳床),以后都会遇到。
5. 总结
学而不思则罔,思而不学则殆。我们大概清楚了Java方法调用的流程,现在可以试着来总结一下:
JavaCalls里面的call_static()
或者call_virtual
通过方法决议找到要调用的方法methodHandle,传递给JavaCalls::call();JavaCalls::call()做一些简单的检查,比如方法是否需要进行C1/C2 JIT,参数对不对,之后调用_call_stub_entry,它会建立栈帧,进入解释器执行字节码,最后从解释器返回,处理返回值,完成方法调用。详细的调用栈如下:
JavaCalls::call_static() // 找到要调用的方法
-> JavaCalls::call()
-> os::os_exception_wrapper()
-> JavaCalls::call_helper()
-> _call_stub_entry() // 建立栈帧,处理解释器返回值
-> `call rbx` // 进入解释器入口点
附录1. 使用hsdis查看对应的汇编表示
如果觉得上述调试方法过于麻烦,还有备选方案。下载hsdis-amd64.dll,将它放在jdk/bin/server/
目录下,然后虚拟机加上参数-XX:+UnlockDiagnosticVMOptions -XX:+PrintStubCode
还可以查看上面的生成的机器代码的汇编形式,不过除了验证比对外一般人也很难从这大段汇编中看出什么...:
StubRoutines::call_stub [0x000001a53eb80b0c, 0x000001a53eb80efe[ (1010 bytes)
0x000001a53eb80b0c: push %rbp
0x000001a53eb80b0d: mov %rsp,%rbp
0x000001a53eb80b10: sub $0x1d8,%rsp
0x000001a53eb80b17: mov %r9,0x28(%rbp)
0x000001a53eb80b1b: mov %r8d,0x20(%rbp)
0x000001a53eb80b1f: mov %rdx,0x18(%rbp)
0x000001a53eb80b23: mov %rcx,0x10(%rbp)
0x000001a53eb80b27: mov %rbx,-0x8(%rbp)
0x000001a53eb80b2b: mov %r12,-0x20(%rbp)
0x000001a53eb80b2f: mov %r13,-0x28(%rbp)
0x000001a53eb80b33: mov %r14,-0x30(%rbp)
0x000001a53eb80b37: mov %r15,-0x38(%rbp)
0x000001a53eb80b3b: vmovdqu %xmm6,-0x48(%rbp)
0x000001a53eb80b40: vmovdqu %xmm7,-0x58(%rbp)
0x000001a53eb80b45: vmovdqu %xmm8,-0x68(%rbp)
0x000001a53eb80b4a: vmovdqu %xmm9,-0x78(%rbp)
0x000001a53eb80b4f: vmovdqu %xmm10,-0x88(%rbp)
0x000001a53eb80b57: vmovdqu %xmm11,-0x98(%rbp)
0x000001a53eb80b5f: vmovdqu %xmm12,-0xa8(%rbp)
0x000001a53eb80b67: vmovdqu %xmm13,-0xb8(%rbp)
0x000001a53eb80b6f: vmovdqu %xmm14,-0xc8(%rbp)
0x000001a53eb80b77: vmovdqu %xmm15,-0xd8(%rbp)
; 省略500+行
附录2. 解释器入口点
意犹未尽吗?上面省略了很多东西,比如进入解释器入口点执行字节码这个重要的事情。那么解释器入口点在哪?我们知道解释器是在虚拟机创建的时候JIT生成的,可以跟踪虚拟机创建找到它,它的调用栈如下:
Threads::create_vm()
-> init_globals()
-> interpreter_init()()
-> TemplateInterpreter::initialize()
-> TemplateInterpreterGenerator() // 构造函数
-> TemplateInterpreterGenerator::generate_all()
-> TemplateInterpreterGenerator::generate_normal_entry()
普通方法(非synchronized,非native)的解释器入口点是通过\hotspot\cpu\x86\templateInterpreterGenerator_x86.cpp
中的generate_normal_entry()生成的。
附录3. 设置解释器入口点
还是这个问题,我们知道了解释器入口点在哪,但是这个解释器入口点又是怎么和方法关联起来的呢?
Java的类在虚拟机中会经过加载 -> 链接 -> 初始化 三个步骤,网上有很多详细解释这里就不在赘述。具体来说instanceKlass
在虚拟机中表示一个Java类,它使用instanceKlass::link_class()
做链接过程。类的链接会触发类中方法的Method::link_method()
,它会给方法设置正确的解释器入口点,编译器适配器等:
// hotspot\share\oops\method.cpp
void Method::link_method(const methodHandle& h_method, TRAPS) {
...
if (!is_shared()) {
// entry_for_method会找到刚刚generate_normal_entry设置的入口点
address entry = Interpreter::entry_for_method(h_method);
// 将它设置为解释器入口点,即可_i2i_entry和_from_interpreted_entry
set_interpreter_entry(entry);
}
...
// 设置_from_compiled_entry的适配器
(void) make_adapters(h_method, CHECK);
}
[Inside HotSpot] Java的方法调用的更多相关文章
- [Inside HotSpot] Java分代堆
[Inside HotSpot] Java分代堆 1. 宇宙初始化 JVM在启动的时候会初始化各种结构,比如模板解释器,类加载器,当然也包括这篇文章的主题,Java堆.在hotspot源码结构中gc/ ...
- [转]Java远程方法调用
Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口.它使客户机上运行的程序可以调用远 ...
- java中方法调用
JAVA中方法的调用[基础] 一.调用本类中的方法 方法一.被调用方法声明为static ,可以在其他方法中直接调用.示例代码如下: public class HelloWord { /** * @p ...
- Java 反射 方法调用
在使用Java 反射时,对方法的调用,可能碰到最多的问题是,方法的变量如何使用.其实,调用方法的变量全部在参数数组里,不管有多少个参数,你都要把它放在参数数组里,如果是单个非数组参数,则可不使用参数数 ...
- java中方法调用在内存中的体现
在java中,方法以及局部变量(即在方法中声明的变量)是放在栈内存上的.当你调用一个方法时,该方法会放在调用栈的栈顶.栈顶的方法是目前正在执行的方法,直到执行完毕才会从栈顶释放.我们知道,栈是一种执行 ...
- java高级用法之:无所不能的java,本地方法调用实况
目录 简介 JDK的本地方法 自定义native方法 总结 简介 相信每个程序员都有一个成为C++大师的梦想,毕竟C++程序员处于程序员鄙视链的顶端,他可以俯视任何其他语言的程序员. 但事实情况是,无 ...
- java 分析方法调用过程
StackTraceElement[] s = new Exception().getStackTrace(); for(int i=0;i<s.length;i++) System.out.p ...
- java native:Java本地方法调用(jni方式)
https://www.cnblogs.com/zh1164/p/6283831.html
- JVM方法调用过程
JVM方法调用过程 重载和重写 同一个类中,如果出现多个名称相同,并且参数类型相同的方法,将无法通过编译.因此,想要在同一个类中定义名字相同的方法,那么它们的参数类型必须不同.这种方法上的联系就是重载 ...
随机推荐
- Nginx+Tomcat搭建高性能负载均衡集群
一. 工具 nginx-1.8.0 apache-tomcat-6.0.33 二. 目标 实现高性能负载均衡的Tomcat集群: 三. 步骤 1.首先下载Nginx ...
- PHP与XML技术
XML的概述 XML(eXtensibleMarkup Language),扩展性标记语言,它是用来描述其他语言的语言.它允许用户设计自己的标记.XML是由W3C(WorldWide 月发布的一种标准 ...
- HSLA色相饱和透明度
H:Hue(色调),取值为:0 - 360.将色相值想成一个圆环中的度数,随着在圆环上移动,得到不同的颜色. S:Saturation(饱和度),取值为:0.0% - 100.0%.数值越低(降低饱和 ...
- 【爬虫】Xpath高级用法
xpath速度比较快,是爬虫在网页定位中的较优选择,但是很多网页前端代码混乱难以定位,而学习定位也较为不易(主要是全面的教程较少),这里列出一点编程过程中可能有用的东西,欢迎共同学习批评指正.试验环境 ...
- IntelliJ IDEA(十) :常用操作
IDEA功能详细,快捷键繁多,但是实际开发时不是所有都能用上,如果我们熟悉一些常用的也足够满足我们日常开发了,多的也只是提高我们的B格. 1.自定义主题 IDEA默认的主题有三款,分别是Intelli ...
- JAVA经典算法40题(原题+分析)之分析
JAVA经典算法40题(下) [程序1] 有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? 1.程序分析: ...
- Django rest_framework快速入门
一.什么是REST 面向资源是REST最明显的特征,资源是一种看待服务器的方式,将服务器看作是由很多离散的资源组成.每个资源是服务器上一个可命名的抽象概念.因为资源是一个抽象的概念,所以它不仅仅能代表 ...
- CSRF Token介绍与应对策略
原文地址:点击打开链接 最近模拟登陆,发现CsrfToken是个很麻烦的问题,所以看了一下CsrfToken的一些介绍.发现这篇文章写得很不错,所以转载过来. CSRF 背景与介绍 CSRF(Cros ...
- 数字类型——python3
今天我为各位小伙伴准备了python3中数字类型,希望能够帮助到你们! Python 数字数据类型用于存储数值. 数据类型是不允许改变的,这就意味着如果改变数字数据类型的值,将重新分配内存空间. 以下 ...
- c语言最大公约数及最小公倍数的详解
今天我打算把,学习到的一些知识整理一下,方便给以后的学弟学妹做一个参考! 这一次是关于最大公约数和最小公倍数的知识:这是百度关于最大公约数的介绍 感谢我的一位学姐的博文,让我能够更快的明白! 求最小公 ...