先看以下2个案例。
Runnable runnable = () -> {
System.out.println(1);
};
new Thread(runnable).start();
为什么调用Thread的start方法就能执行Runnable的代码?
public static void main(String[] args) {
System.out.println(1);
}
作为Java开发者,一定明白main方法是作为Java程序的入口。那么,为什么是main方法?那main方法是如何被调用执行的?
此篇文章借用2个很简单的案例,带读者明白JVM层面是如何调用Java方法。
源码分析:
版本信息如下:
jdk版本:jdk8u40
为了源码的简单,使用字节码解释器:C++解释器
我们不妨借用上文的2个案例来推理一下:
- 方法是属于类
- 在Hotspot中使用 Klass/Oop(C++类)二分模型表示类和对象。
- 类加载步骤大致分为:加载.class文件,根据jvm规范手册解析.class文件中的常量池(ConstantPool)、方法(Method)、变量(Field)等等(详情请看JVM手册第4章)最终生成Klass类装载以上解析后的数据
- 根据以上3点,我们能明白方法是在类解析的过程中就生成好了,并且存放在Klass中,也就是存放在类中。
- Java方法经过 javac 编译后生成.class文件的表示无非就是一条条的字节码指令。
所以,我们不妨大胆猜测,JVM层面调用Java方法无非就是调用类中对应的方法,而方法的表示就是一条条字节码指令(只考虑解释器模式)
既然猜测了,那么接下来就是源码论证环节。
源码选自JVM启动流程中调用Java层面main方法的部分源码细节。/src/share/bin/java.c文件
int JNICALL
JavaMain(void * _args)
{
…………
// 得到启动类。
mainClass = LoadMainClass(env, mode, what);
// 得到启动类中main方法签名。
mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
"([Ljava/lang/String;)V");
// 执行
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
// 尝试结束JVM进程。
LEAVE();
}
我们并不关心如何得到启动类,如何得到main方法的签名,我们只关心如何执行main方法。所以接着看CallStaticVoidMethod的实现。
JNI_ENTRY(void, jni_CallStaticVoidMethod(JNIEnv *env, jclass cls, jmethodID methodID, ...))
…………
JavaValue jvalue(T_VOID); // JavaValue表示Java层面的值,而这里是作为返回值类型
JNI_ArgumentPusherVaArg ap(methodID, args); // 包装参数
jni_invoke_static(env, &jvalue, NULL, JNI_STATIC, methodID, &ap, CHECK);
JNI_END
static void jni_invoke_static(JNIEnv *env, JavaValue* result, jobject receiver, JNICallType call_type, jmethodID method_id, JNI_ArgumentPusher *args, TRAPS) {
…………
// 通过方法签名得到Method(c++类表示方法)
methodHandle method(THREAD, Method::resolve_jmethod_id(method_id));
// 得到方法参数的个数
int number_of_parameters = method->size_of_parameters();
// 参数包装器
JavaCallArguments java_args(number_of_parameters);
// 调用方法
JavaCalls::call(result, method, &java_args, CHECK);
}
这一步很清楚的可以看到拿到Method对象(因为Hotspot是C++实现,所以也存在对象),而Method对象表示一个Java方法。是不是感觉离真相越来越近了~
继续看到JavaCalls::call方法
void JavaCalls::call(JavaValue* result, methodHandle method, JavaCallArguments* args, TRAPS) {
assert(THREAD->is_Java_thread(), "only JavaThreads can make JavaCalls");
// 执行的包装,实际上就是调用call_helper方法。
os::os_exception_wrapper(call_helper, result, &method, args, THREAD);
}
void JavaCalls::call_helper(JavaValue* result, methodHandle* m, JavaCallArguments* args, TRAPS) {
………… 省略很多断言判断
// 拿到解释器的执行入口
// 而解释器的执行入口,是在JVM启动的流程中做了初始化,与当前内容关联不大,所以暂时省略
// 我们清楚,字节码指令仅仅是JVM抽象的指令集。
// 而不是CPU能够执行的ISA指令集
// 所以这里需要包装一个解释器的执行入口,然后去执行方法对应的字节码指令
address entry_point = method->from_interpreted_entry();
………… 省略很多断言判断
// do call
{ JavaCallWrapper link(method, receiver, result, CHECK);
{ HandleMark hm(thread); // HandleMark used by HandleMarkCleaner
// 这里很多人看不懂
// 其实,这里是一个函数指针的回调。
// StubRoutines::call_stub() 拿到具体的函数地址。
// 意义在于,统一入口,再根据entry_point做分发。
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());
}
}
}
}
这段代码比较重要,所以对这里做一个总结:
- 从Method对象中获取到解释器的入口地址。解释器的入口地址是JVM启动的过程中放入的,与当前的流程关联不大,所以不在此赘述。
- StubRoutines::call_stub() 这个方法会拿到一个Calls to Java 的函数地址,这里实际上就是在回调函数,而函数是在JVM启动时根据底层平台已经确定好的。很多读者可能看不懂这个写法,更不懂意义在何处。其实,很简单,就是统一入口,屏蔽底层不同平台的实现。
所以看StubRoutines::call_stub()得到的具体函数实现。
static void call_stub(
JavaCallWrapper *call_wrapper,
intptr_t* result,
BasicType result_type,
Method* method,
address entry_point,
intptr_t* parameters,
int parameter_words,
TRAPS) {
EntryFrame *frame =
EntryFrame::build(parameters, parameter_words, call_wrapper, THREAD);
if (!HAS_PENDING_EXCEPTION) {
// 开辟此时方法的栈
thread->push_zero_frame(frame);
// 调用方法
// 内部会调用entry_point,最终就是执行字节码
Interpreter::invoke_method(method, entry_point, THREAD);
// 出栈
thread->pop_zero_frame();
}
}
// 因为这里存在多次方法的回调,所以为了篇幅的简单,省略一部分过程,直接看到最终的字节码执行方法
// Interpreter::invoke_method方法,最终会回调此方法执行方法的字节码指令集
int CppInterpreter::normal_entry(Method* method, intptr_t UNUSED, TRAPS) {
JavaThread *thread = (JavaThread *) THREAD;
// Allocate and initialize our frame.
InterpreterFrame *frame = InterpreterFrame::build(method, CHECK_0);
thread->push_zero_frame(frame);
// 循环执行方法中字节码指令集
main_loop(0, THREAD);
return 0;
}
因为这里存在多次方法的回调,所以为了篇幅的简单,省略一部分过程,直接看到最终的字节码执行main_loop方法,此方法会循环的执行方法中字节码指令集。
写到这,也已经通过源码论证了我们的猜想。实际上就是JVM执行方法的字节码指令集。