Java ASM詳解:MethodVisitor和Opcode(二)類型、數(shù)組、字段、方法、異常與同步

上次講過了操作棧與數(shù)值運算操作,這篇專欄主要講ASM中有關(guān)于類型、數(shù)組與方法調(diào)用的字節(jié)碼。
P.S.ASM庫已經(jīng)更新到了9.2版本,可以試試解析Java 18的類了。.
一.有關(guān)于類型的字節(jié)碼
有關(guān)于類型的字節(jié)碼都是用visitTypeInsn進行寫入的。這類字節(jié)碼共有4個:NEW,ANEWARRAY,INSTANCEOF和CHECKCAST。ANEWARRAY在之后的數(shù)組字節(jié)碼里面會仔細去講。
[1. new]
NEW只進行創(chuàng)建對象,不負責(zé)調(diào)用構(gòu)造函數(shù),所以內(nèi)部字段的值都為默認值。調(diào)用構(gòu)造函數(shù)必須用invokespecial字節(jié)碼進行調(diào)用(下文)。
在調(diào)用這個字節(jié)碼時,如果指向的類沒有初始化,就它的調(diào)用靜態(tài)初始化函數(shù)<clinit>。如果在初始化中發(fā)生異常就會拋出錯誤。如果目標類的類格式有誤,則拋出異常。如果目標類時抽象的,則拋出InstantiationError
。
[2. instanceof]
instanceof用于檢查對象是否為這個類型的實例,如果是則返回boolean值true,即操作棧上的一個int數(shù)據(jù)1;如果不是就返回0。
對于null對象,該字節(jié)碼永遠返回0。
[3. checkcast]
checkcast用于檢查對象的類型,類似于instanceof。但不同的是,如果無法將對象轉(zhuǎn)換為指定類型,該字節(jié)碼會拋出ClassCastException。這個字節(jié)碼經(jīng)常見于泛型中。
加入這個字節(jié)碼通常是為了指定對象是某個類型好讓驗證器驗證,在局部變量無法得知確切類型時必須加入此字節(jié)碼保證驗證通過(運行時報錯就是另一回事了)。
下面是這三個字節(jié)碼組合的例子:
要生成的Java代碼如下:
對應(yīng)的生成這段代碼的字節(jié)碼程序如下:
二.數(shù)組操作的字節(jié)碼
數(shù)組操作的字節(jié)碼一共有20個,其中加載指令8個,存儲指令8個,三個創(chuàng)建還有一個獲取數(shù)組長度的字節(jié)碼。
[1. newarray]
和newarray字節(jié)碼用于創(chuàng)建基本類型的數(shù)組,它的參數(shù)代表了它的類型,在Opcodes類中一共有8個:T_BOOLEAN(boolean),T_CHAR(char),T_FLOAT(float),T_DOUBLE(double),T_BYTE(byte),T_SHORT(short),T_INT(int)和T_LONG(long)。
如果數(shù)組長度小于0,這個字節(jié)碼會拋出NegativeArraySizeException
。
[2. anewarray]
基本類型的數(shù)組由newarray創(chuàng)建,而不是基本類型的數(shù)組由anewarray創(chuàng)建。
和newarray一樣,如果數(shù)組長度小于0,這個字節(jié)碼會拋出NegativeArraySizeException
。
[3. multianewarray]
創(chuàng)建一個多維數(shù)組,多維數(shù)組的描述符要與第二個參數(shù)維度相匹配。和另兩個字節(jié)碼相同,如果多維數(shù)組任意一維的長度小于0,這個字節(jié)碼就會拋出NegativeArraySizeException
。
下面是使用這三個字節(jié)碼的例子:
Java代碼:
生成這些代碼的字節(jié)碼程序:
在創(chuàng)建數(shù)組時,如果是一維數(shù)組就用newarray或anewarray。multianewarray也能創(chuàng)建一維數(shù)組,但是使用上面的兩個更加高效。
[4. arraylength]
獲取數(shù)組的長度,返回int。如果數(shù)組輸入為null,拋出空指針異常。
[5. xaload]
x=a,b,c,d,f,i,l,s, 其中b同時負責(zé)了byte和boolean
xaload的作用是從數(shù)組指定下標取元素。如果下標超過數(shù)組長度,拋出ArrayIndexOutOfBoundsException
。對于多維數(shù)組的提取元素方式類似下面:
[6. xastore]
x=a,b,c,d,f,i,l,s, 其中b同時負責(zé)了byte和boolean
將對象存入數(shù)組指定下標。如果下標超過數(shù)組長度,拋出ArrayIndexOutOfBoundsException
。對于多維數(shù)組,存儲對象需要和xaload一起配合。
三.操作字段的字節(jié)碼
在代碼中我們經(jīng)常會調(diào)用類中的字段,例如System.out。Java提供了四個字節(jié)碼用于訪問和修改字段。
[1. getfield]
getfield用于獲取非靜態(tài)字段的值。如果它作用目標是一個靜態(tài)字段,則在類連接驗證時拋出IncompatibleClassChangeError
。
如果輸入的對象是null,這個字節(jié)碼會在運行時拋出空指針異常。
這個字節(jié)碼不能調(diào)用數(shù)組的length字段,在編譯的時候length字段會自行轉(zhuǎn)變成arraylength字節(jié)碼。
[2. getstatic]
getstatic用于獲取靜態(tài)字段的值。如果它作用目標是一個非靜態(tài)字段,則在類連接驗證時拋出IncompatibleClassChangeError
。
[3. putfield]
putfield用于修改非靜態(tài)字段的值。如果它作用目標是一個靜態(tài)字段,則在類連接驗證時拋出IncompatibleClassChangeError
。
如果輸入的對象是null,這個字節(jié)碼會在運行時拋出空指針異常。
對于final字段,如果不是在初始化對象時修改(構(gòu)造函數(shù)中),那么就會拋出IllegalAccessError
。
[4. putstatic]
putstatic用于修改靜態(tài)字段的值。如果它作用目標是一個非靜態(tài)字段,則在類連接驗證時拋出IncompatibleClassChangeError
。
對于final字段,如果不是在類初始化時修改(<clinit>中),那么就會拋出IllegalAccessError
。
四.調(diào)用方法的字節(jié)碼
調(diào)用方法的字節(jié)碼共有五個:invokevirtual,invokespecial,invokestatic,invokeinterface和invokedynamic。invokedynamic使用了BSM(BootStrap Method),講解起來很復(fù)雜,所以這個要單獨分出來一篇文章去講。這篇文章主要討論前四個。
這些字節(jié)碼都使用visitMethodInsn方法,其中最后一個參數(shù)代表這個方法是不是在接口內(nèi)定義,而不是代表是不是抽象方法。
[1. invokevirtual]
這個字節(jié)碼用于調(diào)用實例方法:如果對象是子類的對象且子類復(fù)寫了這個方法,則調(diào)用子類的方法;如果對象就是該類的直接對象或者對象所屬子類沒有復(fù)寫這個方法,就調(diào)用現(xiàn)在類的方法。
在編譯時,如果子類調(diào)用了父類的方法且子類沒有實現(xiàn)此方法,那么方法所在的類要寫為父類。如果使用super,要用invokespecial調(diào)用(下文)。
如果方法調(diào)用目標是靜態(tài)的,在連接驗證時會拋出IncompatibleClassChangeError
。
如果方法調(diào)用目標是抽象的,并且在繼承樹上沒有任何實現(xiàn)此方法的類,在調(diào)用時會拋出AbstractMethodError
。
如果方法調(diào)用目標是抽象的,而繼承樹上由多個實現(xiàn)此方法的類,且這些方法都是可被選中成為調(diào)用目標的方法(比如一個類繼承于一個抽象類,又實現(xiàn)了兩個接口,兩個接口中都有一個同樣的default方法可作為抽象類中抽象方法的實現(xiàn)目標),這時此字節(jié)碼會拋出IncompatibleClassChangeError
。
如果方法調(diào)用目標是native的,且沒有任何JNI連接查詢到這個方法和哪個C函數(shù)相連接,這時這個字節(jié)碼拋出UnsatisfiedLinkError
。
[2. invokespecial]
invokespecial類似于invokevirtual,但不同的是,它和調(diào)用方法的對象的類型無關(guān):它的方法調(diào)用對象就是字節(jié)碼內(nèi)部標定的方法,如果這個類找不到就尋找直接超類的方法,而不是像invokevirtual要考慮繼承樹所有的方法。
這個方法經(jīng)常在構(gòu)造函數(shù)中看到,因為無論什么類都需要有一個構(gòu)造函數(shù),而構(gòu)造函數(shù)內(nèi)部必須自動調(diào)用父類構(gòu)造函數(shù)。
一個默認的構(gòu)造函數(shù)類似于下面:
在生成類時,如果沒有自定義其他構(gòu)造函數(shù),就要加上這個默認構(gòu)造函數(shù):
[3. invokestatic]
invokestatic用于調(diào)用靜態(tài)方法,如果調(diào)用目標不是個靜態(tài)方法,拋出IncompatibleClassChangeError
。
和invokevirtual一樣,如果目標是個native方法而JNI找不到連接的C函數(shù),該字節(jié)碼拋出UnsatisfiedLinkError
。
[4. invokeinterface]
這個字節(jié)碼類似于invokevirtual,異常情況的處理也和它類似。它用于調(diào)用接口實例方法,而不是像invokevirtual的實例方法。
五.拋出異常的字節(jié)碼:athrow
athrow負責(zé)將一個Throwable對象拋出。如果對象是null,那么就不會拋出這個null,而是拋出NullPointerException。
通常情況下,我們都是直接new一個Throwable對象然后直接拋出,就像這樣:
翻譯為字節(jié)碼如下:
六.同步字節(jié)碼
同步操作共有兩個字節(jié)碼,monitorenter和monitorexit,成套使用。
輸入的對象必須是引用類型對象,不能是基本類型的值。
使用同步塊時,代碼類似這樣:
對應(yīng)的字節(jié)碼:
monitorenter就是嘗試加鎖的操作。如果這個對象的監(jiān)視器條目計數(shù)為0,此線程會把這個計數(shù)設(shè)置為1,這時此線程就是這個對象的監(jiān)視器;如果不為0且線程不是該對象的監(jiān)視器,線程會阻塞直到計數(shù)為0時重新嘗試加鎖;如果線程已經(jīng)是這個對象的監(jiān)視器,計數(shù)遞增。
monitorexit就是釋放鎖的操作。如果線程是這個對象的監(jiān)視器,計數(shù)遞減,當(dāng)計數(shù)減為0時該線程就不是這個對象的監(jiān)視器了。如果線程不是這個對象的監(jiān)視器,這個字節(jié)碼會拋出IllegalMonitorStateException
。
monitorenter可以和很多monitorexit一起出現(xiàn),在一個方法的所有可能流程中的加鎖次數(shù)和釋放次數(shù)必須相同,否則在調(diào)用時會發(fā)生IllegalMonitorStateException
。
對于同步方法(訪問標志含有ACC_SYNCHRONIZED),不需要手動對自身對象或類加鎖。JVM在調(diào)用方法前隱式加鎖,在調(diào)用之后隱式釋放。
七.應(yīng)用:計算兩數(shù)之積
學(xué)到了這些字節(jié)碼,接下來我們要試試用純字節(jié)碼解決這道簡單的問題。
在Java代碼下,我們可以這樣寫:
下面是用ASM生成的步驟:
首先還是創(chuàng)建類和方法,不再多說。
第一行,創(chuàng)建Scanner對象,這里用到的就是new。
第二行和第三行都是讀取double,這里是調(diào)用了Scanner的nextDouble方法,這里只給第二行的例子:
接下來是個重頭戲。首先來看看PrintStream::printf的定義:
可以看到,args是個不定長參數(shù),這怎么表示呢?
在Java中,不定長參數(shù)都被解析為數(shù)組,也就是說,它在字節(jié)碼中的表示其實是這樣的:
現(xiàn)在我們需要傳遞的參數(shù)就是一個字符串和一個Object數(shù)組??墒莇ouble不是引用類型,這又要怎么辦呢?
在Java中,基本類型都有它們的“包裝類”。double的包裝類是java.lang.Double,通過Double::valueOf方法就可以把double值轉(zhuǎn)變?yōu)镈ouble對象,也就是裝箱操作。在平常編寫時,Java編譯器會自動為我們添加裝箱操作,也就是自動裝箱。
經(jīng)過這樣的解析,最后這句話的Java代碼表示就像這樣:
其中Object[]是一個長度為1的數(shù)組,也就是先創(chuàng)建它然后將Double對象用aastore字節(jié)碼放入就行。
最后寫入return和visitMaxs,局部變量一共5個槽位,最大的操作棧大小是9:
下面就可以實驗了!
測試結(jié)果和預(yù)測一樣!
全部代碼:https://paste.ubuntu.com/p/NXDfFpQ4y6/

這篇專欄的內(nèi)容結(jié)束了,下一篇:Java ASM詳解:MethodVisitor與Opcode(三)標簽,選擇結(jié)構(gòu),循環(huán)結(jié)構(gòu),棧幀
這篇文章一共講了34個字節(jié)碼,從開始到現(xiàn)在一共講了164個。
有錯誤在評論中指出。
這篇文章稍后也同步到https://nickid2018.github.io上。