本文由HeapDump性能社区首席讲师鸠摩(马智)授权整理发布
第29篇-调用Java主类的main()方法
前面已经写了许多篇介绍字节码指令对应的汇编代码执行逻辑,还有一些字节码指令对应的汇编代码逻辑没有介绍,这些指令包括方法调用指令、同步指令、异常抛出指令,这些指令的汇编代码实现逻辑比较复杂,所以后面在介绍到方法调用、同步和异常处理的知识点时,会通过大篇幅的文章进行详细介绍!
在第1篇中大概介绍过Java中主类方法main()的调用过程,这一篇介绍的详细一点,大概的调用过程如下图所示。
其中浅红色的函数由主线程执行,而另外的浅绿色部分由另外一个线程执行,浅绿色的线程最终也会负责执行Java主类中的main()方法。在JavaMain()函数中调用LoadMainClass()函数加载Java主类。接着在JavaMain()函数中有如下调用:
源代码位置:openjdk/jdk/src/share/bin/java.c
mainID = (*env)->GetStaticMethodID(
env,
mainClass,
"main",
"([Ljava/lang/String;)V");
env为JNIEnv*类型。调用JNIEnv类型中定义的GetStaticMethodID()函数获取Java主类中main()方法的方法唯一ID,调用GetStaticMethodID()函数就是调用jni_GetStaticMethodID()函数,此函数的实现如下:
源代码位置:openjdk/hotspot/src/share/vm/prims/jni.cpp
JNI_ENTRY(jmethodID, jni_GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig))
jmethodID ret = get_method_id(env, clazz, name, sig, true, thread);
return ret;
JNI_END
static jmethodID get_method_id(
JNIEnv *env,
jclass clazz,
const char *name_str,
const char *sig,
bool is_static,
TRAPS
){
const char *name_to_probe = (name_str == NULL)
? vmSymbols::object_initializer_name()->as_C_string()
: name_str;
TempNewSymbol name = SymbolTable::probe(name_to_probe, (int)strlen(name_to_probe));
TempNewSymbol signature = SymbolTable::probe(sig, (int)strlen(sig));
KlassHandle klass(THREAD,java_lang_Class::as_Klass(JNIHandles::resolve_non_null(clazz)));
// 保证java.lang.Class类已经初始化完成
klass()->initialize(CHECK_NULL);
Method* m;
if ( name == vmSymbols::object_initializer_name() || 查找的是<init>方法
name == vmSymbols::class_initializer_name() ) { 查找的是<clinit>方法
// 因为要查找的是构造函数,构造函数没有继承特性,所以当前类找不到时不向父类中继续查找
if (klass->oop_is_instance()) {
// find_method()函数不会向上查找
m = InstanceKlass::cast(klass())->find_method(name, signature);
} else {
m = NULL;
}
} else {
// lookup_method()函数会向上查找
m = klass->lookup_method(name, signature);
if (m == NULL && klass->oop_is_instance()) {
m = InstanceKlass::cast(klass())->lookup_method_in_ordered_interfaces(name, signature);
}
}
return m->jmethod_id();
}
获取Java类中main()方法的jmethod_id。
源代码位置:method.hpp
// Get this method's jmethodID -- allocate if it doesn't exist
jmethodID jmethod_id() {
methodHandle this_h(this);
return InstanceKlass::get_jmethod_id(method_holder(), this_h);
}
调用的InstanceKlass::get_jmethod_id()函数获取唯一ID,关于如何获取或生成ID的过程这里不再详细介绍,有兴趣的自行研究。
在JavaMain()函数中有如下调用:
mainArgs = CreateApplicationArgs(env, argv, argc);
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
通过调用CallStaticVoidMethod()函数来调用Java主类中的main()方法。控制权转移到Java主类中的main()方法之中。调用CallStaticVoidMethod()函数就是调用jni_CallStaticVoidMethod()函数,此函数的实现如下:
源代码位置:openjdk/hotspot/src/share/vm/prims/jni.cpp
JNI_ENTRY(void, jni_CallStaticVoidMethod(JNIEnv *env, jclass cls, jmethodID methodID, ...))
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
将传给Java方法的参数以C的可变长度参数传入后,使用JNI_ArgumentPusherVaArg实例ap是将其封装起来。JNI_ArgumentPusherVaArg类的继承体系如下:
JNI_ArgumentPusherVaArg->JNI_ArgumentPusher->SignatureIterator
调用的jni_invoke_static()函数的实现如下:
// 通过jni的方式调用Java静态方法
static void jni_invoke_static(
JNIEnv *env,
JavaValue* result,
jobject receiver,
JNICallType call_type,
jmethodID method_id,
JNI_ArgumentPusher *args,
TRAPS
){
Method* m = Method::resolve_jmethod_id(method_id);
methodHandle method(THREAD, m);
ResourceMark rm(THREAD);
int number_of_parameters = method->size_of_parameters();
// 这里进一步将要传给Java的参数转换为JavaCallArguments对象传下去
JavaCallArguments java_args(number_of_parameters);
args->set_java_argument_object(&java_args);
// Fill out(填,填写) JavaCallArguments object
Fingerprinter fp = Fingerprinter(method);
uint64_t x = fp.fingerprint();
args->iterate(x);
// Initialize result type
BasicType bt = args->get_ret_type();
result->set_type(bt);
// Invoke the method. Result is returned as oop.
JavaCalls::call(result, method, &java_args, CHECK);
// Convert result
if (
result->get_type() == T_OBJECT ||
result->get_type() == T_ARRAY
) {
oop tmp = (oop) result->get_jobject();
jobject jobj = JNIHandles::make_local(env,tmp);
result->set_jobject(jobj);
}
}
通过JavaCalls::call()函数来调用Java主类的main()方法。关于JavaCalls::call()函数大家应该不会陌生,这个函数是怎么建立Java栈帧以及找到Java方法入口在之前已经详细介绍过,这里不再介绍。
第30篇-解释执行main()方法小实例
我们在介绍完一些常用字节码指令的汇编代码执行逻辑后,基本看到一个main()方法从开始调用、栈帧建立、字节码执行的整个逻辑了,但是方法退栈、同步方法以及异常抛出等知识点还没有介绍,我们这里只举个最简单的例子,可以帮助大家回顾一下之前那么多篇文章所学到的内容。
在第7篇详细介绍过为Java方法创建的栈帧,如下图所示。
调用完generate_fixed_frame()函数后一些寄存器中保存的值如下:
rbx:Method*
ecx:invocation counter
r13:bcp(byte code pointer)
rdx:ConstantPool* 常量池的地址
r14:本地变量表第1个参数的地址
现在我们举一个例子,来完整的走一下解释执行的过程。这个例子如下:
package com.classloading;
public class Test {
public static void main(String[] args) {
int i = 0;
i = i++;
}
}
通过javap -verbose Test.class命令反编译后的字节码文件内容如下:
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // com/classloading/Test
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 main
#9 = Utf8 ([Ljava/lang/String;)V
#10 = Utf8 SourceFile
#11 = Utf8 Test.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 com/classloading/Test
#14 = Utf8 java/lang/Object
{
...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: iconst_0
1: istore_1
2: return
}
如上实例对应的栈帧状态如下图所示。
现在我们就以解释执行的方式执行main()方法中的字节码。由于是从虚拟机调用过来的,而调用完generate_fixed_frame()函数后一些寄存器中保存的值并没有涉及到栈顶缓存,所以需要从iconst_0这个字节码指令的vtos入口进入,然后找到iconst_0这个字节码指令对应的机器指令片段。
现在回顾一下字节码分派的逻辑,在generate_normal_entry()函数中会调用generate_fixed_frame()函数为Java方法的执行生成对应的栈帧,接下来还会调用dispatch_next()函数执行Java方法的字节码,首次获取字节码时的汇编如下:
// 在generate_fixed_frame()方法中已经让%r13存储了bcp
movzbl 0x0(%r13),%ebx // %ebx中存储的是字节码的操作码
// $0x7ffff73ba4a0这个地址指向的是对应state状态下的一维数组,长度为256
movabs $0x7ffff73ba4a0,%r10
// 注意%r10中存储的是常量,根据计算公式%r10+%rbx*8来获取指向存储入口地址的地址,
// 通过*(%r10+%rbx*8)获取到入口地址,然后跳转到入口地址执行
jmpq *(%r10,%rbx,8)
注意如上的$0x7ffff73ba4a0这个常量值已经表示了栈顶缓存状态为vtos下的一维数组首地址。而在首次进行方法的字节码分派时,通过0x0(%r13)即可取出字节码对应的Opcode,使用这个Opcode可定位到iconst_0的入口地址。
%r10指向的是对应栈顶缓存状态state下的一维数组,长度为256,其中存储的值为Opcode,这在第8篇详细介绍过,示意图如下图所示。
现在就是看入口为vtos,出口为itos的iconst_0所要执行的汇编代码了,如下:
...
// vtos入口
mov $0x1,%eax
...
// iconst_0对应的汇编代码
xor %eax,%eax
汇编指令足够简单,最后将值存储到了%eax中,所以也就是栈顶缓存的出口状态为itos。
上图紫色的部分是本地变量表,由于本地变量表的大小为2,所以我画了2个方格表示slot。
执行下一个字节码指令istore_1,也会执行字节码分派相关的逻辑。这里需要提醒下,其实之前在介绍字节码指令对应的汇编时,只关注了字节码指令本身的执行逻辑,其实在为每个字节码指令生成机器指令时,一般都会为这些字节码指令生成3部分机器指令片段:
(1)不同栈顶状态对应的入口执行逻辑;
(2)字节码指令本身需要执行的逻辑;
(3)分派到下一个字节码指令的逻辑。
对于字节码指令模板定义中,如果flags中指令有disp,那么这些指令自己会含有分派的逻辑,如goto、ireturn、tableswitch、lookupswitch、jsr等。由于我们的指令是iconst_0,所以会为这个字节码指令生成分派逻辑,生成的逻辑如下:
movzbl 0x1(%r13),%ebx // %ebx中存储的是字节码的操作码
movabs itos对应的一维数组的首地址,%r10
jmpq *(%r10,%rbx,8)
需要注意的是,如果要让%ebx中存储istore_1的Opcode,则%r13需要加上iconst_0指令的长度,即1。由于iconst_0执行后的出口栈顶缓存为itos,所以要找到入口状态为itos,而Opcode为istore_1的机器指令片段执行。指令片段如下:
mov %eax,-0x8(%r14)
代码将栈顶的值%eax存储到本地变量表下标索引为1的位置处。通过%r14很容易定位到本地变量表的位置,执行完成后的栈状态如下图所示。
执行iconst_0和istore_1时,整个过程没有向表达式栈(上图中sp/rsp开始以下的部分就是表达式栈)中压入0,实际上如果没有栈顶缓存的优化,应该将0压入栈顶,然后弹出栈顶存储到局部变量表,但是有了栈顶缓存后,没有压栈操作,也就有弹栈操作,所以能极大的提高程序的执行效率。
return指令判断的逻辑比较多,主要是因为有些方法可能有synchro