開篇就提到效能優(yōu)化涉及的范圍會很廣晋柱,考慮后面需要經(jīng)常用到 asm 字節(jié)碼插樁壁晒,我們首先從 《Gradle 插件 + ASM 實戰(zhàn)》開始講,但又希望大家能知其然也知其所以然疗琉,因此我們首先得講下 JVM 虛擬機加載 Class 字節(jié)碼的原理冈欢。這往往也是我面試新同學必問的一個內(nèi)容,因為如果對這個不了解的話没炒,像插件化與熱修復涛癌、性能優(yōu)化、覆蓋率統(tǒng)計等等很多功能都是不好實現(xiàn)的送火。小公司很少有人用,這也是實話先匪,至于大家要不要學种吸,這就看個人情況了,其實也不是用不用得上的問題呀非,就看大家愿不愿意做一個吃螃蟹的人坚俗。我們主要從以下三個方面來說:
1. class 文件字節(jié)碼結(jié)構(gòu)
1.1 class 字節(jié)碼示例
我們先來看一個非常簡單的 HelloWorld.java
public class HelloWorld {
public HelloWorld() {
}
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
用文本編輯器打開生成的 HelloWorld.class 文件镜盯,是這樣的:
cafe babe 0000 0033 0022 0a00 0600 1409
0015 0016 0800 170a 0018 0019 0700 1a07
001b 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 264c 636f 6d2f 6578
616d 706c 652f 6d79 6170 706c 6963 6174
696f 6e2f 4865 6c6c 6f57 6f72 6c64 3b01
0004 6d61 696e 0100 1628 5b4c 6a61 7661
2f6c 616e 672f 5374 7269 6e67 3b29 5601
0004 6172 6773 0100 135b 4c6a 6176 612f
6c61 6e67 2f53 7472 696e 673b 0100 0a53
6f75 7263 6546 696c 6501 000f 4865 6c6c
6f57 6f72 6c64 2e6a 6176 610c 0007 0008
0700 1c0c 001d 001e 0100 0c48 656c 6c6f
2057 6f72 6c64 2107 001f 0c00 2000 2101
0024 636f 6d2f 6578 616d 706c 652f 6d79
6170 706c 6963 6174 696f 6e2f 4865 6c6c
6f57 6f72 6c64 0100 106a 6176 612f 6c61
6e67 2f4f 626a 6563 7401 0010 6a61 7661
2f6c 616e 672f 5379 7374 656d 0100 036f
7574 0100 154c 6a61 7661 2f69 6f2f 5072
696e 7453 7472 6561 6d3b 0100 136a 6176
612f 696f 2f50 7269 6e74 5374 7265 616d
0100 0770 7269 6e74 6c6e 0100 1528 4c6a
6176 612f 6c61 6e67 2f53 7472 696e 673b
2956 0021 0005 0006 0000 0000 0002 0001
0007 0008 0001 0009 0000 002f 0001 0001
0000 0005 2ab7 0001 b100 0000 0200 0a00
0000 0600 0100 0000 0a00 0b00 0000 0c00
0100 0000 0500 0c00 0d00 0000 0900 0e00
0f00 0100 0900 0000 3700 0200 0100 0000
09b2 0002 1203 b600 04b1 0000 0002 000a
0000 000a 0002 0000 000c 0008 000d 000b
0000 000c 0001 0000 0009 0010 0011 0000
0001 0012 0000 0002 0013
好家伙,這怎么能夠看得懂猖败?但是既然 java 虛擬機能夠看懂速缆,我們也可以想辦法看懂,用 javap -verbose HelloWorld.class 看起來就稍微簡單一點:
Last modified 2021-1-7; size 586 bytes
MD5 checksum bf91e508b76a0dc7d4c0250b0e55f75b
Compiled from "HelloWorld.java"
public class com.example.myapplication.HelloWorld
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello World!
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/example/myapplication/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/example/myapplication/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello World!
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/example/myapplication/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public com.example.myapplication.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/myapplication/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 12: 0
line 13: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
1.2 類文件結(jié)構(gòu)
.class 文件是一組以 8 位字節(jié)為基礎(chǔ)單位的二進制流恩闻,各數(shù)據(jù)項目嚴格按照順序緊湊地排列在 .class 文件中艺糜,中間沒有添加任何分隔符,這使得整個 .class 文件中存儲的內(nèi)容幾乎全都是程序需要的數(shù)據(jù)幢尚,沒有空隙存在破停。至于具體有哪些內(nèi)容,這里有一張表大家可以參考尉剩。
虛擬機加載 .class 文件真慢,就是按照上面這樣的規(guī)則去解析,最終解析的結(jié)果大致就是 javap -verbose 命令所生成的那樣理茎,如果大家只是閱讀文章的話黑界,建議大家自己要一點一點去嘗試解析下,當然直播上我會帶大家一起來看皂林。
2. jvm 類的加載機制
2.1 類的加載時機
在 JVM 虛擬機規(guī)范中并沒有規(guī)定加載的時機朗鸠,但是卻規(guī)定了初始化的時機,有以下五種情況需要必須立即對類進行初始化:
- 遇到 new式撼、getstatic童社、putstatic 或 invokestatic 這 4 條字節(jié)碼指令時,如果類沒有進行過初始化著隆,則需要先觸發(fā)其初始化扰楼。生成這 4 條指令最常見的 Java 代碼場景是:使用 new 關(guān)鍵字實例化對象、讀取或設(shè)置一個類的靜態(tài)字段(被 final 修飾美浦、已在編譯期把結(jié)果放入到常量池的靜態(tài)字段除外)以及調(diào)用一個類的靜態(tài)方法的時候
- 使用 java.lang.reflect 包的方法對類進行反射調(diào)用的時候
- 當初始化一個類的時候弦赖,如果發(fā)現(xiàn)其父類還沒有被初始化過,則需要先觸發(fā)其父類的初始化
- 當虛擬機啟動時浦辨,用戶需要指定一個要執(zhí)行的主類(包含 main() 方法的類)蹬竖,虛擬機會先初始化這個主類
- 當使用 JDK 1.7 的動態(tài)語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最后的解析結(jié)果 REF_getStatic流酬、REF_putStatic币厕、REF_invodeStatic 的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進行過初始化芽腾,則需要先觸發(fā)其初始化旦装。
2.2 類的加載流程
類的加載過程大致分為 5 個步驟:加載、驗證摊滔、準備阴绢、解析和初始化店乐,作為過來人早期我犯過很嚴重的錯誤,那就是為了面試習慣背呻袭,這樣過段時間發(fā)現(xiàn)很容易忘記眨八,而且開發(fā)中遇到類似的問題往往不知所措,因此希望大家能好好的理解理解左电,這樣才能做到一勞永逸:
2.2.1 加載
- 通過一個類的全限定名獲取定義此類的二進制字節(jié)流
- 將二進制字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)中的運行時數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個代表此類的 java.lang.Class 的對象廉侧,作為方法區(qū)中這個類的訪問入口
- jvm 虛擬機并沒有規(guī)定從哪里獲取二進制字節(jié)流。我們可以從 .class 靜態(tài)存儲文件中獲取券腔,也可以從 apk伏穆、zip、jar 等包中讀取纷纫,可以從數(shù)據(jù)庫中讀取枕扫,也可以從網(wǎng)絡(luò)中獲取,甚至我們自己可以在運行時自動生成辱魁。
- 在內(nèi)存中實例化一個代表此類的 java.lang.Class 對象之后烟瞧,并沒有規(guī)定此 Class 對象是方法 Java 堆中的,有些虛擬機就會將 Class 對象放到方法區(qū)中染簇,比如 HotSpot参滴,一個 ClassLoader 只會實例化一個 Class 對象。
2.2.2 驗證
- 文件格式驗證:主要驗證二進制字節(jié)流數(shù)據(jù)是否符合 .class 文件的規(guī)范锻弓,并且該 .class 文件是否在本虛擬機的處理范圍之內(nèi)(版本號驗證)砾赔。只有通過了文件格式的驗證之后,二進制的字節(jié)流才會進入到內(nèi)存中的方法區(qū)進行存儲青灼。而且只有通過了文件格式驗證之后暴心,才會進行后面三個驗證,后面三個驗證都是基于方法區(qū)中的存儲結(jié)構(gòu)進行的
- 元數(shù)據(jù)驗證:主要是對類的元數(shù)據(jù)信息進行語義檢查杂拨,保證不存在不符合 Java 語義規(guī)范的元數(shù)據(jù)信息
- 字節(jié)碼驗證:字節(jié)碼驗證是整個驗證中最復雜的一個過程专普,在元數(shù)據(jù)驗證中,驗證了元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗后弹沽,字節(jié)碼驗證主要對類的方法體進行校驗分析檀夹,保證被校驗的類的方法不會做出危害虛擬機的行為
- 符號引用驗證:符號引用驗證發(fā)生在連接的第三個階段解析階段中,主要是保證解析過程可以正確地執(zhí)行策橘。符號引用驗證是類本身引用的其他類的驗證炸渡,包括:通過一個類的全限定名是否可以找到對應(yīng)的類,訪問的其他類中的字段和方法是否存在丽已,并且訪問性是否合適等
2.2.3 準備
- 在方法區(qū)中分配內(nèi)存的只有類變量(被 static 修飾的變量)偶摔,而不包括實例變量,實例變量將會跟隨著對象在 Java 堆中為其分配內(nèi)存
- 初始化類變量的時候促脉,是將類變量初始化為其類型對應(yīng)的 0 值辰斋,比如有如下類變量,在準備階段完成之后瘸味,val 的值是 0 而不是設(shè)置宫仗,為 val 復制為具體值,是在初始化階段
- 對于常量旁仿,其對應(yīng)的值會在編譯階段就存儲在字段表的 ConstantValue 屬性當中藕夫,所以在準備階段結(jié)束之后,常量的值就是 ConstantValue 所指定的值了枯冈。
2.2.4 解析
- 虛擬機規(guī)范中并未規(guī)定解析階段發(fā)生的具體時間毅贮,只規(guī)定了在執(zhí)行newarray、new尘奏、putfidle滩褥、putstatic、getfield炫加、getstatic 等 16 個指令之前瑰煎,對它們所使用的符號引用進行解析。所以虛擬機可以在類被加載器加載之后就進行解析俗孝,也可以在執(zhí)行這幾個指令之前才進行解析
- 對同一個符號引用進行多次解析是很常見的事酒甸,除 invokedynamic 指令以外,虛擬機實現(xiàn)可以對第一次解析的結(jié)果進行緩存赋铝,以后解析相同的符號引用時插勤,只要取緩存的結(jié)果就可以了
- 解析動作主要對類或接口、字段革骨、類方法农尖、接口方法、方法類型苛蒲、方法句柄和調(diào)用點限定符 7 類符號引用進行解析
2.2.5 初始化
- 類構(gòu)造器 <clinit>() 是由編譯器自動收集類中出現(xiàn)的類變量卤橄、靜態(tài)代碼塊中的語句合并產(chǎn)生的,收集的順序是在源文件中出現(xiàn)的順序決定的臂外,靜態(tài)代碼塊可以訪問出現(xiàn)在靜態(tài)代碼塊之前的類變量窟扑,出現(xiàn)的靜態(tài)代碼塊之后的類變量,只可以賦值漏健,但是不能訪問嚎货。
- <clinit>() 類構(gòu)造器和<init>()實例構(gòu)造器不同,類構(gòu)造器不需要顯示的父類的類構(gòu)造蔫浆,在子類的類構(gòu)造器調(diào)用之前殖属,會自動的調(diào)用父類的類構(gòu)造器。因此虛擬機中第一個被調(diào)用的 <clinit>() 方法是 java.lang.Object 的類構(gòu)造器
- 由于父類的類構(gòu)造器優(yōu)先于子類的類構(gòu)造器執(zhí)行瓦盛,所以父類中的 static{} 代碼塊也優(yōu)先于子類的 static{} 執(zhí)行
- 類構(gòu)造器<clinit>() 對于類來說并不是必需的洗显,如果一個類中沒有類變量外潜,也沒有 static{},那這個類不會有類構(gòu)造器 <clinit>()
- 接口中不能有 static{}挠唆,但是接口中也可以有類變量处窥,所以接口中也可以有類構(gòu)造器 <clinit>{},但是接口的類構(gòu)造器和類的類構(gòu)造器有所不同玄组,接口在調(diào)用類構(gòu)造器的時候滔驾,如果不需要,不用調(diào)用父接口的類構(gòu)造器俄讹,除非用到了父接口中的類變量哆致,接口的實現(xiàn)類在初始化的時候也不會調(diào)用接口的類構(gòu)造器
- 虛擬機會保證一個類的 <clinit>() 方法在多線程環(huán)境中被正確地加鎖、同步患膛,如果多個線程同時去初始化一個類摊阀,那么只有一個線程去執(zhí)行這個類的類構(gòu)造器 <clinit>(),其他線程會被阻塞剩瓶,直到活動線程執(zhí)行完類構(gòu)造器 <clinit>() 方法
2.3 雙親委派模型
雙親委派模型驹溃,我們看一下 ClassLoader 的源碼就能明白了,我們公司的 Shadow 就是利用這個點來做插件類加載的延曙,來公司后我自主學習看的第一個源碼就是 Shadow 豌鹤,順便打個廣告 Shadow 是一個騰訊自主研發(fā)的 Android 插件框架,經(jīng)過線上億級用戶量檢驗枝缔。 Shadow 不僅開源分享了插件技術(shù)的關(guān)鍵代碼布疙,還完整的分享了上線部署所需要的所有設(shè)計。與市面上其他插件框架相比愿卸,Shadow 主要具有以下特點:
- 復用獨立安裝App的源碼:插件App的源碼原本就是可以正常安裝運行的灵临。
- 零反射無 Hack 實現(xiàn)插件技術(shù):從理論上就已經(jīng)確定無需對任何系統(tǒng)做兼容開發(fā),更無任何隱藏 API 調(diào)用趴荸,和 Google 限制非公開 SDK 接口訪問的策略完全不沖突儒溉。
- 全動態(tài)插件框架:一次性實現(xiàn)完美的插件框架很難,但 Shadow 將這些實現(xiàn)全部動態(tài)化起來发钝,使插件框架的代碼成為了插件的一部分顿涣。插件的迭代不再受宿主打包了舊版本插件框架所限制。
- 宿主增量極性秃馈:得益于全動態(tài)實現(xiàn)涛碑,真正合入宿主程序的代碼量極小(15KB孵淘,160方法數(shù)左右)蒲障。
Kotlin 實現(xiàn):core.loader,core.transform 核心代碼完全用 Kotlin 實現(xiàn),代碼簡潔易維護揉阎。
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
// 是否已經(jīng)被加載了
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
// 先從 parent 中加載
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
// 最后再從 this 加載
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
3. jvm 虛擬機執(zhí)行引擎
了解了 .class 里面有啥庄撮,了解了 .class 怎么被解析加載,最后自然得了解下字節(jié)碼命令是怎么執(zhí)行的余黎。在這之前我們先得了解兩個概念重窟,什么是棧幀?什么是分派惧财?
3.1 棧幀
棧幀(Stack Frame)是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機運行時數(shù)據(jù)區(qū)中的虛擬機棧(Virtual Machine Stack)的棧元素扭仁。棧幀存儲了方法的局部變量表垮衷、操作數(shù)棧、動態(tài)連接和方法返回地址等信息乖坠。每一個方法從調(diào)用開始至執(zhí)行完成的過程搀突,都對應(yīng)著一個棧幀在虛擬機棧里面從入棧到出棧的過程。每一個棧幀都包括了局部變量表熊泵、操作數(shù)棧仰迁、動態(tài)連接、方法返回地址和一些額外的附加信息顽分。在編譯程序代碼的時候徐许,棧幀中需要多大的局部變量表,多深的操作數(shù)棧都已經(jīng)完全確定了卒蘸,并且寫入到方法表的 Code 屬性之中雌隅,因此一個棧幀需要分配多少內(nèi)存,不會受到程序運行期變量數(shù)據(jù)的影響缸沃,而僅僅取決于具體的虛擬機實現(xiàn)恰起。一個線程中的方法調(diào)用鏈可能會很長,很多方法都同時處于執(zhí)行狀態(tài)趾牧。對于執(zhí)行引擎來說检盼,在活動線程中,只有位于棧頂?shù)臈攀怯行У那痰ィQ為當前棧幀(Current Stack Frame)吨枉,與這個棧幀相關(guān)聯(lián)的方法稱為當前方法(Current Method),執(zhí)行引擎運行的所有字節(jié)碼指令都只針對當前棧幀進行操作县恕。
3.2 分派
分派調(diào)用有可能是靜態(tài)的东羹,也有可能是動態(tài)的,我們?nèi)绻斫饬诉@個忠烛,就會知道 Java 中的多態(tài)性是怎么實現(xiàn)的属提,像“重載”和“重寫”等。Java 虛擬機識別方法的關(guān)鍵在于類名、方法名以及方法描述符冤议。前面兩個就不做過多的解釋了斟薇,至于方法描述符,它是由方法的參數(shù)類型以及返回類型所構(gòu)成恕酸。在同一個類中堪滨,如果同時出現(xiàn)多個名字相同且描述符也相同的方法,那么 Java 虛擬機會在類的驗證階段報錯蕊温。
可以看到袱箱,Java 虛擬機與 Java 語言不同,它并不限制名字與參數(shù)類型相同义矛,但返回類型不同的方法出現(xiàn)在同一個類中发笔,對于調(diào)用這些方法的字節(jié)碼來說,由于字節(jié)碼所附帶的方法描述符包含了返回類型凉翻,因此 Java 虛擬機能夠準確地識別目標方法了讨。
靜態(tài)分派指的是在解析時便能夠直接識別目標方法的情況,而動態(tài)分派則指的是需要在運行過程中根據(jù)調(diào)用者的動態(tài)類型來識別目標方法的情況制轰。Java 虛擬機中其實是不存在重載概念的前计,因為在編譯期間我們就能確定需要執(zhí)行那個方法,如果非得區(qū)分那就是:重載被稱為靜態(tài)綁定或者編譯時多態(tài)垃杖;而重寫則被稱為動態(tài)綁定男杈。確切地說,Java 虛擬機中的靜態(tài)分派指的是在解析時便能夠直接識別目標方法的情況缩滨,而動態(tài)分派則指的是需要在運行過程中根據(jù)調(diào)用者的動態(tài)類型來識別目標方法的情況势就。Java 虛擬機執(zhí)行方法一般有五種指令:
- invokestatic:用于調(diào)用靜態(tài)方法。
- invokespecial:用于調(diào)用私有實例方法脉漏、構(gòu)造器苞冯,以及使用 super 關(guān)鍵字調(diào)用父類的實例方法或構(gòu)造器,和所實現(xiàn)接口的默認方法侧巨。
- invokevirtual:用于調(diào)用非私有實例方法舅锄。
- invokeinterface:用于調(diào)用接口方法。
- invokedynamic:用于調(diào)用動態(tài)方法司忱。
3.3 實例
有了這兩個概念后皇忿,我們就需要來看一個具體的實例了:
public class HelloWorld {
public static void main(String[] args){
int num1 = 100;
int num2 = 200;
int sum = sum(num1, num2);
System.out.println("sum = "+sum);
}
private static final int sum(int num1, int num2){
return num1 + num2;
}
}
javap -verbose HelloWorld.class:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: bipush 100
2: istore_1
3: sipush 200
6: istore_2
7: iload_1
8: iload_2
9: invokestatic #2 // Method sum:(II)I
12: istore_3
13: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
16: new #4 // class java/lang/StringBuilder
19: dup
20: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
23: ldc #6 // String sum =
25: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: iload_3
29: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
32: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
35: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
38: return
LineNumberTable:
line 12: 0
line 13: 3
line 14: 7
line 15: 13
line 16: 38
LocalVariableTable:
Start Length Slot Name Signature
0 39 0 args [Ljava/lang/String;
3 36 1 num1 I
7 32 2 num2 I
13 26 3 sum I
這個理解是比較重要的,雖然我們在后面講 asm 的時候會有傻瓜式操作坦仍,但是能不能理解怎么寫為什么要那么寫鳍烁,就靠我們對著每一條指令集的理解了。我們需要知道每個指令代表的是什么意思繁扎,比如 bipush 100 代表把數(shù)字 100 壓入棧中幔荒,istore_1 代表把剛壓入棧的 100 放到局部變量表中糊闽。我們需要清楚的知道每運行一個指令,當前棧和局部變量表中的數(shù)據(jù)是怎樣變化的爹梁。
本文基本都是文字原理右犹,大家要有耐心,如果能夠理解其實是非常簡單的東西姚垃。這本身是三四次課的內(nèi)容念链,我把其壓縮到了一兩次課來講』矗考慮到大家的水平不一掂墓,很多同學可能會感覺沒有講到位,因此大家可以去找些額外文章用來輔助理解絮宁,但是大的方向肯定是這個方向梆暮。