前言
本文繼續(xù)總結(jié)輝哥的第二周二三節(jié)課的總結(jié)辈双。這兩節(jié)課主要還是對JVM引擎執(zhí)行,Gradle進(jìn)行了初步的介紹和學(xué)習(xí)馏段。
注意正文中所有的對虛擬機(jī)的討論都是基于Android 7.0 art虛擬機(jī)轩拨。
如果遇到什么問題歡迎來到本文:http://www.reibang.com/p/084b636c5cc4 互相討論
正文
JVM 引擎執(zhí)行
JVM引擎是如何執(zhí)行的。這里我們以art虛擬機(jī)為例子院喜。先來看看整個核心原理圖亡蓉。
首先JVM執(zhí)行代碼分為如下兩種:
1.解釋執(zhí)行,通俗一點(diǎn)的話語就是一遍執(zhí)行一遍翻譯字節(jié)碼够坐。常常我們會把這種執(zhí)行方式和JIT(Just In time)聯(lián)系起來說寸宵。JIT是解釋執(zhí)行的核心機(jī)制,當(dāng)某個方法執(zhí)行的次數(shù)達(dá)到了閾值(也就是閾值)就會翻譯成機(jī)器碼
2.機(jī)器碼執(zhí)行 直接是機(jī)器碼進(jìn)行本地執(zhí)行元咙。其中一個核心的機(jī)制梯影,也就是我們常說的AOT(Ahead Of Time), 運(yùn)行前編譯。也就是在安裝的時候庶香,把代碼經(jīng)過PMS的DexManager 跨進(jìn)程執(zhí)行dex2oat翻譯成優(yōu)化過的本地機(jī)器碼甲棍,可以被機(jī)器識別。
對于ART虛擬機(jī)來說赶掖,其實整個過程較為復(fù)雜感猛,但是值得稱贊的是,整個流程的封裝設(shè)計做的不錯奢赂,不糾結(jié)底層的細(xì)節(jié)理解起來沒有想象的困難陪白。
說起方法執(zhí)行,就必須提及一個概念膳灶,棧幀咱士。
ShadowFrame棧幀
棧幀(Stack Frame)是用于支持虛擬機(jī)方法執(zhí)行和調(diào)用的數(shù)據(jù)結(jié)構(gòu),他是虛擬機(jī)運(yùn)行時在數(shù)據(jù)區(qū)中虛擬機(jī)棧的棧元素轧钓。棧幀保存了方法的局部變量表序厉,操作數(shù)棧,動態(tài)鏈接和方法返回地址和一些額外信息毕箍。
當(dāng)一個方法從調(diào)用到運(yùn)行完畢弛房,實際上是一個棧幀從入棧到出棧的過程。
在編譯執(zhí)行過程中而柑,會在Class文件中保存好Code字段中記錄好這個方法需要多少個局部變量文捶,多少操作棧,方法的參數(shù)個數(shù)牺堰。通過這些數(shù)據(jù)就能決定了在當(dāng)前虛擬機(jī)環(huán)境下拄轻,當(dāng)前棧幀內(nèi)存大小。
一個線程中虛擬機(jī)棧的棧幀可能很長伟葫,只有在棧頂?shù)臈攀腔钴S的狀態(tài)恨搓,成為當(dāng)前棧幀。與這個棧幀關(guān)聯(lián)的方法稱為當(dāng)前方法筏养。
單單這么說概念斧抱,可能會不太明白。這里我們就挑出Android 7.0對應(yīng)的虛擬機(jī)中的對應(yīng)的數(shù)據(jù)結(jié)構(gòu)來進(jìn)一步闡述棧幀為何物:
機(jī)器碼執(zhí)行相關(guān)的邏輯較為復(fù)雜和抽象渐溶,這里來聊聊解釋執(zhí)行中棧幀的原理辉浦。
在ART虛擬機(jī)中,棧幀指代的數(shù)據(jù)結(jié)構(gòu)為ShadowFrame
茎辐。
private:
ShadowFrame(uint32_t num_vregs, ShadowFrame* link, ArtMethod* method,
uint32_t dex_pc, bool has_reference_array)
: link_(link), method_(method), result_register_(nullptr), dex_pc_ptr_(nullptr),
code_item_(nullptr), number_of_vregs_(num_vregs), dex_pc_(dex_pc) {
// TODO(iam): Remove this parameter, it's an an artifact of portable removal
DCHECK(has_reference_array);
if (has_reference_array) {
memset(vregs_, 0, num_vregs * (sizeof(uint32_t) + sizeof(StackReference<mirror::Object>)));
} else {
memset(vregs_, 0, num_vregs * sizeof(uint32_t));
}
}
...
// Link to previous shadow frame or null.
ShadowFrame* link_;
ArtMethod* method_;
JValue* result_register_;
const uint16_t* dex_pc_ptr_;
const DexFile::CodeItem* code_item_;
LockCountData lock_count_data_; // This may contain GC roots when lock counting is active.
const uint32_t number_of_vregs_;
uint32_t dex_pc_;
int16_t cached_hotness_countdown_;
int16_t hotness_countdown_;
// This is a two-part array:
// - [0..number_of_vregs) holds the raw virtual registers, and each element here is always 4
// bytes.
// - [number_of_vregs..number_of_vregs*2) holds only reference registers. Each element here is
// ptr-sized.
// In other words when a primitive is stored in vX, the second (reference) part of the array will
// be null. When a reference is stored in vX, the second (reference) part of the array will be a
// copy of vX.
uint32_t vregs_[0];
DISALLOW_IMPLICIT_CONSTRUCTORS(ShadowFrame);
};
能看到宪郊,ShadowFrame
本質(zhì)上是被ManagerStack
管理所有的棧幀掂恕。
class PACKED(4) ManagedStack {
public:
ManagedStack()
: top_quick_frame_(nullptr), link_(nullptr), top_shadow_frame_(nullptr) {}
void PushManagedStackFragment(ManagedStack* fragment) {
// Copy this top fragment into given fragment.
memcpy(fragment, this, sizeof(ManagedStack));
// Clear this fragment, which has become the top.
memset(this, 0, sizeof(ManagedStack));
// Link our top fragment onto the given fragment.
link_ = fragment;
}
void PopManagedStackFragment(const ManagedStack& fragment) {
DCHECK(&fragment == link_);
// Copy this given fragment back to the top.
memcpy(this, &fragment, sizeof(ManagedStack));
}
...
private:
ArtMethod** top_quick_frame_;
ManagedStack* link_;
ShadowFrame* top_shadow_frame_;
};
ManagerStack
托管著一個當(dāng)前正在運(yùn)行的ShadowFrame
。并能通過ManagedStack
中的link 對象知道上一次線程切換時候的幀布局弛槐,從而進(jìn)行切換懊亡。
棧幀的創(chuàng)建與虛擬機(jī)的內(nèi)存劃分
上文說到棧幀屬于哪個數(shù)據(jù)區(qū)域。 我們回顧一下c++ 編程中的內(nèi)存四驅(qū)模型:
- 1.棧區(qū):由編譯器自動分配釋放乎串,存放函數(shù)參數(shù)梢什,局部變量角寸。
- 2.堆區(qū):由編程者手動分配釋放肠缔,如果不釋放則造成內(nèi)存泄露忿项,只有進(jìn)程被銷毀時,才被OS回收
- 3.數(shù)據(jù)區(qū):存放全局變量长豁,靜態(tài)變量钧唐,常量字符等。只能被OS回收
- 4.代碼區(qū)域:存放代碼的區(qū)域
在java 虛擬機(jī)中內(nèi)存的劃分如下:
- 1.方法區(qū)
- 2.虛擬機(jī)棧
- 3.本地方法棧
- 4.堆
- 5.程序計數(shù)器
這一塊我們通常稱為運(yùn)行時數(shù)據(jù)區(qū)匠襟,也就是JVM內(nèi)存逾柿。
在這一塊內(nèi)存中又分為共享內(nèi)存,和線程私有內(nèi)存:
- 共享內(nèi)存: 方法區(qū)宅此,堆
- 線程私有內(nèi)存:虛擬機(jī)棧机错,本地方法棧,程序計數(shù)
我們現(xiàn)在只討論JVM在Android的行為父腕。實際上JVM的內(nèi)存劃分必定是建立在c++編程中四驅(qū)模型的架構(gòu)上弱匪。
為了徹底弄懂棧幀的的內(nèi)存申請,讓我們把JVM和四驅(qū)模型聯(lián)系起來吧璧亮。
apk編譯產(chǎn)物
先聊聊apk安裝編譯產(chǎn)物萧诫,當(dāng)dexManager通過dex2oat進(jìn)程把dex轉(zhuǎn)化為如下幾種文件:
dex 是把jar包(內(nèi)含class文件)經(jīng)過壓縮后的產(chǎn)物
odex 則是在Android 5.0之前常用的,是App安裝時通過dex2opt對apk包中dex文件進(jìn)行了優(yōu)化產(chǎn)物枝嘶,之后運(yùn)行App就加載odex文件帘饶,加速App響應(yīng)時間
oat 是在Android 5.0之后,安裝時候會對dex文件中的數(shù)據(jù)通過dex2oat翻譯成本地機(jī)器碼保存起來群扶。本質(zhì)上是一個elf格式的文件
art 是Android安裝后生成的可選對象及刻,一旦生成了則會獲取odex中部分常用內(nèi)容(長方法等)存儲到elf文件中。并保存到ImageSpace中竞阐,通過這種方式加速App的運(yùn)行效率缴饭。
- vdex 是Android 8.0之后的產(chǎn)物。當(dāng)dex經(jīng)過了編譯的校驗后骆莹,則會把校驗后代碼方法都緩存到vdex中颗搂。
而這幾個文件,最終都會加載到內(nèi)存中幕垦,對于Android高版本中丢氢,我們來討論oat文件以及art文件傅联。
art文件的內(nèi)存從屬
來看看art文件通過JVM加載到內(nèi)存是一個什么形式:
art文件實際上在App應(yīng)用啟動初期進(jìn)行初始化JVM時候,會加載到一個為ImageSpace的內(nèi)存空間中疚察。這個內(nèi)存實際上是屬于c/c++四驅(qū)的堆纺且,也就是說是通過malloc的方式為ImageSpace申請內(nèi)存。
當(dāng)然稍浆,ImageSpace中存在兩個十分重要字段:mem_map
和live_bitmap
。這兩個實際上是通過mmap
系統(tǒng)調(diào)用把a(bǔ)rt文件中重要的數(shù)據(jù)字段映射到內(nèi)存中猜嘱。
其中mem_map
映射了art中的方法衅枫,屬性,類表等核心對象朗伶。
當(dāng)生成了ImageSpace后弦撩,會把這個對象托付給JVM Heap對象進(jìn)行管理,而這個對象就是JVM中堆:
而堆本質(zhì)上也是通過malloc 從內(nèi)存中申請出來的论皆,也是屬于c/c++四驅(qū)模型的堆一份子益楼。
oat文件的內(nèi)存從屬
art文件只是作為輔助的作用,大部分的核心代碼都是由oat文件進(jìn)行管理点晴。從apk安裝產(chǎn)物一圖可以得知感凤,oat文件的結(jié)構(gòu)粗略是可以分2個區(qū)域:象征Oat文件頭部的OatHeader
,以及象征Oat文件的內(nèi)容單元OatDexFile
。OatDexFile
在Oat中是數(shù)組的單元對象粒督,他象征著被壓縮到oat
文件中的dex文件陪竿。
這里面的結(jié)構(gòu)稍微有點(diǎn)復(fù)雜,但是還是可以一窺究竟:
1.OatHeader 帶了Oat文件的魔數(shù)屠橄,版本號等數(shù)據(jù)族跛,當(dāng)然也指向了
Trampoline code
也就是只有boot.oat(framework.jar對應(yīng)的oat)文件才有的直接調(diào)準(zhǔn)方法2.OatDexFile 則是Dex文件本身經(jīng)過翻譯后,壓縮到文件中的數(shù)據(jù)锐墙〗负澹可以從圖中可以看到內(nèi)含了dex文件對應(yīng)的偏移量,class對應(yīng)的偏移量
3.DexFile OatDexFile 指向的dex偏移量就是指的
DexFile
對象溪北,這個對象其實指代了dex文件4.TypeLookupTable 是一個哈希表桐绒,這個哈希表目的是快速通過類名找到dex對應(yīng)class數(shù)組的偏移索引
5.ClassOffsets 這是這個class二維偏移數(shù)組是指,第幾個dex的第幾個class
6.OatClass 則是ClassOffsets 指向的class對象之拨。其中包含了當(dāng)前的class的狀態(tài)(加載掏膏,校驗,準(zhǔn)備敦锌,解析馒疹,初始化),當(dāng)前class的編譯類型(是全部轉(zhuǎn)化成機(jī)器碼乙墙,還是部分轉(zhuǎn)化機(jī)器碼颖变,還是壓根沒經(jīng)過轉(zhuǎn)化)生均,class中的方法等等
7.VmapTable 是指CompileMethod 中的
vmap_table_
數(shù)組。CompileMethod
是dex2oat 進(jìn)程處理dex中java方法的結(jié)果對象腥刹。其中CompileMethod
的vmap_table_
定長數(shù)組是一個映射表马胧,反映了機(jī)器碼用到的物理寄存器到dex中虛擬寄存器的映射關(guān)系.8.OatQuickMethodHeader數(shù)組 指向所有的Oat文件的方法,內(nèi)含當(dāng)前方法所需要的棧幀大小衔峰,vmap的偏移量佩脊,代碼段數(shù)據(jù)結(jié)構(gòu)的偏移量等信息。
這些所有的數(shù)據(jù)最終都會加載到四驅(qū)模型的堆內(nèi)存中垫卤。
art和oat的關(guān)系
能看到實際上在上一片文章提到過的ArtMethod在這里可以看到關(guān)聯(lián)威彰。首先art文件加載進(jìn)來的ArtMethod結(jié)構(gòu)體內(nèi)含一個核心的字段ptr_sized_fields
的entry_point_from_quick_compiled_code_
指向真正保存代碼段的數(shù)據(jù)結(jié)構(gòu),也就是加載到內(nèi)存的Oat文件中的OatQuickMethod
的code
數(shù)組字段穴肘。
通過這樣層層遞進(jìn)歇盼,才能找到方法中真正的執(zhí)行邏輯。
方法區(qū)
方法區(qū)和堆是可以進(jìn)行JVM內(nèi)跨線程共享评抚,那么肯定是一些運(yùn)行時不變的數(shù)據(jù)豹缀。
方法區(qū)是用于存放class二進(jìn)制文件數(shù)據(jù),內(nèi)含虛擬機(jī)的類信息慨代,靜態(tài)常量邢笙,常量數(shù)據(jù),編譯后代碼數(shù)據(jù)侍匙。
有了上一節(jié)的基礎(chǔ)知識后鸣剪,就能清晰的明白方法區(qū)實際上就是指加載到四驅(qū)模型的堆中。
而實際上在JVM中丈积,我們常說的堆并非是c/c++編程的堆筐骇,而是從四驅(qū)模型的堆中申請一塊對象名為Heap的才是Java堆。
堆
對于Java語言來說江滨,堆特指的是JVM虛擬機(jī)生成的Heap對象铛纬。JVM全局有且只有一個。所有的對象都會保存在堆的位圖中唬滑。每一個對象實際上都是屬于四驅(qū)模型中的堆告唆。
肯定有的人覺得疑惑,那在方法內(nèi)申請出來的對象不應(yīng)該屬于在棧中嗎晶密?從我們編程的角度看來似乎如此擒悬。實際上在解析字節(jié)碼并不會聯(lián)系代碼段的上下文,而是一個指令進(jìn)行一種操作稻艰。
當(dāng)發(fā)現(xiàn)new的操作的時候懂牧,就是NEW_INSTANCE
和NEW_ARRAY
申請對象和數(shù)組時候,都會調(diào)用不同內(nèi)存管理器的Alloc方法尊勿,從提前malloc出來的內(nèi)存區(qū)域獲取對象僧凤。
一個虛擬機(jī)會根據(jù)啟動配置而生成不同的內(nèi)存管理器畜侦,而不同的內(nèi)存管理器就是會有不同的內(nèi)存回收策略(GC),如標(biāo)記-清除算法(MarkAndSwap),標(biāo)記-壓縮算法等躯保。
關(guān)于內(nèi)存分配與回收這部分細(xì)節(jié)旋膳,之后會有專題解析。
那么又衍生出一個新的問題途事,棧是如何管理局部變量的验懊。
虛擬機(jī)棧
關(guān)于虛擬機(jī)棧就是我們說的java方法的棧。上文已經(jīng)介紹了在解釋執(zhí)行中尸变,棧幀實際上指代的是ShadowFrame
對象义图。這個對象對應(yīng)的內(nèi)存實際上是申請在棧中,如下圖:
能看到ShadowFrame本質(zhì)上是通過placement new 方式為一個對象申請內(nèi)存振惰。這種特殊的方式會從棧中申請內(nèi)存。
在虛擬棧中垄懂,有這個4個核心對象構(gòu)成:
- 1.局部變量表 用于存儲方法中的局部變量
- 2.操作數(shù)棧 進(jìn)行邏輯操作時候會對臨時變量進(jìn)行緩存骑晶,一旦一個操作需要完成則會彈出操作數(shù)棧
- 3.動態(tài)鏈表 是指把常量池中的數(shù)據(jù)鏈接到對應(yīng)的字段中
- 4.返回地址 方法返回地址
要徹底理解這4個對象,我們需要進(jìn)一步的了解dex指令碼草慧。
dex指令碼
dex指令碼和java字節(jié)碼有著不少的區(qū)別桶蛔。可以這么看這兩者的關(guān)系漫谷,java編程生成jar包對應(yīng)的class文件此時內(nèi)容是java字節(jié)碼仔雷。而在Android編譯apk時候,則會取出這些代碼數(shù)據(jù)舔示,壓縮成dex字節(jié)碼碟婆。其中會把所有的java字節(jié)碼進(jìn)行轉(zhuǎn)化壓縮成dex字節(jié)碼。dex字節(jié)碼內(nèi)容格式更小更加利于apk的包體積大小惕稻。
dex字節(jié)碼更少竖共,但是相對的記錄的信息多了。
class Test {
private static int a = 88;
private static String name = "1111";
public static void test(String a){
int b = 127;
Log.e("aaa",b+"");
}
}
只看Code字段對應(yīng)的字節(jié)碼:
com.yjy.hellotest.Test();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 14: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/yjy/hellotest/Test;
public static void test(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: bipush 127
2: istore_1
3: ldc #2 // String aaa
5: new #3 // class java/lang/StringBuilder
8: dup
9: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
12: iload_1
13: invokevirtual #5 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
16: ldc #6 // String
18: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: invokestatic #9 // Method android/util/Log.e:(Ljava/lang/String;Ljava/lang/String;)I
27: pop
28: return
LineNumberTable:
line 19: 0
line 20: 3
line 21: 28
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 a Ljava/lang/String;
3 26 1 b I
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 88
2: putstatic #10 // Field a:I
5: ldc #11 // String 1111
7: putstatic #12 // Field name:Ljava/lang/String;
10: return
LineNumberTable:
line 15: 0
line 16: 5
}
再來看看apk包壓縮之后俺祠,取出其中的dex文件公给,調(diào)用dexdump
命令打印dex中所有的內(nèi)容,比較兩者之間的區(qū)別蜘渣。
Class #1027 -
Class descriptor : 'Lcom/yjy/hellotest/Test;'
Access flags : 0x0000 ()
Superclass : 'Ljava/lang/Object;'
Interfaces -
Static fields -
#0 : (in Lcom/yjy/hellotest/Test;)
name : 'a'
type : 'I'
access : 0x000a (PRIVATE STATIC)
#1 : (in Lcom/yjy/hellotest/Test;)
name : 'name'
type : 'Ljava/lang/String;'
access : 0x000a (PRIVATE STATIC)
Instance fields -
Direct methods -
#0 : (in Lcom/yjy/hellotest/Test;)
name : '<clinit>'
type : '()V'
access : 0x10008 (STATIC CONSTRUCTOR)
code -
registers : 1
ins : 0
outs : 0
insns size : 9 16-bit code units
11e854: |[11e854] com.yjy.hellotest.Test.<clinit>:()V
11e864: 1300 5800 |0000: const/16 v0, #int 88 // #58
11e868: 6700 9b2b |0002: sput v0, Lcom/yjy/hellotest/Test;.a:I // field@2b9b
11e86c: 1a00 df01 |0004: const-string v0, "1111" // string@01df
11e870: 6900 9c2b |0006: sput-object v0, Lcom/yjy/hellotest/Test;.name:Ljava/lang/String; // field@2b9c
11e874: 0e00 |0008: return-void
catches : (none)
positions :
0x0000 line=15
0x0004 line=16
locals :
#1 : (in Lcom/yjy/hellotest/Test;)
name : '<init>'
type : '()V'
access : 0x10000 (CONSTRUCTOR)
code -
registers : 1
ins : 1
outs : 1
insns size : 4 16-bit code units
11e878: |[11e878] com.yjy.hellotest.Test.<init>:()V
11e888: 7010 243c 0000 |0000: invoke-direct {v0}, Ljava/lang/Object;.<init>:()V // method@3c24
11e88e: 0e00 |0003: return-void
catches : (none)
positions :
0x0000 line=14
locals :
0x0000 - 0x0004 reg=0 this Lcom/yjy/hellotest/Test;
#2 : (in Lcom/yjy/hellotest/Test;)
name : 'test'
type : '(Ljava/lang/String;)V'
access : 0x0009 (PUBLIC STATIC)
code -
registers : 4
ins : 1
outs : 2
insns size : 25 16-bit code units
11e890: |[11e890] com.yjy.hellotest.Test.test:(Ljava/lang/String;)V
11e8a0: 1300 7f00 |0000: const/16 v0, #int 127 // #7f
11e8a4: 2201 9007 |0002: new-instance v1, Ljava/lang/StringBuilder; // type@0790
11e8a8: 7010 573c 0100 |0004: invoke-direct {v1}, Ljava/lang/StringBuilder;.<init>:()V // method@3c57
11e8ae: 6e20 5d3c 0100 |0007: invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;.append:(I)Ljava/lang/StringBuilder; // method@3c5d
11e8b4: 1a02 0000 |000a: const-string v2, "" // string@0000
11e8b8: 6e20 613c 2100 |000c: invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; // method@3c61
11e8be: 6e10 663c 0100 |000f: invoke-virtual {v1}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String; // method@3c66
11e8c4: 0c01 |0012: move-result-object v1
11e8c6: 1a02 711e |0013: const-string v2, "aaa" // string@1e71
11e8ca: 7120 ac05 1200 |0015: invoke-static {v2, v1}, Landroid/util/Log;.e:(Ljava/lang/String;Ljava/lang/String;)I // method@05ac
11e8d0: 0e00 |0018: return-void
catches : (none)
positions :
0x0000 line=19
0x0002 line=20
0x0018 line=21
locals :
0x0002 - 0x0019 reg=0 b I
0x0000 - 0x0019 reg=3 a Ljava/lang/String;
Virtual methods -
source_file_idx : 6825 (Test.java)
這部分內(nèi)容最終都會壓縮在Dex文件的CodeItem結(jié)構(gòu)體中淌铐。我們來看看靜態(tài)構(gòu)造函數(shù):
Code:
stack=1, locals=0, args_size=0
0: bipush 88
2: putstatic #10 // Field a:I
5: ldc #11 // String 1111
7: putstatic #12 // Field name:Ljava/lang/String;
10: return
bipush
把88壓入到操作數(shù)棧中,putstatic
則從操作數(shù)棧中彈出棧頂設(shè)置到a字段中蔫缸。
我們來看看對應(yīng)的dex字節(jié)碼做了什么腿准?
Direct methods -
#0 : (in Lcom/yjy/hellotest/Test;)
name : '<clinit>'
type : '()V'
access : 0x10008 (STATIC CONSTRUCTOR)
code -
registers : 1
ins : 0
outs : 0
insns size : 9 16-bit code units
11e854: |[11e854] com.yjy.hellotest.Test.<clinit>:()V
11e864: 1300 5800 |0000: const/16 v0, #int 88 // #58
11e868: 6700 9b2b |0002: sput v0, Lcom/yjy/hellotest/Test;.a:I // field@2b9b
11e86c: 1a00 df01 |0004: const-string v0, "1111" // string@01df
11e870: 6900 9c2b |0006: sput-object v0, Lcom/yjy/hellotest/Test;.name:Ljava/lang/String; // field@2b9c
11e874: 0e00 |0008: return-void
此時就能看到不同了,此時bipush
java字節(jié)碼轉(zhuǎn)化成const_16
dex字節(jié)碼拾碌,并把88壓倒操作棧中释涛,最后通過sput
dex字節(jié)碼把88從操作數(shù)棧彈出加叁,設(shè)置到靜態(tài)字段a中。
操作數(shù)棧唇撬,局部變量表內(nèi)存劃分
當(dāng)有了上一節(jié)的基礎(chǔ)知識后它匕,我們進(jìn)一步的來討論操作數(shù)棧和局部變量在其中的所屬的內(nèi)存已經(jīng)作用。
我們回顧圖ShadowFrame家族
窖认,這個圖中詳細(xì)的列舉了ShadowFrame
幾個核心的對象豫柬。
vregs
這個int數(shù)組就是我們常說的操作數(shù)棧以及局部變量表。很奇妙是吧扑浸。
總的來說烧给,java字節(jié)碼可以分為如下4類:
- 1.
*load_*
這種格式的字節(jié)碼代表從本地變量表加載到操作棧 - 2.
*store_*
則是把一個數(shù)值從操作棧讀取到本地變量表中 - 3.
*ipush
,ldc_*
,*const_*
是把一個常量讀取到操作棧中。 - 4.
putstatic
等 把數(shù)值彈出操作棧并進(jìn)行后續(xù)的邏輯操作喝噪,如賦值給某個對象
而dex字節(jié)碼础嫡,經(jīng)過獲取到j(luò)ava字節(jié)碼后,就能獲得更加相信的信息酝惧,從而轉(zhuǎn)化為更加準(zhǔn)確dex字節(jié)碼榴鼎。此時只有一個引用表 vregs
。這個引用表可以作為局部變量以及操作數(shù)棧晚唇。其實這么設(shè)計也是可行的巫财,無論是局部變量表還是操作數(shù)棧都是在運(yùn)行字節(jié)碼和解釋執(zhí)行時候的臨時結(jié)果的緩存內(nèi)存,只是hotspot虛擬機(jī)對這兩者的職責(zé)劃分更加詳細(xì)哩陕,一個表控制局部變量平项,另一個表控制邏輯操作臨時結(jié)果。
因此操作數(shù)棧和局部變量表都是屬于四驅(qū)內(nèi)存的棧區(qū)域悍及。
程序計數(shù)器
回顧圖ShadowFrame家族
.實際上程序計數(shù)器就是指shadowFrame中的dex_pc
闽瓢。每當(dāng)出現(xiàn)打斷的時候,就會終止一個ShadowFrame
解析執(zhí)行其中的CodeItem
.此時就會記錄當(dāng)前運(yùn)行的行數(shù)的dex_pc
心赶,當(dāng)下一次回到當(dāng)前的ShadowFrame
繼續(xù)執(zhí)行鸳粉,就會跳到dex_pc
繼續(xù)執(zhí)行。
一般的园担,打斷當(dāng)前ShadowFrame
的執(zhí)行有如下幾種情況:
- 1.從解釋執(zhí)行跳到機(jī)器碼執(zhí)行
- 2.跳轉(zhuǎn)到c/c++本地庫的代碼中執(zhí)行
- 3.因時間片輪轉(zhuǎn)届谈,而切換線程執(zhí)行方法
由于dex_pc
也是屬于ShadowFrame
的一個字段,因此也是屬于棧區(qū)域
本地方法棧
本地方法棧弯汰,可以看成就是c/c++四驅(qū)模型中的棧區(qū)域艰山,并沒有特指什么具體的對象。直接執(zhí)行c/c++中的代碼塊咏闪。
方法執(zhí)行原理
理解了棧幀ShadowFrame
之后曙搬,再來看看在ART虛擬機(jī)中,方法是如何執(zhí)行的。
方法想要正確執(zhí)行纵装,就需要知道當(dāng)前方法的所屬的類征讲。從下圖:
可以的得知,ART虛擬機(jī)在加載Class的時候會查找所有的屬性和方法橡娄。在LoadMethod步驟的時候就會加載整個class中所有的方法和字段的信息诗箍。當(dāng)然此時只是當(dāng)前class的信息。
方法存儲
只有在LinkMethod和LinkField步驟的時候挽唉,才會把父類和接口的信息全部解析出來并且保存在vtable
滤祖,embedded_table
,methods_
,iftable
中。
其中可實例化對象只存在embedded_table
否則只存在iftable
.
methods_
保存了所有的方法瓶籽。
embedded_table
和iftable
只保存了接口方法匠童。可實例化對象可以通過embedded_table
快速查找當(dāng)前類實現(xiàn)的接口方法塑顺。
方法執(zhí)行
當(dāng)查找到方法后汤求,就能確定這是否是一個重寫的方法。如果是重寫方法就會調(diào)用該類中所存儲的對應(yīng)方法严拒。也就是說重寫是在編譯時候決定扬绪。
如果這是一個重載方法,只有在運(yùn)行時候知道是此時需要調(diào)用的參數(shù)是什么糙俗?通過方法描述符從而決定是調(diào)用哪個方法勒奇。
方法是如何運(yùn)行的预鬓,可以先來看看這個后半段的示意圖巧骚。
整個核心流程可以分為如下幾個步驟:
- 1.整個過程都會流轉(zhuǎn)到
artQuickToInterpreterBridge
中進(jìn)行集中處理。一旦虛擬機(jī)調(diào)用到這個方法后格二,就會從棧中生成一個棧幀對象劈彪,并把當(dāng)前這個棧幀對象放到ManagerStack中管理。
此時就ManagerStack決定了當(dāng)前棧幀為哪個線程的ShadowFrame
.注意看下面一塊的示意圖顶猜,在線程的私有內(nèi)存中沧奴,保存著ManagerStack
對象,這個對象保存的top_shadow_frame_
就是我們常說的當(dāng)前棧幀长窄。
2.ExecuteSwitchImpl 這個方法就會開始解析
ShadowFrame
棧幀中所對應(yīng)的代碼塊滔吠,也就是dex指令流。同時會記錄當(dāng)前的指令調(diào)用到了哪一行挠日,也就是程序計數(shù)器疮绷。3.當(dāng)判斷到不是機(jī)器碼,是解釋執(zhí)行的時候嚣潜。就會調(diào)用
ArtInterpreterToInterpreterBridge
方法繼續(xù)通過ExecuteSwitchImpl
解析下一個棧幀的指令流4.當(dāng)判斷到是機(jī)器碼冬骚,則調(diào)用
ArtInterpreterToCompiledCodeBirdge
,執(zhí)行機(jī)器碼5.調(diào)用ManagerStack的pop方法,把執(zhí)行完畢的當(dāng)前棧幀彈出只冻。
小結(jié)
本文對JVM引擎的方法執(zhí)行做了一個初步的總結(jié)庇麦。能看到實際上我并沒有帶領(lǐng)大家一行行的看看ART虛擬機(jī)整體流程。是因為虛擬機(jī)的內(nèi)容實在太多喜德,沒有一個初步的概念對全局有一個認(rèn)識山橄,會迷失在其中。加上本文也是對輝哥課程進(jìn)行的一次小總結(jié)住诸,以及自己學(xué)習(xí)過的只是進(jìn)行的擴(kuò)展驾胆。
先到這里,我們對JVM是如何執(zhí)行方法的原理明白后贱呐,我們就可以開啟Gradle的總結(jié)丧诺。