JVM方法调用——native方法到java方法
native方法到java方法
最为经典的一个JNI调用Java方法就是调用Main函数,下面顺便会介绍java的启动过程。
java的main
函数在src/java.base/share/native/launcher/main.c
,这个函数会处理一些有参数的内容,然后进入到libjli的JLI_Launch
函数中。这个函数最主要的任务就是对libjvm.so进行了加载,同时获取了一些符号的地址,将这些地址保存在了如下一个结构体中:
/*
* Pointers to the needed JNI invocation API, initialized by LoadJavaVM.
*/
typedef jint (JNICALL *CreateJavaVM_t)(JavaVM **pvm, void **env, void *args);
typedef jint (JNICALL *GetDefaultJavaVMInitArgs_t)(void *args);
typedef jint (JNICALL *GetCreatedJavaVMs_t)(JavaVM **vmBuf, jsize bufLen, jsize *nVMs);
typedef struct {
CreateJavaVM_t CreateJavaVM; // JNI_CreateJavaVM
GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs; // JNI_GetDefaultJavaVMInitArgs
GetCreatedJavaVMs_t GetCreatedJavaVMs; // JNI_GetCreatedJavaVMs
} InvocationFunctions;
虚拟机以动态链接库的形式存在,除了享受到一般的动态链接库的好处之外,还可以将jvm嵌入到其他程序里面。
正式进入主类的main方法其实还有非常多的任务,主要就是初始化一些数据、生成一些代码,以及启动一些工作线程,这些任务主要在Threads::create_vm
和init_globals
中完成。之所以要分为两个线程来完成,推测可能是便于处理异常?我也不太清楚这么设计的原因。在加载完主类之后就会开始执行main
方法。
首先使用JNI接口JNIEnv_::CallStaticVoidMethod
,这里使用的是C++的JNI接口和C的在风格上有一些不同。这个函数会调用位于src/hotspot/share/prims/jni.cpp
的函数jni_CallStaticVoidMethod
,下面是其定义:
JNI_ENTRY(void, jni_CallStaticVoidMethod(JNIEnv *env, jclass cls, jmethodID methodID, ...))
JNIWrapper("CallStaticVoidMethod");
HOTSPOT_JNI_CALLSTATICVOIDMETHOD_ENTRY(env, cls, (uintptr_t) methodID);
DT_VOID_RETURN_MARK(CallStaticVoidMethod);
va_list args;
va_start(args, methodID);
JavaValue jvalue(T_VOID);
JNI_ArgumentPusherVaArg ap(methodID, args);
jni_invoke_static(env, &jvalue, NULL, JNI_STATIC, methodID, &ap, CHECK);
va_end(args);
JNI_END
其做的事件有两件,首先是将可变参数构造为一个JNI_ArgumentPusherVaArg对象,这个对象能够提供对参数的访问。另外就是进入到jni_invoke_static
,正式发起调用,接下来会依次去到JavaCalls::call
、os::os_exception_wrapper
、JavaCalls::call_helper
,call_helper里面必须做的就是获取了入口点和处理java方法返回值,其中真正发起调用的地方在:
// do call
{ JavaCallWrapper link(method, receiver, result, CHECK); // 无法理解,除了对JNIHandleBlock的处理之外。
{ HandleMark hm(thread); // HandleMark used by HandleMarkCleaner
// generate_call_stub
StubRoutines::call_stub()(
(address)&link,
// (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
result_val_address, // see NOTE above (compiler problem)
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
);
result = link.result(); // circumvent MS C++ 5.0 compiler bug (result is clobbered across call)
// Preserve oop return value across possible gc points
if (oop_result_flag) {
thread->set_vm_result((oop) result->get_jobject());
}
}
} // Exit JavaCallWrapper (can block - potential return oop must be preserved)
里面有个JavaCallWrapper
,暂时没有完全明白到底做了什么,里面获取了receiver和result指针,我觉得可能是和GC有关,另外的就是设置了JNIHandleBlock
这个和线程的资源管理有关。StubRoutines::call_stub
会获取一个CallStub
类型的例程。
下面是CallStub的类型声明:
// Calls to Java
typedef void (*CallStub)(
address link,
intptr_t* result,
BasicType result_type,
Method* method,
address entry_point,
intptr_t* parameters,
int size_of_parameters,
TRAPS
);
一共有8个参数,amd64 abi下前6个参数使用寄存器进行传递所以还有两个参数需要使用栈传递,分别是size_of_parameters
和__thread__
,进入了call_stub例程后的栈结构如下:
这个例程由StubGenerator::generate_call_stub
生成,负责栈的切换和调用的实现,栈的变化逻辑非常清晰,下面的代码已经有了注释,进入了java方法的入口之后的处理逻辑已经说过了:
// Call stub stack layout word offsets from rbp
enum call_stub_layout {
rsp_after_call_off = -12,
mxcsr_off = rsp_after_call_off, // simd的寄存器。
r15_off = -11,
r14_off = -10,
r13_off = -9,
r12_off = -8,
rbx_off = -7,
call_wrapper_off = -6,
result_off = -5,
result_type_off = -4,
method_off = -3,
entry_point_off = -2,
parameters_off = -1,
rbp_off = 0,
retaddr_off = 1,
parameter_size_off = 2,
thread_off = 3
};
address generate_call_stub(address& return_address) {
assert((int)frame::entry_frame_after_call_words == -(int)rsp_after_call_off + 1 &&
(int)frame::entry_frame_call_wrapper_offset == (int)call_wrapper_off,
"adjust this code");
StubCodeMark mark(this, "StubRoutines", "call_stub");
address start = __ pc();
// same as in generate_catch_exception()!
const Address rsp_after_call(rbp, rsp_after_call_off * wordSize);
const Address call_wrapper (rbp, call_wrapper_off * wordSize);
const Address result (rbp, result_off * wordSize);
const Address result_type (rbp, result_type_off * wordSize);
const Address method (rbp, method_off * wordSize);
const Address entry_point (rbp, entry_point_off * wordSize);
const Address parameters (rbp, parameters_off * wordSize);
const Address parameter_size(rbp, parameter_size_off * wordSize);
// same as in generate_catch_exception()!
const Address thread (rbp, thread_off * wordSize);
const Address r15_save(rbp, r15_off * wordSize);
const Address r14_save(rbp, r14_off * wordSize);
const Address r13_save(rbp, r13_off * wordSize);
const Address r12_save(rbp, r12_off * wordSize);
const Address rbx_save(rbp, rbx_off * wordSize);
// stub code
__ enter();
__ subptr(rsp, -rsp_after_call_off * wordSize);
// 将参数从寄存器放入栈中,防止占用寄存器。
// save register parameters
__ movptr(parameters, c_rarg5); // parameters
__ movptr(entry_point, c_rarg4); // entry_point
__ movptr(method, c_rarg3); // method
__ movl(result_type, c_rarg2); // result type
__ movptr(result, c_rarg1); // result
__ movptr(call_wrapper, c_rarg0); // call wrapper
// 保存callee-saved寄存器。
// save regs belonging to calling function
__ movptr(rbx_save, rbx);
__ movptr(r12_save, r12);
__ movptr(r13_save, r13);
__ movptr(r14_save, r14);
__ movptr(r15_save, r15);
if (UseAVX > 2) {
__ movl(rbx, 0xffff);
__ kmovwl(k1, rbx);
}
// 不太会用simd拓展,所以不太明白发生了什么,大概还是放入了栈中对应位置吧?
const Address mxcsr_save(rbp, mxcsr_off * wordSize);
{
Label skip_ldmx;
__ stmxcsr(mxcsr_save);
__ movl(rax, mxcsr_save);
__ andl(rax, MXCSR_MASK); // Only check control and mask bits
ExternalAddress mxcsr_std(StubRoutines::addr_mxcsr_std());
__ cmp32(rax, mxcsr_std);
__ jcc(Assembler::equal, skip_ldmx);
__ ldmxcsr(mxcsr_std);
__ bind(skip_ldmx);
}
// Load up thread register
__ movptr(r15_thread, thread);
__ reinit_heapbase();
// 从JavaArguments的数组中复制对应字数的数据到栈中,这个结构就是通过JNI_ArgumentPusher把可变参数列表转化而来的。
// pass parameters if any
BLOCK_COMMENT("pass parameters if any");
Label parameters_done;
__ movl(c_rarg3, parameter_size);
__ testl(c_rarg3, c_rarg3);
__ jcc(Assembler::zero, parameters_done);
Label loop;
__ movptr(c_rarg2, parameters); // parameter pointer
__ movl(c_rarg1, c_rarg3); // parameter counter is in c_rarg1
__ BIND(loop);
__ movptr(rax, Address(c_rarg2, 0));// get parameter
__ addptr(c_rarg2, wordSize); // advance to next parameter
__ decrementl(c_rarg1); // decrement counter
__ push(rax); // pass parameter
__ jcc(Assembler::notZero, loop);
// call Java function
__ BIND(parameters_done);
__ movptr(rbx, method); // get Method*
__ movptr(c_rarg1, entry_point); // get entry_point
__ mov(r13, rsp); // set sender sp
BLOCK_COMMENT("call Java function");
__ call(c_rarg1); // 在这里进入entry_point,实际上是generate_normal_entry生成的代码。
BLOCK_COMMENT("call_stub_return_address:");
return_address = __ pc(); // 会成为一个异常返回点,如果发生了无法处理的异常会跳转到这里。
// 处理返回值,返回值在java方法的栈顶,也就是rax或者xmm0中,x86实现或许有不同。
// store result depending on type (everything that is not
// T_OBJECT, T_LONG, T_FLOAT or T_DOUBLE is treated as T_INT)
__ movptr(c_rarg0, result); // 将返回值保存地址赋给c_rarg0。
Label is_long, is_float, is_double, exit;
__ movl(c_rarg1, result_type);
__ cmpl(c_rarg1, T_OBJECT);
__ jcc(Assembler::equal, is_long);
__ cmpl(c_rarg1, T_LONG);
__ jcc(Assembler::equal, is_long);
__ cmpl(c_rarg1, T_FLOAT);
__ jcc(Assembler::equal, is_float);
__ cmpl(c_rarg1, T_DOUBLE);
__ jcc(Assembler::equal, is_double);
// handle T_INT case
__ movl(Address(c_rarg0, 0), rax); // 返回值放在rax,然后将返回值移动到目标地址。
__ BIND(exit);
// pop parameters 恢复到rsp_after_call位置。
__ lea(rsp, rsp_after_call);
// 将值恢复到寄存器。
__ movptr(r15, r15_save);
__ movptr(r14, r14_save);
__ movptr(r13, r13_save);
__ movptr(r12, r12_save);
__ movptr(rbx, rbx_save);
__ ldmxcsr(mxcsr_save);
// restore rsp
__ addptr(rsp, -rsp_after_call_off * wordSize);
// return
__ vzeroupper();
__ pop(rbp);
__ ret(0);
// 这里就返回了,后面还有代码是因为更好利用指令缓存,不常用到的代码就放后面了。
// handle return types different from T_INT
__ BIND(is_long);
__ movq(Address(c_rarg0, 0), rax);
__ jmp(exit);
__ BIND(is_float);
__ movflt(Address(c_rarg0, 0), xmm0);
__ jmp(exit);
__ BIND(is_double);
__ movdbl(Address(c_rarg0, 0), xmm0);
__ jmp(exit);
return start;
}
在正式进入被调用的java
函数之前的栈布局和枚举call_stub_layout
中描述的一样,除了下面还有传入的参数以外。