Java Main 如何是如何被執行的?
java應用程式的啟動在在/hotspot/src/share/tools/launcher/java.c的main()函式中,而在虛擬機器初始化過程中,將建立並啟動Java的Main執行緒。最後將呼叫JNIEnv的CallStaticVoidMethod()來執行main方法。
CallStaticVoidMethod()對應的jni函式為jni_CallStaticVoidMethod,定義在/hotspot/src/share/vm/prims/jni.cpp中,而jni_CallStaticVoidMethod()又呼叫了jni_invoke_static(),jni_invoke_static()透過JavaCalls的call()發起對Java方法的呼叫
所有來自虛擬機器對Java函式的呼叫最終都將由JavaCalls模組來完成,JavaCalls將透過call_helper()來執行Java方法並返回撥用結果,並最終呼叫StubRoutines::call_stub()來執行Java方法:
// 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 ); 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()); } } }
call_stub()定義在/hotspot/src/share/vm/runtime/stubRoutines.h中,實際上返回的就 是CallStub函式指標_call_stub_entry,該指標指向call_stub的彙編實現的目的碼指令地址,即call_stub的例程 入口。
// Calls to Java typedef void (*CallStub)( address link, intptr_t* result, BasicType result_type, methodOopDesc* 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); }
在分析call_stub的彙編程式碼之前,先了解下x86暫存器和棧幀以及函式呼叫的相關知識。
x86-64的所有暫存器都是與 機器字長(資料匯流排位寬)相同,即64位的,x86-64將x86的8個32位通用暫存器擴充套件為64位(eax、ebx、ecx、edx、eci、 edi、ebp、esp),並且增加了8個新的64位暫存器(r8-r15),在命名方式上,也從”exx”變為”rxx”,但仍保留”exx”進行32 位操作,下表描述了各暫存器的命名和作用
此外,還有16個128位的XMM暫存器,分別為xmm0-15,x84-64的暫存器遵循呼叫約定(Calling Conventions):
1.引數傳遞:
(1).前4個引數的int型別分別透過rcx、rdx、r8、r9傳遞,多餘的在棧空間上傳遞(從右向左依次入棧),暫存器所有的引數都是向右對齊的(低位對齊)
(2).型別的引數透過xmm0-xmm3傳遞,注意不同型別的引數佔用的暫存器序號是根據引數的序號來決定的,比如add(int,double,float,int)就分別儲存在rcx、xmm1、xmm2、r9暫存器中
(3).8/16/32/64型別的結構體或共用體和_m64型別將使用rcx、rdx、r8、r9直接傳遞,而其他型別將會透過指標引用的方式在這4個暫存器中傳遞
(4).被呼叫函式當需要時要把暫存器中的引數移動到棧空間中(shadow space)
2.返回值傳遞
(1).對於可以填充為64位的返回值(包括_m64)將使用rax進行傳遞
(2).對於_m128(i/d)以及浮點數型別將使用xmm0傳遞
(3).對於64位以上的返回值,將由呼叫函式在棧上為其分配空間,並將其指標儲存在rcx中作為”第一個引數”,而傳入引數將依次右移,最後函式呼叫完後,由rax返回該空間的指標
(4).使用者定義的返回值型別長度必須是1、2、4、8、16、32、64
3.呼叫者/被呼叫者儲存暫存器
呼叫者儲存暫存器:rax、rcx、rdx、r8-r11都認為是易失型暫存器(),這些暫存器隨時可能被用到,這些暫存器將由呼叫者 自行維護,當呼叫其他函式時,被呼叫函式對這些暫存器的操作並不會影響呼叫函式(即這些暫存器的作用範圍僅限於當前函式)。
被呼叫者儲存寄 存器:rbx、rbp、rdi、rsi、r12-r15、xmm6-xmm15都是非易失型暫存器(non-volatile),呼叫其他函式時,這些寄 存器的值可能在呼叫返回時還需要用,那麼被呼叫函式就必須將這些暫存器的值儲存起來,當要返回時,恢復這些暫存器的值(即這些暫存器的作用範圍是跨函式調 用的)。
以如下程式為例,分析函式呼叫的棧幀佈局:
double func(int param_i1, float param_f1, double param_d1, int param_i2, double param_d2) { int local_i1, local_i2; float local_f1; double local_d1; double local_d2 = 3.0; local_i1 = param_i1; local_i2 = param_i2; local_f1 = param_f1; local_d1 = param_d1; return local_d1 + local_f1 * (local_i2 - local_i1) - param_d2 + local_d2; } int main() { double res; res = func(1, 1.0, 2.0, 3, 3.0); return 0; }
main函式呼叫func之前的彙編程式碼如下:
main: pushq %rbp //儲存rbp .seh_pushreg %rbp movq %rsp, %rbp //更新棧基址 .seh_setframe %rbp, 0 subq $80, %rsp .seh_stackalloc 80 //main棧需要80位元組的棧空間 .seh_endprologue call __main movabsq $4611686018427387904, %rdx //0x4000000000000000,即浮點數2.0 movabsq $4613937818241073152, %rax //0x3000000000000000,即浮點數3.0 movq %rax, 32(%rsp) //第5個引數3.0,即param_d2儲存在棧空間上 movl $3, %r9d //第4個引數3,即param_i2儲存在r9d中(r9的低32位) movq %rdx, -24(%rbp) movsd -24(%rbp), %xmm2 //第3個引數2.0,即param_d1儲存在xmm2中 movss .LC2(%rip), %xmm1 //第2個引數1.0(0x3f800000),儲存在xmm1中 movl $1, %ecx //第1個引數1,儲存在ecx中(rcx的低32位) call func
func函式返回後,main函式將從xmm0中取出返回結果
call func movq %xmm0, %rax //儲存結果 movq %rax, -8(%rbp) movl $0, %eax //清空eax,回收main棧,恢復棧頂地址 addq $80, %rsp popq %rbp ret
func函式的棧和運算元準備如下:
func: pushq %rbp //儲存rbp(main函式棧的基址) .seh_pushreg %rbp movq %rsp, %rbp //將main棧的棧頂指標作為被呼叫函式的棧基址 .seh_setframe %rbp, 0 subq $32, %rsp //func棧需要32位元組的棧空間 .seh_stackalloc 32 .seh_endprologue movl %ecx, 16(%rbp) //將4個引數移動到棧底偏移16-40的空間(main棧的shadow space) movss %xmm1, 24(%rbp) movsd %xmm2, 32(%rbp) movl %r9d, 40(%rbp) movabsq $4613937818241073152, %rax //本地變數local_d2,即浮點數3.0 movq %rax, -8(%rbp) //5個區域性變數 movl 16(%rbp), %eax movl %eax, -12(%rbp) movl 40(%rbp), %eax movl %eax, -16(%rbp) movl 24(%rbp), %eax movl %eax, -20(%rbp) movq 32(%rbp), %rax movq %rax, -32(%rbp)
隨後的func的運算過程如下:
movl -16(%rbp), %eax //local_i2 - local_i1 subl -12(%rbp), %eax pxor %xmm0, %xmm0 //準備xmm0暫存器,按位異或,xmm0清零 cvtsi2ss %eax, %xmm0 mulss -20(%rbp), %xmm0 //local_f1 * (local_i2 - local_i1) cvtss2sd %xmm0, %xmm0 addsd -32(%rbp), %xmm0 //local_d1 + local_f1 * (local_i2 - local_i1) subsd 48(%rbp), %xmm0 //local_d1 + local_f1 * (local_i2 - local_i1) - param_d2 addsd -8(%rbp), %xmm0 //local_d1 + local_f1 * (local_i2 - local_i1) - param_d2 + local_d2 addq $32, %rsp //回收func棧,恢復棧頂地址 popq %rbp ret
根據以上程式碼分析,大概得出該程式呼叫棧結構:
這裡沒有考慮func函式再次呼叫其他函式而準備運算元的棧內容的情況,但結合main函式棧,大致可以得出棧的通用結構如下:
call_stub由generate_call_stub()解釋成彙編程式碼,有興趣的可以繼續閱讀call_stub的彙編程式碼進行分析。
下面對call_stub的彙編部分進行分析:
先來看下call_stub的呼叫棧結構:(注:本文實驗是在windows_64位平臺上實現的)
// Call stubs are used to call Java from C // return_from_Java 是緊跟在call *%eax後面的那條指令的地址 // [ return_from_Java ]1.根據函式呼叫棧的結構:
在被調函式棧幀的棧底 %rbp + 8(棧地址向下增長,堆地址向上增長,棧底的正偏移值指向呼叫函式棧幀內容)儲存著被調函式的傳入引數,這裡即:
JavaCallWrapper指標、返回結果指標、返回結果型別、被呼叫方法的methodOop、被呼叫方法的解釋程式碼的入口地址、引數地址、引數個數。
StubRoutines::call_stub [0x0000000002400567, 0x00000000024006cb[ (356 bytes) //儲存bp 0x0000000002400567: push %rbp //更新棧頂地址 0x0000000002400568: mov %rsp,%rbp //call_stub需要的棧空間大小為0xd8 0x000000000240056b: sub $0xd8,%rsp2.rcx、rdx、r8d、r9d分別儲存著傳入call_stub的前4個引數,現在需要將其複製到棧上的shadow space中
//分別使用rcx、rdx、r8、r9來儲存第1、2、3、4個引數,多出來的其他引數用棧空間來傳遞 //使用xmm0-4來傳遞第1-4個浮點數引數 //這裡將引數複製到棧空間,這樣call_stub的所有引數就在rbp + 0x10 ~ 0x48棧空間上 0x0000000002400572: mov %r9,0x28(%rbp) 0x0000000002400576: mov %r8d,0x20(%rbp) 0x000000000240057a: mov %rdx,0x18(%rbp) 0x000000000240057e: mov %rcx,0x10(%rbp)3.將被呼叫者儲存暫存器的值壓入call_stub棧中:
;; save registers: //依次儲存rbx、rsi、rdi這三個被呼叫者儲存的暫存器,隨後儲存r12-r15、XMM暫存器組xmm6-xmm15 0x0000000002400582: mov %rbx,-0x8(%rbp) 0x0000000002400586: mov %r12,-0x20(%rbp) 0x000000000240058a: mov %r13,-0x28(%rbp) 0x000000000240058e: mov %r14,-0x30(%rbp) 0x0000000002400592: mov %r15,-0x38(%rbp) 0x0000000002400596: vmovdqu %xmm6,-0x48(%rbp) 0x000000000240059b: vmovdqu %xmm7,-0x58(%rbp) 0x00000000024005a0: vmovdqu %xmm8,-0x68(%rbp) 0x00000000024005a5: vmovdqu %xmm9,-0x78(%rbp) 0x00000000024005aa: vmovdqu %xmm10,-0x88(%rbp) 0x00000000024005b2: vmovdqu %xmm11,-0x98(%rbp) 0x00000000024005ba: vmovdqu %xmm12,-0xa8(%rbp) 0x00000000024005c2: vmovdqu %xmm13,-0xb8(%rbp) 0x00000000024005ca: vmovdqu %xmm14,-0xc8(%rbp) 0x00000000024005d2: vmovdqu %xmm15,-0xd8(%rbp) 0x00000000024005da: mov %rsi,-0x10(%rbp) 0x00000000024005de: mov %rdi,-0x18(%rbp) //棧底指標的0x48偏移儲存著thread物件,0x6d01a2c3(%rip)為異常處理入口 0x00000000024005e2: mov 0x48(%rbp),%r15 0x00000000024005e6: mov 0x6d01a2c3(%rip),%r12 # 0x000000006f41a8b04.call_stub的引數儲存著Java方法的引數,現在就需要將引數壓入call_stub棧中
/棧底指標的0x40偏移儲存著引數的個數 0x00000000024005ed: mov 0x40(%rbp),%r9d //若引數個數為0,則直接跳轉0x000000000240060d準備呼叫Java方法 0x00000000024005f1: test %r9d,%r9d 0x00000000024005f4: je 0x000000000240060d //若引數個數不為0,則遍歷引數,將所有引數壓入本地棧 //其中棧底指標的0x38偏移儲存著引數的地址,edx將用作迴圈的迭代器 0x00000000024005fa: mov 0x38(%rbp),%r8 0x00000000024005fe: mov %r9d,%edx ;; loop: //從第一個引數開始,將Java方法的引數壓人本地棧 /* * i = parameter_size; //確保不等於0 * do{ * push(parameter[i]); * i--; * }while(i!=0); */ 0x0000000002400601: mov (%r8),%rax 0x0000000002400604: add $0x8,%r8 0x0000000002400608: dec %edx 0x000000000240060a: push %rax 0x000000000240060b: jne 0x00000000024006015.呼叫Java方法的解釋程式碼
;; prepare entry: //棧底指標的0x28和0x30偏移分別儲存著被呼叫Java方法的methodOop指標和解釋程式碼的入口地址 0x000000000240060d: mov 0x28(%rbp),%rbx 0x0000000002400611: mov 0x30(%rbp),%rdx 0x0000000002400615: mov %rsp,%r13 //儲存棧頂指標 ;; jump to run Java method: 0x0000000002400618: callq *%rdx6.準備儲存返回結果,這裡需要先根據不同的返回型別取出返回結果,然後儲存到返回結果指標所指向的位置
;; prepare to save result: //棧底指標的0x18和0x20偏移分別儲存著返回結果的指標和結果型別 0x000000000240061a: mov 0x18(%rbp),%rcx 0x000000000240061e: mov 0x20(%rbp),%edx ;; handle result accord to different result_type: 0x0000000002400621: cmp $0xc,%edx 0x0000000002400624: je 0x00000000024006b7 0x000000000240062a: cmp $0xb,%edx 0x000000000240062d: je 0x00000000024006b7 0x0000000002400633: cmp $0x6,%edx 0x0000000002400636: je 0x00000000024006bc 0x000000000240063c: cmp $0x7,%edx 0x000000000240063f: je 0x00000000024006c2 ;; save result for the other result_type: 0x0000000002400645: mov %eax,(%rcx)下面分別為返回結果型別為long、float、double的情況
;; long 型別返回結果儲存: 0x00000000024006b7: mov %rax,(%rcx) 0x00000000024006ba: jmp 0x0000000002400647 ;; float 型別返回結果儲存: 0x00000000024006bc: vmovss %xmm0,(%rcx) 0x00000000024006c0: jmp 0x0000000002400647 ;; double 型別返回結果儲存: 0x00000000024006c2: vmovsd %xmm0,(%rcx) 0x00000000024006c6: jmpq 0x00000000024006477.被呼叫者儲存暫存器的恢復,以及棧指標的復位
;; restore registers: 0x0000000002400647: lea -0xd8(%rbp),%rsp 0x000000000240064e: vmovdqu -0xd8(%rbp),%xmm15 0x0000000002400656: vmovdqu -0xc8(%rbp),%xmm14 0x000000000240065e: vmovdqu -0xb8(%rbp),%xmm13 0x0000000002400666: vmovdqu -0xa8(%rbp),%xmm12 0x000000000240066e: vmovdqu -0x98(%rbp),%xmm11 0x0000000002400676: vmovdqu -0x88(%rbp),%xmm10 0x000000000240067e: vmovdqu -0x78(%rbp),%xmm9 0x0000000002400683: vmovdqu -0x68(%rbp),%xmm8 0x0000000002400688: vmovdqu -0x58(%rbp),%xmm7 0x000000000240068d: vmovdqu -0x48(%rbp),%xmm6 0x0000000002400692: mov -0x38(%rbp),%r15 0x0000000002400696: mov -0x30(%rbp),%r14 0x000000000240069a: mov -0x28(%rbp),%r13 0x000000000240069e: mov -0x20(%rbp),%r12 0x00000000024006a2: mov -0x8(%rbp),%rbx 0x00000000024006a6: mov -0x18(%rbp),%rdi 0x00000000024006aa: mov -0x10(%rbp),%rsi ;; back to old(caller) stack frame: 0x00000000024006ae: add $0xd8,%rsp //棧頂指標復位 0x00000000024006b5: pop %rbp //棧底指標復位 0x00000000024006b6: retq歸納出call_stub棧結構如下:
8.對於不同的Java方法,虛擬機器在初始化時會生成不同的方法入口例程
(method entry point)來準備棧幀,這裡以較常被使用的zerolocals方法入口為例,分析Java方法的棧幀結構與呼叫過程,入口例程目的碼的產生在InterpreterGenerator::generate_normal_entry()中:
(1).根據之前的分析,初始的棧結構如下:
獲取傳入引數數量到rcx中:
address InterpreterGenerator::generate_normal_entry(bool synchronized) { // determine code generation flags bool inc_counter = UseCompiler || CountCompiledCalls; // ebx: methodOop // r13: sender sp address entry_point = __ pc(); const Address size_of_parameters(rbx, methodOopDesc::size_of_parameters_offset()); const Address size_of_locals(rbx, methodOopDesc::size_of_locals_offset()); const Address invocation_counter(rbx, methodOopDesc::invocation_counter_offset() + InvocationCounter::counter_offset()); const Address access_flags(rbx, methodOopDesc::access_flags_offset()); // get parameter size (always needed) __ load_unsigned_short(rcx, size_of_parameters);其中methodOop指標被儲存在rbx中,呼叫Java方法的sender sp被儲存在r13中,引數大小儲存在rcx中
(2). 獲取區域性變數區的大小,儲存在rdx中,並減去引數數量,將除引數以外的區域性變數數量儲存在rdx中(雖然引數作為區域性變數是方法的一部分,但引數由呼叫 者提供,這些引數應有呼叫者棧幀而非被呼叫者棧幀維護,即被呼叫者棧幀只需要維護區域性變數中除了引數的部分即可)
// rbx: methodOop // rcx: size of parameters // 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(3).對棧空間大小進行檢查,判斷是否會發生棧溢位
// see if we've got enough room on the stack for locals plus overhead. generate_stack_overflow_check();(4).獲取返回地址,儲存在rax中(注意此時棧頂為呼叫函式call指令後下一條指令的地址)
// get return address __ pop(rax);(5).由於引數在棧中由低地址向高地址是以相反的順序存放的,所以第一個引數的地址應該是 rsp+rcx*8-8(第一個引數地址範圍為 rsp+rcx*8-8 ~ rsp+rcx*8),將其儲存在r14中
// compute beginning of parameters (r14) __ lea(r14, Address(rsp, rcx, Address::times_8, -wordSize))(6).為除引數以外的區域性變數分配棧空間,若這些區域性變數數量為0,那麼就跳過這一部分處理,否則,將壓入 maxlocals – param_size個0,以初始化這些區域性變數
//該部分為一個loop迴圈// 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這時棧的層次如下:
(7).將方法的呼叫次數儲存在rcx/ecx中
// (pre-)fetch invocation count if (inc_counter) { __ movl(rcx, invocation_counter); }(8).初始化當前方法的棧幀
// initialize fixed part of activation frame generate_fixed_frame(false);generate_fixed_frame()的實現如下:
__ push(rax); // save return address __ enter(); // save old & set new rbp __ push(r13); // set sender sp __ push((int)NULL_WORD); // leave last_sp as null __ movptr(r13, Address(rbx, methodOopDesc::const_offset())); // get constMethodOop __ lea(r13, Address(r13, constMethodOopDesc::codes_offset())); // get codebase __ push(rbx);儲存返回地址,為被呼叫的Java方法準備棧幀,並將sender sp指標、last_sp(設定為0)壓入棧,根據methodOop的constMethodOop成員將位元組碼指標儲存到r13暫存器中,並將methodOop壓入棧
} else { __ push(0); //methodData } __ movptr(rdx, Address(rbx, methodOopDesc::constants_offset())); __ movptr(rdx, Address(rdx, constantPoolOopDesc::cache_offset_in_bytes())); __ push(rdx); // set constant pool cache __ push(r14); // set locals pointer if (native_call) { __ push(0); // no bcp } else { __ push(r13); // set bcp } __ push(0); // reserve word for pointer to expression stack bottom __ movptr(Address(rsp, 0), rsp); // set expression stack bottom}將methodData以0為初始值壓入棧,根據methodOop的ConstantPoolOop成員將常量池緩衝地址壓入棧,r14中儲存著 區域性變數區(第一個引數的地址)指標,將其壓入棧,此外如果呼叫的是native呼叫,那麼位元組碼指標部分為0,否則正常將位元組碼指標壓入棧,最後為棧留 出一個字的表示式棧底空間,並更新rsp
最後棧的空間結構如下:
(9).增加方法的呼叫計數
// 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); } }(當呼叫深度過大會丟擲StackOverFlow異常)
(10).同步方法的Monitor物件分配和方法的加鎖(在彙編部分分析中沒有該部分,如果對同步感興趣的請自行分析)
if (synchronized) { // Allocate monitor and lock method lock_method();(11).JVM工具介面部分
// jvmti support __ notify_method_entry();(12).跳轉到第一條位元組碼的原生程式碼處執行
__ dispatch_next(vtos);以上分析可能略顯複雜,但重要的是明白方法的入口例程是如何為Java方法構造新的棧幀,從而為位元組碼的執行提供呼叫棧環境。
method entry point彙編程式碼的分析可以參考隨後的一篇文章。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/855/viewspace-2804035/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- SQL是如何執行的SQL
- CPU內部的奧秘:程式碼是如何被執行的?
- sql更新是如何執行的?SQL
- c++是如何執行的C++
- sql查詢是如何執行的?SQL
- 爬蟲代理是如何執行的?爬蟲
- 17.Sql是如何執行的SQL
- WinMain是如何被呼叫的AI
- 執行緒池中多餘的執行緒是如何回收的?執行緒
- Python是如何編譯執行的Python編譯
- 代理伺服器是如何執行的?伺服器
- 啟動一個最簡單的Java main程式時,有多少個執行緒被建立JavaAI執行緒
- 單執行緒的js是如何工作的執行緒JS
- JVM中的Hello World是如何執行的?JVM
- 多執行緒程式是如何執行程式碼的?執行緒行程
- 【Java面試】如何中斷一個正在執行的執行緒?Java面試執行緒
- Java 執行緒池中的執行緒複用是如何實現的?Java執行緒
- 24. 一個普通main方法的執行,是單執行緒模式還是多執行緒模式?為什麼?AI執行緒模式
- Java中如何保證執行緒順序執行Java執行緒
- 面試官:Java 執行緒如何啟動的?面試Java執行緒
- Java公式:如何執行字串表示式?!Java公式字串
- 如何在 Java 11 下執行 RocketMQJavaMQ
- AS執行main()方法報錯:SourceSet with name ‘main‘ not foundAI
- 一條update SQL語句是如何執行的SQL
- 一條SQL更新語句是如何執行的SQL
- MySQL:一條更新語句是如何執行的MySql
- 一條SQL更新語句是如何執行的?SQL
- 密碼是如何被竊取的密碼
- 如何在main函式前後執行程式碼AI函式行程
- 解讀Java8中ConcurrentHashMap是如何保證執行緒安全的JavaHashMap執行緒
- 一條更新的SQL語句是如何執行的?SQL
- Java執行緒面試題(02) Java執行緒中如何避免死鎖Java執行緒面試題
- Java 例項 - 如何檢視當前 Java 執行的版本?Java
- 如何優雅的關閉Java執行緒池Java執行緒
- Java中如何實現執行緒呢?Java執行緒
- Java如何測量方法執行時間Java
- Java如何獲取當前執行緒Java執行緒
- java編譯 Error: Could not find or load main class java執行包main方法Java編譯ErrorAI