在前一篇 第1篇關(guān)于Java虛擬機(jī)HotSpot,開篇說的簡單些 中介紹了call_static()
、call_virtual()
等函數(shù)的作用,這些函數(shù)會調(diào)用JavaCalls::call()
函數(shù)。我們看Java
類中main()
方法的調(diào)用,
調(diào)用棧如下:
1
2
3
4
5
6
7
8
|
JavaCalls::call_helper() at javaCalls.cpp os::os_exception_wrapper() at os_linux.cpp JavaCalls::call() at javaCalls.cpp jni_invoke_static() at jni.cpp jni_CallStaticVoidMethod() at jni.cpp JavaMain() at java.c start_thread() at pthread_create.c clone() at clone.S |
這是Linux上的調(diào)用棧,通過JavaCalls::call_helper()
函數(shù)來執(zhí)行main()
方法。棧的起始函數(shù)為clone()
,這個函數(shù)會為每個進(jìn)程(Linux進(jìn)程對應(yīng)著Java線程)創(chuàng)建單獨的棧空間,這個棧空間如下圖所示。
在Linux操作系統(tǒng)上,棧的地址向低地址延伸,所以未使用的棧空間在已使用的棧空間之下。圖中的每個藍(lán)色小格表示對應(yīng)方法的棧幀,而棧就是由一個一個的棧幀組成。native
方法的棧幀、Java解釋棧幀和Java編譯棧幀都會在***域中分配,所以說他們寄生在宿主棧中,這些不同的棧幀都緊密的挨在一起,所以并不會產(chǎn)生什么空間碎片這類的問題,而且這樣的布局非常有利于進(jìn)行棧的遍歷。上面給出的調(diào)用棧就是通過遍歷一個一個棧幀得到的,遍歷過程也是棧展開的過程。后續(xù)對于異常的處理、運(yùn)行jstack打印線程堆棧、GC查找根引用等都會對棧進(jìn)行展開操作,所以棧展開是后面必須要介紹的。
下面我們繼續(xù)看JavaCalls::call_helper()
函數(shù),這個函數(shù)中有個非常重要的調(diào)用,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// do call { JavaCallWrapper link(method, receiver, result, CHECK); { HandleMark hm(thread); // HandleMark used by HandleMarkCleaner StubRoutines::call_stub()( (address)&link, result_val_address, result_type, method(), entry_point, args->parameters(), args->size_of_parameters(), CHECK ); result = link.result(); // Preserve oop return value across possible gc points if (oop_result_flag) { thread->set_vm_result((oop) result->get_jobject()); } } } |
調(diào)用StubRoutines::call_stub()函
數(shù)返回一個函數(shù)指針,然后通過函數(shù)指針來調(diào)用函數(shù)指針指向的函數(shù)。通過函數(shù)指針調(diào)用和通過函數(shù)名調(diào)用的方式一樣,這里我們需要清楚的是,調(diào)用的目標(biāo)函數(shù)仍然是C/C++函數(shù),所以由C/C++函數(shù)調(diào)用另外一個C/C++函數(shù)時,要遵守調(diào)用約定。這個調(diào)用約定會規(guī)定怎么給被調(diào)用函數(shù)(Callee)傳遞參數(shù),以及被調(diào)用函數(shù)的返回值將存儲在什么地方。
下面我們就來簡單說說Linux X86架構(gòu)下的C/C++函數(shù)調(diào)用約定,在這個約定下,以下寄存器用于傳遞參數(shù):
- 第1個參數(shù):rdi c_rarg0
- 第2個參數(shù):rsi c_rarg1
- 第3個參數(shù):rdx c_rarg2
- 第4個參數(shù):rcx c_rarg3
- 第5個參數(shù):r8 c_rarg4
- 第6個參數(shù):r9 c_rarg5
在函數(shù)調(diào)用時,6個及小于6個用如下寄存器來傳遞,在HotSpot
中通過更易理解的別名c_rarg*來使用對應(yīng)的寄存器。如果參數(shù)超過六個,那么程序?qū)谜{(diào)用棧來傳遞那些額外的參數(shù)。
數(shù)一下我們通過函數(shù)指針調(diào)用時傳遞了幾個參數(shù)?8個,那么后面的2個就需要通過調(diào)用函數(shù)(Caller)
的棧來傳遞,這兩個參數(shù)就是args->size_of_parameters()
和CHECK
(這是個宏,擴(kuò)展后就是傳遞線程對象)。
所以我們的調(diào)用棧在調(diào)用函數(shù)指針指向的函數(shù)時,變?yōu)榱巳缦聽顟B(tài):
右邊是具體的call_helper()
棧幀中的內(nèi)容,我們把thread
和parameter size
壓入了調(diào)用棧中,其實在調(diào)目標(biāo)函數(shù)的過程還會開辟新的棧幀并在parameter size
后壓入返回地址和調(diào)用棧的棧底,下一篇我們再詳細(xì)介紹。先來介紹下JavaCalls::call_helper()
函數(shù)的實現(xiàn),我們分3部分依次介紹。
1、檢查目標(biāo)方法是否"首次執(zhí)行前就必須被編譯”,是的話調(diào)用JIT編譯器去編譯目標(biāo)方法;
代碼實現(xiàn)如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
void JavaCalls::call_helper( JavaValue* result, methodHandle* m, JavaCallArguments* args, TRAPS ) { methodHandle method = *m; JavaThread* thread = (JavaThread*)THREAD; ... assert (!thread->is_Compiler_thread(), "cannot compile from the compiler" ); if (CompilationPolicy::must_be_compiled(method)) { CompileBroker::compile_method(method, InvocationEntryBci, CompilationPolicy::policy()->initial_compile_level(), methodHandle(), 0 , "must_be_compiled" , CHECK); } ... } |
對于main()
方法來說,如果配置了-Xint選項,則是以解釋模式執(zhí)行的,所以并不會走上面的compile_method()
函數(shù)的邏輯。后續(xù)我們要研究編譯執(zhí)行時,可以強(qiáng)制要求進(jìn)行編譯執(zhí)行,然后查看執(zhí)行過程。
2、獲取目標(biāo)方法的解釋模式入口from_interpreted_entry
,也就是entry_point
的值。獲取的entry_point
就是為Java方法調(diào)用準(zhǔn)備棧楨,并把代碼調(diào)用指針指向method
的第一個字節(jié)碼的內(nèi)存地址。entry_point
相當(dāng)于是method
的封裝,不同的method
類型有不同的entry_point
。
接著看call_helper()函數(shù)的代碼實現(xiàn),如下:
1
|
address entry_point = method->from_interpreted_entry(); |
調(diào)用method
的from_interpreted_entry()
函數(shù)獲取Method
實例中_from_interpreted_entry
屬性的值,這個值到底在哪里設(shè)置的呢?我們后面會詳細(xì)介紹。
3、調(diào)用call_stub()函數(shù),需要傳遞8個參數(shù)。這個代碼在前面給出過,這里不再給出。下面我們詳細(xì)介紹一下這幾個參數(shù),如下:
-
(1)link 此變量的類型為
JavaCallWrapper
,這個變量對于棧展開過程非常重要,后面會詳細(xì)介紹; -
(2)
result_val_address
函數(shù)返回值地址; -
(3)
result_type
函數(shù)返回類型; -
(4)
method()
當(dāng)前要執(zhí)行的方法。通過此參數(shù)可以獲取到Java方法所有的元數(shù)據(jù)信息,包括最重要的字節(jié)碼信息,這樣就可以根據(jù)字節(jié)碼信息解釋執(zhí)行這個方法了; -
(5)en
try_point HotSpot
每次在調(diào)用Java函數(shù)時,必然會調(diào)用CallStub
函數(shù)指針,這個函數(shù)指針的值取自_call_stub_entry,HotSpot
通過_call_stub_entry
指向被調(diào)用函數(shù)地址。在調(diào)用函數(shù)之前,必須要先經(jīng)過entry_point
,HotSpot實際是通過entry_point從method()
對象上拿到Java方法對應(yīng)的第1個字節(jié)碼命令,這也是整個函數(shù)的調(diào)用入口; -
(6)
args->parameters()
描述Java函數(shù)的入?yún)⑿畔ⅲ?/li> -
(7)
args->size_of_parameters()
參數(shù)需要占用的,以字為單位的內(nèi)存大小 - (8)CHECK 當(dāng)前線程對象。
到此這篇關(guān)于Java虛擬機(jī)調(diào)用Java主類的main()方法的文章就介紹到這了,更多相關(guān)Java虛擬機(jī)調(diào)用main()方法內(nèi)容請搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://www.heapdump.cn/article/2866671