举个小实例,如下:
public class TestJNI { static { // 程序在加载时,自动加载libdiaoyong.so库 System.loadLibrary("diaoyong"); } public static native int get(); public static void main(String[] args) { TestJNI.get(); } }
其字节码的实现如下:
Constant pool: #1 = Methodref #6.#18 // java/lang/Object."<init>":()V #2 = Methodref #5.#19 // TestJNI.get:()I #3 = String #20 // diaoyong #4 = Methodref #21.#22 // java/lang/System.loadLibrary:(Ljava/lang/String;)V #5 = Class #23 // TestJNI #6 = Class #24 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 get #12 = Utf8 ()I #13 = Utf8 main #14 = Utf8 ([Ljava/lang/String;)V #15 = Utf8 <clinit> #16 = Utf8 SourceFile #17 = Utf8 TestJNI.java #18 = NameAndType #7:#8 // "<init>":()V #19 = NameAndType #11:#12 // get:()I #20 = Utf8 diaoyong #21 = Class #25 // java/lang/System #22 = NameAndType #26:#27 // loadLibrary:(Ljava/lang/String;)V #23 = Utf8 TestJNI #24 = Utf8 java/lang/Object #25 = Utf8 java/lang/System #26 = Utf8 loadLibrary #27 = Utf8 (Ljava/lang/String;)V { // ... public static native int get(); descriptor: ()I flags: ACC_PUBLIC, ACC_STATIC, ACC_NATIVE public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: invokestatic #2 // Method get:()I 3: pop 4: return // ... }
native方法get()对应的本地函数的头文件TestJNI.h的实现如下:
#include <jni.h> #ifndef _Included_TestJNI #define _Included_TestJNI #ifdef __cplusplus extern "C" { #endif JNIEXPORT jint JNICALL Java_TestJNI_get(JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif
TestJNI.c文件的实现如下:
#include <stdio.h> #include "TestJNI.h" JNIEXPORT jint JNICALL Java_TestJNI_get(JNIEnv * env, jclass jc){ printf("ok!You have successfully passed the Java call c\n"); return 100; }
为如上的本地方法生成libdiaoyong.so动态链接库,运行后会输出如下结果:
ok!You have successfully passed the Java call c
由于native方法本质上是C/C++函数,所以不会有对应的字节码。我们在main()方法中通过invokestatic字节码指令调用native方法,在执行invokestatic字节码之前栈状态如下图所示。
下面我们来简单介绍一下解释执行的main()方法调用native方法get()的具体过程。
调用的invokestatic字节码指令的汇编如下:
0x00007fffe101c030: mov %r13,-0x38(%rbp) 0x00007fffe101c034: movzwl 0x1(%r13),%edx 0x00007fffe101c039: mov -0x28(%rbp),%rcx 0x00007fffe101c03d: shl $0x2,%edx 0x00007fffe101c040: mov 0x10(%rcx,%rdx,8),%ebx 0x00007fffe101c044: shr $0x10,%ebx 0x00007fffe101c047: and $0xff,%ebx 0x00007fffe101c04d: cmp $0xb8,%ebx // 检查invokestatic=184的bytecode是否已经连接,如果已经连接就进行跳转 0x00007fffe101c053: je 0x00007fffe101c0f2 // 调用InterpreterRuntime::resolve_invoke()函数对invokestatic=184的 // 的bytecode进行连接,因为字节码指令还没有连接 // ... 省略了解析invokestatic的汇编代码 // 将invokestatic x中的x加载到%edx中 0x00007fffe101c0e6: movzwl 0x1(%r13),%edx // 将ConstantPoolCache的首地址存储到%rcx中 0x00007fffe101c0eb: mov -0x28(%rbp),%rcx // %edx中存储的是ConstantPoolCacheEntry项的索引,转换为字偏移 0x00007fffe101c0ef: shl $0x2,%edx // 获取ConstantPoolCache::_f1属性的值 0x00007fffe101c0f2: mov 0x18(%rcx,%rdx,8),%rbx // 获取ConstantPoolCache::_flags属性的值 0x00007fffe101c0f7: mov 0x28(%rcx,%rdx,8),%edx // 从flags中获取return type,也就是从_flags的高4位保存的TosState 0x00007fffe101c0fb: shr $0x1c,%edx // 将TemplateInterpreter::invoke_return_entry地址存储到%r10 0x00007fffe101c0fe: movabs $0x7ffff73b5d00,%r10 // 找到对应return type的invoke_return_entry的地址 0x00007fffe101c108: mov (%r10,%rdx,8),%rdx // 压入返回地址,这个返回地址就是通过invokestatic指令调用的函数的返回地址 0x00007fffe101c10c: push %rdx // 设置调用者栈顶 0x00007fffe101c10d: lea 0x8(%rsp),%r13 // 向栈中last_sp的位置保存调用者栈顶 0x00007fffe101c112: mov %r13,-0x10(%rbp) // 跳转到Method::_from_interpretered_entry入口去执行 0x00007fffe101c116: jmpq *0x58(%rbx)
根据ConstantCachePoolEntry中的信息来获取返回地址TemplateInterpreter::invoke_return_entry并压入栈中,然后就会跳转到Method::_from_interpretered_entry去执行,这个Method::_from_interpretered_entry保存的就是由
InterpreterGenerator::generate_native_entry()函数生成的例程入口。此时的栈帧状态如下图所示。
这里需要提示一下,因为使用invokestatic调用的get()方法没有参数,所以在-0x8(%rsp)的位置处并没有本地变量表。我们可以举一个需要本地变量表传递参数的例子,如下:
public class TestLocalTable { public void get(int a,int b) { // ... } public static void main(String args[]) { get(1,2); } }
在test()方法中调用实例方法get(),并且传递了2个参数,生成的字节码如下:
public void test(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: aload_0 1: iconst_1 2: iconst_2 3: invokevirtual #2 // Method get:(II)V 6: return
实际上会在test()方法的表达式栈中压入3个实参,分别是接收者、常数1和常数2,而这3个参数会做为get()方法局部变量表的一部分存在,所以无论是invokevirtual还是invokestatic等字节码指令,在调用时,调用者的表达式栈中已经准备好了实参,这一部分将做为被调用者的局部变量表组成的一部分,这叫栈帧重叠,之前介绍过。
开始执行native方法的例程,如下:
// 在调用此例程时,各个寄存器中的值如下: // rbx: Method* // r13: sender sp // 将ConstMethod*存储到%rcx中 0x00007fffe1014c00: mov 0x10(%rbx),%rcx // 将参数的大小存储到%ecx中 0x00007fffe1014c04: movzwl 0x2a(%rcx),%ecx // 将返回地址弹出到%rax中 0x00007fffe1014c08: pop %rax // rbx: Method* // rcx: size of parameters 通过上面的操作,将参数的大小存储到rcx寄存器中 // r13: sender sp // 根据%rsp和参数大小计算参数的地址 // %r14指向局部变量表第一个参数的位置 // 注意,由于调用的是native方法,所以局部变量表只用来单纯传递参数, // 不用考虑本地变量,所以我们只开辟能存储参数大小的局部变量表即可 0x00007fffe1014c09: lea -0x8(%rsp,%rcx,8),%r14 // 为本地调用初始化两个8字节的数据,其中一个保存result_handler,一个保存oop temp 0x00007fffe1014c0e: pushq $0x0 // oop temp对于静态的native方法来说,保存的可能是mirror, // 或者native方法调用结果为对象时,保存这个对象 0x00007fffe1014c13: pushq $0x0
由于用来传递参数的局部变量表已经存在于栈中了,所以可通过lea -0x8(%rsp,%rcx,8),%r14汇编指令直接计算局部变量表第1个参数的地址,然后保存到%r14中。
接下来为native方法生成栈帧,如下:
0x00007fffe1014c18: push %rax 0x00007fffe1014c19: push %rbp 0x00007fffe1014c1a: mov %rsp,%rbp 0x00007fffe1014c1d: push %r13 0x00007fffe1014c1f: pushq $0x0 0x00007fffe1014c24: mov 0x10(%rbx),%r13 0x00007fffe1014c28: lea 0x30(%r13),%r13 0x00007fffe1014c2c: push %rbx 0x00007fffe1014c2d: mov 0x18(%rbx),%rdx 0x00007fffe1014c31: test %rdx,%rdx 0x00007fffe1014c34: je 0x00007fffe1014c41 0x00007fffe1014c3a: add $0x90,%rdx 0x00007fffe1014c41: push %rdx 0x00007fffe1014c42: mov 0x10(%rbx),%rdx 0x00007fffe1014c46: mov 0x8(%rdx),%rdx 0x00007fffe1014c4a: mov 0x18(%rdx),%rdx 0x00007fffe1014c4e: push %rdx 0x00007fffe1014c4f: push %r14 0x00007fffe1014c51: pushq $0x0 0x00007fffe1014c56: pushq $0x0 0x00007fffe1014c5b: mov %rsp,(%rsp)
执行完如上汇编后的栈帧状态如下图所示。
接着开辟传参空间,这个空间将会存放native方法对应的本地函数需要的参数,如下:
// 从栈帧中取出Method*存储到%rbx中 0x00007fffe1014d87: mov -0x18(%rbp),%rbx // 获取ConstMethod*存储到%r11中 0x00007fffe1014d8b: mov 0x10(%rbx),%r11 // 将方法参数的大小放到%r11d中 0x00007fffe1014d8f: movzwl 0x2a(%r11),%r11d // 将%r11d中的内容左移3位,也就是算出方法参数需要占用的字节数 0x00007fffe1014d94: shl $0x3,%r11d // 更新%rsp的值,为方法参数开辟存储参数的空间 0x00007fffe1014d98: sub %r11,%rsp // 对linux系统来说不起作用 0x00007fffe1014d9b: sub $0x0,%rsp // 必须是16字节边界(see amd64 ABI) 0x00007fffe1014d9f: and $0xfffffffffffffff0,%rsp
本地函数Java_TestJNI_get()虽然需要JNIEnv*和jclass参数,但是这2个参数是通过寄存器传递的,所以本实例不需要开辟任何传参空间。
我们能够看到,一个解释执行的Java方法调用native方法时,需要有局部变量表来给native方法传递参数,然后在调用native方法对应的本地函数时,还需要开辟另外一个传参空间。现在局部变量表已经有值,而新开辟的空间还没有设置对应的值,接着就是调用signature_handler来根据局部变量表中存储的值设置新开辟空间中各个slot的值了。之所以这样做,就是因为解释执行的调用约定和本地函数的调用约定不同,也就是传参的约定不同。
接下来是执行signature_handler,如下:
// 调用Method::signature_handler函数 0x00007fffe1014e40: callq *%r11 // 重新获取Method 0x00007fffe1014e43: mov -0x18(%rbp),%rbx // 将%rax中的result_handler存储到方法栈帧中,result_handler // 是执行signature_handler例程后的返回值,根据方法签名的返回类型获取的 0x00007fffe1014e47: mov %rax,0x18(%rbp)
Method实例的第2个附加slot的signature_handler指向的例程用来消除Java解释器栈和C/C++栈调用约定的不同,将位于解析器栈中的参数适配到本地函数使用的C栈。生成的signature_handler与result_handler的例程如下:
argument handler #56 for: static TestJNI.get()I (fingerprint = 341, 11 bytes generated) // 将result_handler的地址存储到%rax中 0x00007f98e911c85d: movabs $0x7f98e900f1f6,%rax 0x00007f98e911c867: retq --- associated result handler --- 0x00007f98e900f1f9: retq
result handler的实现非常简单,因为本地方法根据调用约定,会将int类型的返回值放到%rax中,我们只需要从%rax中获取值即可。
接下来会执行如下汇编代码:
// 将Method::access_flags存储到%r11d中 0x00007fffe1014e4b: mov 0x28(%rbx),%r11d // 判断是否为static本地方法,其中$0x8表示JVM_ACC_STATIC 0x00007fffe1014e4f: test $0x8,%r11d // 如果为0,表示是非static方法,要跳转到-- L2 -- 0x00007fffe1014e56: je 0x00007fffe1014e74 // 执行这里代码时,说明方法是static方法 // 如下4个mov指令将通过Method->ConstMehod->ConstantPool->mirror // 获取到java.lang.Class的oop 0x00007fffe1014e5c: mov 0x10(%rbx),%r11 0x00007fffe1014e60: mov 0x8(%r11),%r11 0x00007fffe1014e64: mov 0x20(%r11),%r11 0x00007fffe1014e68: mov 0x70(%r11),%r11 // 将mirror存储到栈帧中,也就是oop temp这个slot位置 0x00007fffe1014e6c: mov %r11,0x10(%rbp) // 将mirror拷到%rsi中作为静态方法调用的第2个参数 0x00007fffe1014e70: lea 0x10(%rbp),%rsi
对于实例来说,get()方法是静态方法,所以会将mirror放到栈帧中的oop temp中。
接下来执行如下汇编:
// 获取Method::native_function的地址并存储到%rax中 0x00007fffe1014e74: mov 0x60(%rbx),%rax // %r11中存储的是SharedRuntime::native_method_throw_unsatisfied_link_error_entry() 0x00007fffe1014e78: movabs $0x7ffff6a08f14,%r11 // 判断rax中的地址是否是native_method_throw_unsatisfied_link_error_entry的 // 地址,如果是说明本地方法未绑定 0x00007fffe1014e82: cmp %r11,%rax // 如果不等于,即native方法已经绑定,跳转到----L3---- 0x00007fffe1014e85: jne 0x00007fffe1014f1b // ... 省略查找native_function的逻辑 // 重新获取Method*到%rbx中 0x00007fffe1014f13: mov -0x18(%rbp),%rbx // 获取native_function的地址拷到%rax中 0x00007fffe1014f17: mov 0x60(%rbx),%rax
我们假设native_function已经存储到了Method实例的对应slot处,那么接下来就直接调用这个本地函数了,如下:
// 将当前线程的JavaThread::jni_environment放入c_rarg0,也就是%rdi中 0x00007fffe1014f1b: lea 0x210(%r15),%rdi // ... // 调用native_function本地函数 0x00007fffe1014f4c: callq *%rax // ... // 如下4行代码是为了保存调用native_function函数后得到的结果,将 // 结果存储到栈顶 0x00007fffe1014f51: sub $0x10,%rsp 0x00007fffe1014f55: vmovsd %xmm0,(%rsp) 0x00007fffe1014f5a: sub $0x10,%rsp 0x00007fffe1014f5e: mov %rax,(%rsp)
在调用native方法时,将JNIEnv*存储到c_rarg0,mirror存储到c_rarg1中,然后调用native方法的本地函数。根据C/C++函数的调用约定,如果返回浮点数,则会存储到%xmm0中,如果是对象或整数等类型,则会存储到%rax中。将%xmm0和%rax中的值压入栈中,最后会执行如下汇编代码:
// 将栈顶的代表方法调用结果的数据pop到%rax和%xmm0寄存器中 0x00007fffe101543c: mov (%rsp),%rax 0x00007fffe1015440: add $0x10,%rsp 0x00007fffe1015444: vmovsd (%rsp),%xmm0 0x00007fffe1015449: add $0x10,%rsp // 获取result_handler存储到%r11中 0x00007fffe101544d: mov 0x18(%rbp),%r11 0x00007fffe1015451: callq *%r11 // 调用result_handler处理方法调用结果 0x00007fffe1015454: mov -0x8(%rbp),%r11 // 获取sender sp,开始恢复上一个Java栈帧 0x00007fffe1015458: leaveq // 相当于指令mov %ebp,%esp和pop %ebp 0x00007fffe1015459: pop %rdi // 获取return address 0x00007fffe101545a: mov %r11,%rsp // 设置sender sp 0x00007fffe101545d: jmpq *%rdi // 跳转到返回地址处继续执行
调用result_handler处理方法调用结果,最终只是执行了retq指令,所以此次的callq和retq指令执行后没有对栈帧产生任何影响。
继续执行Interpreter::_invoke_return_entry例程,如下:
// 将-0x10(%rbp)存储到%rsp后,置空-0x10(%rbp) 0x00007fffe1006ce0: mov -0x10(%rbp),%rsp // 更改rsp 0x00007fffe1006ce4: movq $0x0,-0x10(%rbp) // 更改栈中特定位置的值 // 恢复bcp和locals,使%r14指向本地变量表,%r13指向bcp 0x00007fffe1006cec: mov -0x38(%rbp),%r13 0x00007fffe1006cf0: mov -0x30(%rbp),%r14 // 获取ConstantPoolCacheEntry的索引并加载到%ecx 0x00007fffe1006cf4: movzwl 0x1(%r13),%ecx // 获取栈中-0x28(%rbp)的ConstantPoolCache并加载到%ecx 0x00007fffe1006cf9: mov -0x28(%rbp),%rbx // shl是逻辑左移,获取字偏移 0x00007fffe1006cfd: shl $0x2,%ecx // 获取ConstantPoolCacheEntry中的_flags属性值 0x00007fffe1006d00: mov 0x28(%rbx,%rcx,8),%ebx // 获取_flags中的低8位中保存的参数大小 0x00007fffe1006d04: and $0xff,%ebx // 注意这里会更改%rsp的指向,会将调用方表达式栈(被调用方局部变量表组成的一部分)中压入的、给调用的 // 方法传递参数的值从表达式栈中弹出去,这样在解释执行的情况下,由调用方完成实参的清理工作 0x00007fffe1006d0a: lea (%rsp,%rbx,8),%rsp // 跳转到下一指令执行 0x00007fffe1006d0e: movzbl 0x3(%r13),%ebx 0x00007fffe1006d13: add $0x3,%r13 0x00007fffe1006d17: movabs $0x7ffff73b7ca0,%r10 0x00007fffe1006d21: jmpq *(%r10,%rbx,8)
如上汇编主要是恢复调用方的栈帧状态,同时清理表达式栈中因为调用方法而压入的实参,最后就是继续执行main()方法中剩余指令了。
公众号 深入剖析Java虚拟机HotSpot 已经更新虚拟机源代码剖析相关文章到60+,欢迎关注,如果有任何问题,可加作者微信mazhimazh,拉你入虚拟机群交流