方法作为程序的基本单元,作为原子指令的初步封装,计算机必须支持方法的调用。同样,Java语言的原子指令是字节码,Java方法是对字节码的封装,因此jvm必须支持对Java方法的调用。
JVM最后实际调用的并不是Java函数,而是对应的机器指令。
以汇编演示一个求和程序:
先是main函数
main:
//保存调用者栈基地址,并为main函数分配新栈空间
pushl %ebp
movl %esp,%ebp
sub $32,%esp //分配新栈,给main函数分配32字节的空间 (新栈栈顶地址减32(地址从高位向地位方向分配))
//初始化2个数据,一个是5,一个是3
movl $5,20(%esp) //放到操作数栈中
movl $3,24(%esp) //放到操作数栈中
//压栈
movl 24(%esp),%eax //从操作数栈中取出到寄存器中
movl %eax,4(%esp) //把寄存器中的数存到局部变量表中
movl 20(%esp),%eax
movl %eax,(%esp)
//调用add方法
call add
movl %eax,28(%esp)
//返回
movl $0,%eax
leave
ret
主要有以下几步:
add函数
add:
//保存调用者栈基,并分配新栈
pushl %ebp
movl %esp,%ebp
sub $16,%esp //给add函数分配16个字节的空间
//获取入参
movl12(%ebp), %eax (当前方法栈上移12字节的位置)
movl8(%ebp), %edx (之所以是8,是因为main和add方法栈中间8个字节,
其中4个字节用来保存调用方(这里就是main函数)下一条指令的地址,以及调用方(这里指main函数)的栈起始地址)
//执行运算
addl%edx,%eax //把两个寄存器中的值求和,存到后一个寄存器
mov%eax,-4(%ebp) //把结果入栈
//返回
movl-4(%ebp),%eax
leave
ret
主要有以下几步:
给函数分配堆栈,有一个约定,就是分配的空间必须是16的倍数,也就是内存对齐。多了不退,少了要补齐。
再看看C程序
int add(int a,int b);
int main(){
int a=5;
int b=3;
int c=add(a,b);
return 0;
}
int add(int a,int b){
int z=1+2;
return z;
}
main:
pushl %ebp
movl%esp,%ebp
add$-16,%esp
sub$32.%esp
movl$5,20(%esp)
movl$3,24(%esp)
movl24(%esp),%eax
movl%eax,4(%ebp)
movl20(%esp),%eax
movl%eax,(%esp)
calladd
movl%eax,28(%esp) //从eax寄存器取被调用方法的返回结果
movl$0,%eax
leave
ret
add:
pushl %ebp
movl%esp,%ebp
subl$16,%esp
movl$3,-4(%ebp)
movl-4(ebp),%eax
leave
ret
在真实的物理机器上,执行函数调用主要包含以下几个步骤:
前面演示了物理机器的函数调用机制和C语言函数的调用机制,下面讲述jvm的调用。
java源码:
package test;
public class Test {
public static void main(String[] args) {
add(5,8);
}
public static int add(int a, int b) {
int c=a+b;
int d=c+9;
return d;
}
}
对应字节码:
Classfile /D:/项目/study/out/production/study/test/Test.class
Last modified 2018-9-19; size 525 bytes
MD5 checksum 73412e99dd6757092306da214d3a9207
Compiled from "Test.java"
public class test.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#25 // java/lang/Object."":()V
#2 = Methodref #3.#26 // test/Test.add:(II)I
#3 = Class #27 // test/Test
#4 = Class #28 // java/lang/Object
#5 = Utf8
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 LocalVariableTable
#10 = Utf8 this
#11 = Utf8 Ltest/Test;
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 args
#15 = Utf8 [Ljava/lang/String;
#16 = Utf8 add
#17 = Utf8 (II)I
#18 = Utf8 a
#19 = Utf8 I
#20 = Utf8 b
#21 = Utf8 c
#22 = Utf8 d
#23 = Utf8 SourceFile
#24 = Utf8 Test.java
#25 = NameAndType #5:#6 // "":()V
#26 = NameAndType #16:#17 // add:(II)I
#27 = Utf8 test/Test
#28 = Utf8 java/lang/Object
{
public test.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ltest/Test;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: iconst_5
1: bipush 8
3: invokestatic #2 // Method add:(II)I
6: pop
7: return
LineNumberTable:
line 8: 0
line 9: 7
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 args [Ljava/lang/String;
public static int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=2
0: iload_0 //常量0入栈
1: iload_1 //常量1入栈
2: iadd //对上面2个数求和,并将结果入栈
3: istore_2 //将栈顶的值(也就是求和的结果)存到局部变量表2(从0开始计数,2的位置是c的)的位置,
4: iload_2 //把c的值加载到操作数栈
5: bipush 9 //把常量9入栈
7: iadd //求和
8: istore_3 //赋值给d
9: iload_3 //加载d到栈顶
10: ireturn //将栈顶值返回
LineNumberTable:
line 12: 0
line 13: 4
line 14: 9
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 a I
0 11 1 b I
4 7 2 c I
9 2 3 d I
}
SourceFile: "Test.java"
上面的方法的字节码指令,最终会变成机器指令。而C语言调用机器指令是通过函数指针实现的。
jvm内部有一个函数指针–call_stub,这个函数指针是JVM内部C语言程序和机器指令的分水岭。
call_stub函数有8个参数
参数名 | 含义 |
---|---|
link | 连接器 |
result_val_address | 函数返回值地址 |
result_type | 函数返回类型 |
method() | jvm内部表示的Java方法对象 |
entry_point | jvm调用Java函数的例程入口。jvm内部每一段历程都是在jvm启动过程中预先生成好的机器指令。要调用Java方法,必须经过本历程,即先执行这段机器指令,才能跳转到Java方法字节码所对应的机器指令去执行。 |
parameters | Java方法的入参集合 |
size_of_parameters | java方法的入参数量 |
Check | 当前线程对象 |
call_stub函数做了:调用者栈帧保存、堆栈动态扩展、现场保存、Java函数参数压栈这一系列逻辑处理后,调用entry_point例程。
entry_point例程完成了Java主函数栈帧的创建,找到了Java主函数对应的第一个字节码并进入执行。
在hotspot内部,存在3种解释器,分别是字节码解释器,C++解释器和模板解释器。字节码解释器逐条翻译字节码指令,使用C/C++执行字节码指令逻辑,执行效率低下。
模板解释器相比于字节码指示器,高级之处在于直接把字节码指令翻译成对应的机器指令,高效的多。JVM默认使用模板解释器。
例程名 | 作用 |
---|---|
zerolocals | 一般Java方法调用,经过该例程 |
zerolocals | 同步Java方法调用,经过 |
方法在Jvm中的内存结构
右边是constMethod的数据结构,jvm内部基于偏移量来分配堆栈空间。
对于Java方法之间的调用,调用方的操作数栈和被调用方的局部变量表存在重叠区。所以在运行期,jvm只需要为Java方法的局部变量分配空间,而不需要为入参分配空间。