JVM内部机制(二)Java方法调用
封装、继承、多态。
封装做为继承奠定基础,继承又为多态的表达奠定基础。
堆栈
堆的索引保存在栈上。
堆栈是一种数据结构,但更准确的说法是它是一种算法。
方法调用过程中, 堆栈的作用:
-
存储方法用到的参数。用BP+偏移量的方法,把参数引入到方法作用域内。
-
存储方法的返回值。把返回值重新放回到调用方的栈中。
-
存储下一条指令的地址, 以便方法调用返回后继续执行。
-
存储机器指令用到的操作数。
堆栈先进后出、后进先出的性质, 契合了函数嵌套调用的过程。
就像拉皮筋,被动伸展(这个过程消耗生物体能量),却可自动收缩。递归表达了更高级的调用形式- 自动伸缩。
方法调用的技术实现可通过两个寄存器(bp,sp)交替赋值做到。这样形式是为传参和带值返回设计的,因为参数的数量和类型是不固定的。
如果不考虑传参,直接jmp也可以模拟函数调用。在数字电路的实现程序中存在很多的goto或者直接跳转。
进程加载后,加载器在table中默认寻找代码段main标签指向的地址,并放入寄存器(CS:IP),开始拉皮筋式的函数调用。
CS:IP 和 BP:SP 张开了一个至少2个维度的空间。随着指令指针IP的往前推进,BP和SP像两个用弹簧连在一起的滑块,上下移动。
在Hotspot中通过解析字节码实现的函数调用,在宏观上也是基于上面的认识。
JVM中java函数的执行过程
JVM中执行Java方法经历了两个大的阶段:
-
- JVM为执行java函数配备堆栈模型,然后JMP到调用Main函数。 这一步实现了从C++到Java的跨越。
虚拟机的设计目标就是为了做到这一步,JVM中所有的设计都是为了高效、准确的执行java字节码中的方法。
-
- 从字节码中解析字节码函数调用指令,eg. invoke_virtual,通过解释器或者JIT的指令分发函数去执行java函数。 从这个角度讲,java函数完成了‘函数’的定义,这个阶段的函数是抽象命令的集合。当被JIT解释执行的时候,才对应到了不同CPU架构下的具体指令的函数实现。Java的函数实现过程是用具体CPU架构下的指令去拟合的过程。
从抽象到具体的实现过程中,存在各种映射关系。这种映射可以有不同类型的解释器或者编译器完成。
第一阶段 使用JNI接口调用Main函数
下面是一个长途跋涉的分析过程。
在jvm的加载过程中,InvocationFunctions* ifn 这个结构体是libjvm动态库加载过程的精华所在。InvocationFunctions 是由3个函数指针组成的结构体。分别指向三个JNI接口。
启动步骤包括
JLI_Launch -> LoadJavaVM -> JVMInit
第一步,启动器中的main函数
int
main(int argc, char **argv)
{
/* ..省略.. */ // 这一步直接返回JLI_Launch
return JLI_Launch(margc, margv,
sizeof(const_jargs) / sizeof(char *), const_jargs,
sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,
FULL_VERSION,
DOT_VERSION,
(const_progname != NULL) ? const_progname : *margv,
(const_launcher != NULL) ? const_launcher : *margv,
(const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,
const_cpwildcard, const_javaw, const_ergo_class);
}
第二步,初始化jvm, 这一步相当于为工厂车间里面的各种生产工具上电。
/*
* Entry point.
*/
int
JLI_Launch(int argc, char ** argv, /* main argc, argc */
int jargc, const char** jargv, /* java args */
int appclassc, const char** appclassv, /* app classpath */
const char* fullversion, /* full version defined */
const char* dotversion, /* dot version defined */
const char* pname, /* program name */
const char* lname, /* launcher name */
jboolean javaargs, /* JAVA_ARGS */
jboolean cpwildcard, /* classpath wildcard*/
jboolean javaw, /* windows-only javaw */
jint ergo /* ergonomics class policy */
)
{
int mode = LM_UNKNOWN; // 有三种模式 { "Unknown", "Main class", "JAR file" }; 用Main class 启动或者jar包启动,默认为未知
char *what = NULL;
char *cpath = 0;
char *main_class = NULL; // 指定main_class
int ret;
InvocationFunctions ifn; // vm从动态库加载以后,返回地址存在这个结构中。为JNI本地函数调用放开接口,这个结构体中存储了3个函数指针。调用者可直接跳转。
jlong start, end;
char jvmpath[MAXPATHLEN]; // jvm 路径
char jrepath[MAXPATHLEN]; // jre 路径
char jvmcfg[MAXPATHLEN];
_fVersion = fullversion;
_dVersion = dotversion;
_launcher_name = lname;
_program_name = pname;
_is_java_args = javaargs;
_wc_enabled = cpwildcard;
_ergo_policy = ergo; // java -XX:+PrintFlagsInitial -version jvm 运行时参数设置。 ergonomics class 可以解释为 java运行时体征类
/*
* 略过
*/
if (!LoadJavaVM(jvmpath, &ifn)) { // 从相应平台上加载jvm的动态链接库,初始化ifn
return(6);
}
.... 为java application设置参数(略过) ....
// 启动jvm, 这个函数以后,java就像天空中的风筝,与启动器分道扬镳了
return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}
接下来,创建一个启动线程。
int
JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret)
{
ShowSplashScreen();
return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}
最后,设置主线程堆栈大小,传入参数,正式载入JavaMain
int
ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret)
{
/*
* If user doesn't specify stack size, check if VM has a preference.
* Note that HotSpot no longer supports JNI_VERSION_1_1 but it will
* return its default stack size through the init args structure.
*/
if (threadStackSize == 0) {
struct JDK1_1InitArgs args1_1;
memset((void*)&args1_1, 0, sizeof(args1_1));
args1_1.version = JNI_VERSION_1_1;
ifn->GetDefaultJavaVMInitArgs(&args1_1); /* ignore return value */
if (args1_1.javaStackSize > 0) {
threadStackSize = args1_1.javaStackSize;
}
}
{ /* Create a new thread to create JVM and invoke main method */
JavaMainArgs args;
int rslt;
args.argc = argc;
args.argv = argv;
args.mode = mode;
args.what = what;
args.ifn = *ifn;
rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
/* If the caller has deemed there is an error we
* simply return that, otherwise we return the value of
* the callee
*/
return (ret != 0) ? ret : rslt;
}
}
jvm中使用pthread_create创建线程资源。
关于pthread_create相关的系统调用函数在
在pthread_create调用过程中,参数 (void ()(void*))continuation 是一个函数指针格式的入参,指向JavaMain入口函数。
JavaMain的定义
int JNICALL JavaMain(void * args); /* entry point
int JNICALL
JavaMain(void * _args)
{
JavaMainArgs *args = (JavaMainArgs *)_args; // 参数的地址是随线程启动传入的
int argc = args->argc; // 参数数量,对函数来说,数量和类型同等重要,因为它是寻址依据。
char **argv = args->argv;
int mode = args->mode;
char *what = args->what;
InvocationFunctions ifn = args->ifn;//函数指针构成的结构体
JavaVM *vm = 0;
JNIEnv *env = 0;
jclass mainClass = NULL;
jclass appClass = NULL; // actual application class being launched
jmethodID mainID;
jobjectArray mainArgs;
int ret = 0;
jlong start, end;
....
if (!InitializeJVM(&vm, &env, &ifn)) { // ifn->CreateJavaVM(pvm, (void **)penv, &args); 这一步初始化虚拟机, 可以看到ifn所以jvm动态库入口的函数指针,在这个函数中的作用是初始化JVM。以上的5步函数跳转是为这次调用准备环境变量和参数。
JLI_ReportErrorMessage(JVM_ERROR1);
exit(1);
}
.....
/* Invoke main method. */ // 这里调用Main函数。
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
}
启动器主体部分逻辑分析完毕。
ifn的定义如下:
/*
* 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;
GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs;
GetCreatedJavaVMs_t GetCreatedJavaVMs;
} InvocationFunctions;
ifn->CreateJavaVM的值指向了libjvm动态库导出的符号链接JNI_CreateJavaVM。
另外两个函数指针被Java Native函数使用。
这个方法的定义文件是
/jdk8u/hotspot/src/share/vm/prims/jni.cpp
JNI_CreateJavaVM函数后,Java世界大门从此打开。
Java中的函数调用过程
函数调用堆栈
- JavaCalls
经过一系列的引导和堆栈切换,最终函数落到这个地方:
jdk8u/hotspot/src/share/vm/runtime/javaCalls.cpp
...
// do call
{ JavaCallWrapper link(method, receiver, result, CHECK);
{ HandleMark hm(thread); // HandleMark used by HandleMarkCleaner
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
);
} // Exit JavaCallWrapper (can block - potential return oop must be preserved)
这个方法是Jvm中最精妙的地方,它是一个陷阱。
Java文件中定义的方法, 它按照MethodDesc定义的格式被载入内存后,始终以java字节码的形式存在于Metaspace的堆中。背后的问题是,C++生成的机器指令如何调用抽象的“字节码”?
或者说,java的字节码是如何“嫁接”或者“寄生”到C++的堆栈中的?
字节码中的method在JVM中布局结构,如下图
call_stub 也是一个函数指针,定义如下:
// 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
);
static CallStub call_stub() {
return CAST_TO_FN_PTR(CallStub, _call_stub_entry);
}
x86平台上,_call_stub_entry 在文件
jdk8u/hotspot/src/cpu/x86/vm/stubGenerator_x86_64.cpp
中被初始化,这个类是平台相关的实现。
StubRoutines::_call_stub_entry =
generate_call_stub(StubRoutines::_call_stub_return_address);
这个陷阱的布置路线
thread.cpp -> Threads::create_vm()
↓↓↓↓↓↓↓↓↓
init.cpp -> init_globals() -> stubRoutines_init1() -> stubRoutines_init2()
↓↓↓↓↓↓↓↓↓
stubRoutines.cpp
StubGenerator_generate() -> generate_initial()
↓↓↓↓↓↓↓↓↓
StubRoutines::_call_stub_entry =
generate_call_stub(StubRoutines::_call_stub_return_address);
x86上的陷阱生成函数StubGenerator_generate(), 分析一下这个函数
//------------------------------------------------------------------------------------------------------------------------
// Call stubs are used to call Java from C
//
// [ return_from_Java ] <--- rsp
// [ argument word n ]
// ...
// -N [ argument word 1 ]
// -7 [ Possible padding for stack alignment ]
// -6 [ Possible padding for stack alignment ]
// -5 [ Possible padding for stack alignment ]
// -4 [ mxcsr save ] <--- rsp_after_call
// -3 [ saved rbx, ]
// -2 [ saved rsi ]
// -1 [ saved rdi ]
// 0 [ saved rbp, ] <--- rbp,
// 1 [ return address ]
// 2 [ ptr. to call wrapper ]
// 3 [ result ]
// 4 [ result_type ]
// 5 [ method ]
// 6 [ entry_point ]
// 7 [ parameters ]
// 8 [ parameter_size ]
// 9 [ thread ]
address generate_call_stub(address& return_address) {
StubCodeMark mark(this, "StubRoutines", "call_stub");
address start = __ pc(); // 取到当前指令寄存器的值,从这个地址开始注入“字节码”编译后的汇编代码
// stub code parameters / addresses
assert(frame::entry_frame_call_wrapper_offset == 2, "adjust this code");
bool sse_save = false;
const Address rsp_after_call(rbp, -4 * wordSize); // same as in generate_catch_exception()!
const int locals_count_in_bytes (4*wordSize);
const Address mxcsr_save (rbp, -4 * wordSize);
const Address saved_rbx (rbp, -3 * wordSize);
const Address saved_rsi (rbp, -2 * wordSize);
const Address saved_rdi (rbp, -1 * wordSize);
const Address result (rbp, 3 * wordSize);
const Address result_type (rbp, 4 * wordSize);
const Address method (rbp, 5 * wordSize);
const Address entry_point (rbp, 6 * wordSize);
const Address parameters (rbp, 7 * wordSize);
const Address parameter_size(rbp, 8 * wordSize);
const Address thread (rbp, 9 * wordSize); // same as in generate_catch_exception()!
sse_save = UseSSE > 0;
// stub code // 桩代码开始 __ 这个符号是一个宏,被替换后执行emit方法: 直接把机器指令送入代码段内存
__ enter();
__ movptr(rcx, parameter_size); // parameter counter
__ shlptr(rcx, Interpreter::logStackElementSize); // convert parameter count to bytes
__ addptr(rcx, locals_count_in_bytes); // reserve space for register saves
__ subptr(rsp, rcx);
__ andptr(rsp, -(StackAlignmentInBytes)); // Align stack
// save rdi, rsi, & rbx, according to C calling conventions // 这个地方是最微妙的地方,查了很多资料后来终于懂了
__ movptr(saved_rdi, rdi);
__ movptr(saved_rsi, rsi);
__ movptr(saved_rbx, rbx);
// save and initialize %mxcsr
if (sse_save) {
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);
}
// make sure the control word is correct.
__ fldcw(ExternalAddress(StubRoutines::addr_fpu_cntrl_wrd_std()));
#ifdef ASSERT
// make sure we have no pending exceptions
{ Label L;
__ movptr(rcx, thread);
__ cmpptr(Address(rcx, Thread::pending_exception_offset()), (int32_t)NULL_WORD);
__ jcc(Assembler::equal, L);
__ stop("StubRoutines::call_stub: entered with pending exception");
__ bind(L);
}
#endif
// pass parameters if any
BLOCK_COMMENT("pass parameters if any");
Label parameters_done;
__ movl(rcx, parameter_size); // parameter counter
__ testl(rcx, rcx);
__ jcc(Assembler::zero, parameters_done);
// parameter passing loop
Label loop;
// Copy Java parameters in reverse order (receiver last)
// Note that the argument order is inverted in the process
// source is rdx[rcx: N-1..0]
// dest is rsp[rbx: 0..N-1]
__ movptr(rdx, parameters); // parameter pointer
__ xorptr(rbx, rbx);
__ BIND(loop); // 这一步循环,把参数复制到当前栈中
// get parameter
__ movptr(rax, Address(rdx, rcx, Interpreter::stackElementScale(), -wordSize));
__ movptr(Address(rsp, rbx, Interpreter::stackElementScale(),
Interpreter::expr_offset_in_bytes(0)), rax); // store parameter
__ increment(rbx);
__ decrement(rcx);
__ jcc(Assembler::notZero, loop);
// call Java function
__ BIND(parameters_done);
__ movptr(rbx, method); // get Method* // 保存mothod的地址,跳入 _entry_point 以后会用到
__ movptr(rax, entry_point); // get entry_point // 字节码解析入口
__ mov(rsi, rsp); // set sender sp
BLOCK_COMMENT("call Java function");
__ call(rax);
BLOCK_COMMENT("call_stub_return_address:");
return_address = __ pc();
#ifdef COMPILER2
{
Label L_skip;
if (UseSSE >= 2) {
__ verify_FPU(0, "call_stub_return");
} else {
for (int i = 1; i < 8; i++) {
__ ffree(i);
}
// UseSSE <= 1 so double result should be left on TOS
__ movl(rsi, result_type);
__ cmpl(rsi, T_DOUBLE);
__ jcc(Assembler::equal, L_skip);
if (UseSSE == 0) {
// UseSSE == 0 so float result should be left on TOS
__ cmpl(rsi, T_FLOAT);
__ jcc(Assembler::equal, L_skip);
}
__ ffree(0);
}
__ BIND(L_skip);
}
#endif // COMPILER2
// store result depending on type
// (everything that is not T_LONG, T_FLOAT or T_DOUBLE is treated as T_INT)
__ movptr(rdi, result);
Label is_long, is_float, is_double, exit;
__ movl(rsi, result_type);
__ cmpl(rsi, T_LONG);
__ jcc(Assembler::equal, is_long);
__ cmpl(rsi, T_FLOAT);
__ jcc(Assembler::equal, is_float);
__ cmpl(rsi, T_DOUBLE);
__ jcc(Assembler::equal, is_double);
// handle T_INT case
__ movl(Address(rdi, 0), rax);
__ BIND(exit);
// check that FPU stack is empty
__ verify_FPU(0, "generate_call_stub");
// pop parameters
__ lea(rsp, rsp_after_call);
// restore %mxcsr
if (sse_save) {
__ ldmxcsr(mxcsr_save);
}
// restore rdi, rsi and rbx,
__ movptr(rbx, saved_rbx);
__ movptr(rsi, saved_rsi);
__ movptr(rdi, saved_rdi);
__ addptr(rsp, 4*wordSize);
// return
__ pop(rbp);
__ ret(0);
// handle return types different from T_INT
__ BIND(is_long);
__ movl(Address(rdi, 0 * wordSize), rax);
__ movl(Address(rdi, 1 * wordSize), rdx);
__ jmp(exit);
__ BIND(is_float);
// interpreter uses xmm0 for return values
if (UseSSE >= 1) {
__ movflt(Address(rdi, 0), xmm0);
} else {
__ fstp_s(Address(rdi, 0));
}
__ jmp(exit);
__ BIND(is_double);
// interpreter uses xmm0 for return values
if (UseSSE >= 2) {
__ movdbl(Address(rdi, 0), xmm0);
} else {
__ fstp_d(Address(rdi, 0));
}
__ jmp(exit);
return start;
}
这段代码最让人费解的是参数传递问题:
如何在解析字节码的时候,拿到参数的类型和对应的值?
这个问题最终的答案在_entry_point例程中。 它也是一个提前挖好的“陷阱”。
上一个函数执行以后,通过这个指令
__ movptr(rax, entry_point); // get entry_point // 字节码解析入口
__ mov(rsi, rsp); // set sender sp
BLOCK_COMMENT("call Java function");
__ call(rax);
跳入了 entry_point, 这个陷阱的“生成过程”如下
//
// Generic interpreted method entry to (asm) interpreter
//
address InterpreterGenerator::generate_normal_entry(bool synchronized) {
// determine code generation flags
bool inc_counter = UseCompiler || CountCompiledCalls;
// ebx: Method*
// r13: sender sp
address entry_point = __ pc();
const Address constMethod(rbx, Method::const_offset()); // java字节码存在constMethod中, Method中保存constMethod的引用
const Address access_flags(rbx, Method::access_flags_offset());
const Address size_of_parameters(rdx,
ConstMethod::size_of_parameters_offset()); //取得参数数量
const Address size_of_locals(rdx, ConstMethod::size_of_locals_offset()); //取得本地变量的数量。
// get parameter size (always needed)
__ movptr(rdx, constMethod);
__ load_unsigned_short(rcx, size_of_parameters);
// rbx: Method*
// rcx: size of parameters // rcx寄存器的值,来自_call_stub
// r13: sender_sp (could differ from sp+wordSize if we were called via c2i )
__ load_unsigned_short(rdx, size_of_locals); // get size of locals in words
__ subl(rdx, rcx); // rdx = no. of additional locals
// YYY
// __ incrementl(rdx);
// __ andl(rdx, -2);
// see if we've got enough room on the stack for locals plus overhead.
generate_stack_overflow_check();
// get return address
__ pop(rax);
// compute beginning of parameters (r14)
__ lea(r14, Address(rsp, rcx, Address::times_8, -wordSize));
// rdx - # of additional locals
// allocate space for locals
// explicitly initialize locals // 清零
{
Label exit, loop;
__ testl(rdx, rdx);
__ jcc(Assembler::lessEqual, exit); // do nothing if rdx <= 0
__ bind(loop);
__ push((int) NULL_WORD); // initialize local variables
__ decrementl(rdx); // until everything initialized
__ jcc(Assembler::greater, loop);
__ bind(exit);
}
// initialize fixed part of activation frame // 所有的尺寸设定好以后,生成java中的栈帧
generate_fixed_frame(false);
// make sure method is not native & not abstract
#ifdef ASSERT
__ movl(rax, access_flags);
{
Label L;
__ testl(rax, JVM_ACC_NATIVE);
__ jcc(Assembler::zero, L);
__ stop("tried to execute native method as non-native");
__ bind(L);
}
{
Label L;
__ testl(rax, JVM_ACC_ABSTRACT);
__ jcc(Assembler::zero, L);
__ stop("tried to execute abstract method in interpreter");
__ bind(L);
}
#endif
// Since at this point in the method invocation the exception
// handler would try to exit the monitor of synchronized methods
// which hasn't been entered yet, we set the thread local variable
// _do_not_unlock_if_synchronized to true. The remove_activation
// will check this flag.
const Address do_not_unlock_if_synchronized(r15_thread,
in_bytes(JavaThread::do_not_unlock_if_synchronized_offset()));
__ movbool(do_not_unlock_if_synchronized, true);
__ profile_parameters_type(rax, rcx, rdx);
// increment invocation count & check for overflow
Label invocation_counter_overflow;
Label profile_method;
Label profile_method_continue;
if (inc_counter) {
generate_counter_incr(&invocation_counter_overflow,
&profile_method,
&profile_method_continue);
if (ProfileInterpreter) {
__ bind(profile_method_continue);
}
}
Label continue_after_compile;
__ bind(continue_after_compile);
// check for synchronized interpreted methods
bang_stack_shadow_pages(false);
// reset the _do_not_unlock_if_synchronized flag
__ movbool(do_not_unlock_if_synchronized, false);
// check for synchronized methods
// Must happen AFTER invocation_counter check and stack overflow check,
// so method is not locked if overflows.
if (synchronized) {
// Allocate monitor and lock method
lock_method();
} else {
// no synchronization necessary
#ifdef ASSERT
{
Label L;
__ movl(rax, access_flags);
__ testl(rax, JVM_ACC_SYNCHRONIZED);
__ jcc(Assembler::zero, L);
__ stop("method needs synchronization");
__ bind(L);
}
#endif
}
// start execution
#ifdef ASSERT
{
Label L;
const Address monitor_block_top (rbp,
frame::interpreter_frame_monitor_block_top_offset * wordSize);
__ movptr(rax, monitor_block_top);
__ cmpptr(rax, rsp);
__ jcc(Assembler::equal, L);
__ stop("broken stack frame setup in interpreter");
__ bind(L);
}
#endif
// jvmti support
__ notify_method_entry();
__ dispatch_next(vtos);
// invocation counter overflow
if (inc_counter) {
if (ProfileInterpreter) {
// We have decided to profile this method in the interpreter
__ bind(profile_method);
__ call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::profile_method));
__ set_method_data_pointer_for_bcp();
__ get_method(rbx);
__ jmp(profile_method_continue);
}
// Handle overflow of counter and compile method
__ bind(invocation_counter_overflow);
generate_counter_overflow(&continue_after_compile);
}
return entry_point;
}
// jvmti support
__ notify_method_entry();
__ dispatch_next(vtos);
这段代码继续往下执行字节码指令,进入到模板编译器阶段。