摘要:字節(jié)碼及使用什么是字節(jié)碼機(jī)器碼機(jī)器碼是可直接解讀的指令。字節(jié)碼的執(zhí)行操作,指的就是對當(dāng)前棧幀數(shù)據(jù)結(jié)構(gòu)進(jìn)行的操作。動(dòng)態(tài)鏈接每個(gè)棧幀指向運(yùn)行時(shí)常量池中該棧幀所屬的方法的引用,也就是字節(jié)碼的發(fā)放調(diào)用的引用。
字節(jié)碼及ASM使用 什么是字節(jié)碼?
機(jī)器碼
機(jī)器碼(machine code)是CPU可直接解讀的指令。機(jī)器碼與硬件等有關(guān),不同的CPU架構(gòu)支持的硬件碼也不相同。
字節(jié)碼
字節(jié)碼(bytecode)是一種包含執(zhí)行程序、由一序列 op 代碼/數(shù)據(jù)對 組成的二進(jìn)制文件。字節(jié)碼是一種中間碼,它比機(jī)器碼更抽象,需要直譯器轉(zhuǎn)譯后才能成為機(jī)器碼的中間代碼。通常情況下它是已經(jīng)經(jīng)過編譯,但與特定機(jī)器碼無關(guān)。字節(jié)碼主要為了實(shí)現(xiàn)特定軟件運(yùn)行和軟件環(huán)境、與硬件環(huán)境無關(guān)。
字節(jié)碼的實(shí)現(xiàn)方式是通過編譯器和虛擬機(jī)器。編譯器將源碼編譯成字節(jié)碼,特定平臺(tái)上的虛擬機(jī)器將字節(jié)碼轉(zhuǎn)譯為可以直接執(zhí)行的指令。
例如:C# IL,Java bytecode
* JAVA代碼編譯和執(zhí)行 Java 代碼編譯是由 Java 源碼編譯器來完成,流程圖如下所示:
Java 字節(jié)碼的執(zhí)行是由 JVM 執(zhí)行引擎來完成,流程圖如下所示: ![](http://ortsyq47e.bkt.clouddn.com/qnsource/images/%E5%AD%97%E8%8A%82%E7%A0%81%E5%8F%8AASM%E4%BD%BF%E7%94%A8/img2.gif)JVM字節(jié)碼執(zhí)行 JVM楨棧結(jié)構(gòu)
以下內(nèi)容可以參照示例閱讀,理解會(huì)不一樣
方法調(diào)用在JVM中轉(zhuǎn)換成的是字節(jié)碼執(zhí)行,字節(jié)碼指令執(zhí)行的數(shù)據(jù)結(jié)構(gòu)就是棧幀(stack frame)。也就是在虛擬機(jī)棧中的棧元素。虛擬機(jī)會(huì)為每個(gè)方法分配一個(gè)棧幀,因?yàn)樘摂M機(jī)棧是LIFO(后進(jìn)先出)的,所以當(dāng)前線程正在活動(dòng)的棧幀,也就是棧頂?shù)臈琂VM規(guī)范中稱之為“CurrentFrame”,這個(gè)當(dāng)前棧幀對應(yīng)的方法就是“CurrentMethod”。字節(jié)碼的執(zhí)行操作,指的就是對當(dāng)前棧幀數(shù)據(jù)結(jié)構(gòu)進(jìn)行的操作。
??JVM的運(yùn)行時(shí)數(shù)據(jù)區(qū)的結(jié)構(gòu)如下圖,本文主要講楨棧結(jié)構(gòu)。運(yùn)行時(shí)數(shù)據(jù)區(qū)
??棧幀的數(shù)據(jù)結(jié)構(gòu)主要分為四個(gè)部分:局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接以及方法返回地址(包括正常調(diào)用和異常調(diào)用的完成結(jié)果)。下面就一一介紹下這四種數(shù)據(jù)結(jié)構(gòu)。
局部變量表(local variables)
??當(dāng)方法被調(diào)用時(shí),參數(shù)會(huì)傳遞到從0開始的連續(xù)的局部變量表的索引位置上。棧幀中局部變量表的長度存儲(chǔ)在類或接口的二進(jìn)制表示中。閱讀Class文件會(huì)找到Code屬性,所以能知道local variables的最大長度是在編譯期間決定的。一個(gè)局部變量表的占用了32位的存儲(chǔ)空間(一個(gè)存儲(chǔ)單位稱之為slot,槽),所以可以存儲(chǔ)一個(gè)boolean、byte、char、short、float、int、refrence和returnAdress數(shù)據(jù),long和double需要2個(gè)連續(xù)的局部變量表來保存,通過較小位置的索引來獲取。如果被調(diào)用的是實(shí)例方法,那么第0個(gè)位置存儲(chǔ)“this”關(guān)鍵字代表當(dāng)前實(shí)例對象的引用。這個(gè)可以通過javap 工具查看實(shí)例方法和靜態(tài)方法對比字節(jié)碼指令的0位置。例子也可以參考JVM 字節(jié)碼指令對于棧幀數(shù)據(jù)操作舉例
操作數(shù)棧(operand stack)
?操作數(shù)棧同局部變量表一樣,也是編譯期間就能決定了其存儲(chǔ)空間(最大的單位長度),通過 Code屬性存儲(chǔ)在類或接口的字節(jié)流中。操作數(shù)棧也是個(gè)LIFO棧。
??操作數(shù)棧是在JVM字節(jié)碼執(zhí)行一些指令(第二部分會(huì)介紹一些指令集)時(shí)創(chuàng)建的,主要是把局部變量表中的變量壓入操作數(shù)棧,在操作數(shù)棧中進(jìn)行字節(jié)碼指令的操作,再將變量出操作數(shù)棧,結(jié)果入操作數(shù)棧。同局部變量表,除了long和double,其他類型數(shù)據(jù)都只占用一個(gè)棧的單位深度。動(dòng)態(tài)鏈接
??每個(gè)棧幀指向運(yùn)行時(shí)常量池中該棧幀所屬的方法的引用,也就是字節(jié)碼的發(fā)放調(diào)用的引用。動(dòng)態(tài)鏈接就是將符號引用所表示的方法,轉(zhuǎn)換成方法的直接引用。加載階段或第一次使用時(shí)轉(zhuǎn)化為直接引用的(將變量的訪問轉(zhuǎn)化為訪問這些變量的存儲(chǔ)結(jié)構(gòu)所在的運(yùn)行時(shí)內(nèi)存位置)就叫做靜態(tài)解析。JVM的動(dòng)態(tài)鏈接還支持運(yùn)行期轉(zhuǎn)化為直接引用。也可以叫做Late Binding,晚期綁定。動(dòng)態(tài)鏈接是java靈活OO的基礎(chǔ)結(jié)構(gòu)。可以參考一個(gè)例子來加深理解從字節(jié)碼指令看重寫在JVM中的實(shí)現(xiàn)
方法返回地址
??方法正常退出,JVM執(zhí)行引擎會(huì)恢復(fù)上層方法局部變量表操作數(shù)棧并把返回值壓入調(diào)用者的棧幀的操作數(shù)棧,PC計(jì)數(shù)器的值就會(huì)調(diào)整到方法調(diào)用指令后面的一條指令。這樣使得當(dāng)前的棧幀能夠和調(diào)用者連接起來,并且讓調(diào)用者的棧幀的操作數(shù)棧繼續(xù)往下執(zhí)行。
??方法的異常調(diào)用完成,主要是JVM拋出的異常,如果異常沒有被捕獲住,或者遇到athrow字節(jié)碼指令顯示拋出,那么就沒有返回值給調(diào)用者。
注:
操作long double類型數(shù)據(jù),一定要分配兩個(gè)slot,如果分配了一個(gè)slot不會(huì)報(bào)錯(cuò),但會(huì)導(dǎo)致數(shù)據(jù)丟失;
實(shí)例方法的第0個(gè)位置存儲(chǔ)“this”關(guān)鍵字代表當(dāng)前實(shí)例對象的引用;
引用自JVM字節(jié)碼執(zhí)行模型及字節(jié)碼指令集,建議閱讀《深入了解JVM》-- 虛擬機(jī)執(zhí)行子系統(tǒng);
字節(jié)碼指令集以下的字節(jié)碼指令集比較枯燥,可參照ASM Opcodes
加載和存儲(chǔ)指令
加載和存儲(chǔ)指令用于將數(shù)據(jù)從棧幀的局部變量表和操作數(shù)棧之間來回傳輸。1)將一個(gè)局部變量加載到操作數(shù)棧的指令包括:iload,iload_,lload、lload_ 、float、 fload_ 、dload、dload_ ,aload、aload_ 。 2)將一個(gè)數(shù)值從操作數(shù)棧存儲(chǔ)到局部變量表的指令:istore,istore_ ,lstore,lstore_ ,fstore,fstore_ ,dstore,dstore_ ,astore,astore_ 3)將常量加載到操作數(shù)棧的指令:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_,lconst_ ,fconst_ ,dconst_ 4)局部變量表的訪問索引指令:wide 一部分以尖括號結(jié)尾的指令代表了一組指令,如iload_,代表了iload_0,iload_1等,這幾組指令都是帶有一個(gè)操作數(shù)的通用指令。
運(yùn)算指令
算術(shù)指令用于對兩個(gè)操作數(shù)棧上的值進(jìn)行某種特定運(yùn)算,并把結(jié)果重新存入到操作棧頂。1)加法指令:iadd,ladd,fadd,dadd 2)減法指令:isub,lsub,fsub,dsub 3)乘法指令:imul,lmul,fmul,dmul 4)除法指令:idiv,ldiv,fdiv,ddiv 5)求余指令:irem,lrem,frem,drem 6)取反指令:ineg,leng,fneg,dneg 7)位移指令:ishl,ishr,iushr,lshl,lshr,lushr 8)按位或指令:ior,lor 9)按位與指令:iand,land 10)按位異或指令:ixor,lxor 11)局部變量自增指令:iinc 12)比較指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmpJava虛擬機(jī)沒有明確規(guī)定整型數(shù)據(jù)溢出的情況,但規(guī)定了處理整型數(shù)據(jù)時(shí),只有除法和求余指令出現(xiàn)除數(shù)為0時(shí)會(huì)導(dǎo)致虛擬機(jī)拋出異常。
Java虛擬機(jī)要求在浮點(diǎn)數(shù)運(yùn)算的時(shí)候,所有結(jié)果否必須舍入到適當(dāng)?shù)木龋绻袃煞N可表示的形式與該值一樣,會(huì)優(yōu)先選擇最低有效位為零的。稱之為最接近數(shù)舍入模式。
浮點(diǎn)數(shù)向整數(shù)轉(zhuǎn)換的時(shí)候,Java虛擬機(jī)使用IEEE 754標(biāo)準(zhǔn)中的向零舍入模式,這種模式舍入的結(jié)果會(huì)導(dǎo)致數(shù)字被截?cái)啵行?shù)部分的有效字節(jié)會(huì)被丟掉。類型轉(zhuǎn)換指令
類型轉(zhuǎn)換指令將兩種Java虛擬機(jī)數(shù)值類型相互轉(zhuǎn)換,這些操作一般用于實(shí)現(xiàn)用戶代碼的顯式類型轉(zhuǎn)換操作。
JVM直接就支持寬化類型轉(zhuǎn)換(小范圍類型向大范圍類型轉(zhuǎn)換):1)int類型到long,float,double類型 2)long類型到float,double類型 3)float到double類型但在處理窄化類型轉(zhuǎn)換時(shí),必須顯式使用轉(zhuǎn)換指令來完成,這些指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和 d2f。
將int 或 long 窄化為整型T的時(shí)候,僅僅簡單的把除了低位的N個(gè)字節(jié)以外的內(nèi)容丟棄,N是T的長度。這有可能導(dǎo)致轉(zhuǎn)換結(jié)果與輸入值有不同的正負(fù)號。
在將一個(gè)浮點(diǎn)值窄化為整數(shù)類型T(僅限于 int 和 long 類型),將遵循以下轉(zhuǎn)換規(guī)則:1)如果浮點(diǎn)值是NaN , 吶轉(zhuǎn)換結(jié)果就是int 或 long 類型的0 2)如果浮點(diǎn)值不是無窮大,浮點(diǎn)值使用IEEE 754 的向零舍入模式取整,獲得整數(shù)v, 如果v在T表示范圍之內(nèi),那就過就是v 3)否則,根據(jù)v的符號, 轉(zhuǎn)換為T 所能表示的最大或者最小正數(shù)對象創(chuàng)建與訪問指令
雖然類實(shí)例和數(shù)組都是對象,Java虛擬機(jī)對類實(shí)例和數(shù)組的創(chuàng)建與操作使用了不同的字節(jié)碼指令。1)創(chuàng)建實(shí)例的指令:new 2)創(chuàng)建數(shù)組的指令:newarray,anewarray,multianewarray 3)訪問字段指令:getfield,putfield,getstatic,putstatic 4)把數(shù)組元素加載到操作數(shù)棧指令:baload,caload,saload,iaload,laload,faload,daload,aaload 5)將操作數(shù)棧的數(shù)值存儲(chǔ)到數(shù)組元素中執(zhí)行:bastore,castore,castore,sastore,iastore,fastore,dastore,aastore 6)取數(shù)組長度指令:arraylength JVM支持方法級同步和方法內(nèi)部一段指令序列同步,這兩種都是通過moniter實(shí)現(xiàn)的。 7)檢查實(shí)例類型指令:instanceof,checkcast操作數(shù)棧管理指令
如同操作一個(gè)普通數(shù)據(jù)結(jié)構(gòu)中的堆棧那樣,Java 虛擬機(jī)提供了一些用于直接操作操作數(shù)棧的指令,包括:1)將操作數(shù)棧的棧頂一個(gè)或兩個(gè)元素出棧:pop、pop2 2)復(fù)制棧頂一個(gè)或兩個(gè)數(shù)值并將復(fù)制值或雙份的復(fù)制值重新壓入棧頂:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。 3)將棧最頂端的兩個(gè)數(shù)值互換:swap控制轉(zhuǎn)移指令
讓JVM有條件或無條件從指定指令而不是控制轉(zhuǎn)移指令的下一條指令繼續(xù)執(zhí)行程序。控制轉(zhuǎn)移指令包括:1)條件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnotnull,if_cmpeq,if_icmpne,if_icmlt,if_icmpgt等 2)復(fù)合條件分支:tableswitch,lookupswitch 3)無條件分支:goto,goto_w,jsr,jsr_w,retJVM中有專門的指令集處理int和reference類型的條件分支比較操作,為了可以無明顯標(biāo)示一個(gè)實(shí)體值是否是null,有專門的指令檢測null 值。boolean類型和byte類型,char類型和short類型的條件分支比較操作,都使用int類型的比較指令完成,而 long,float,double條件分支比較操作,由相應(yīng)類型的比較運(yùn)算指令,運(yùn)算指令會(huì)返回一個(gè)整型值到操作數(shù)棧中,隨后再執(zhí)行int類型的條件比較操作完成整個(gè)分支跳轉(zhuǎn)。各種類型的比較都最終會(huì)轉(zhuǎn)化為int類型的比較操作。
方法調(diào)用和返回指令
invokevirtual指令:調(diào)用對象的實(shí)例方法,根據(jù)對象的實(shí)際類型進(jìn)行分派(虛擬機(jī)分派)。
invokeinterface指令:調(diào)用接口方法,在運(yùn)行時(shí)搜索一個(gè)實(shí)現(xiàn)這個(gè)接口方法的對象,找出合適的方法進(jìn)行調(diào)用。
invokespecial:調(diào)用需要特殊處理的實(shí)例方法,包括實(shí)例初始化方法,私有方法和父類方法
invokestatic:調(diào)用類方法(static)
方法返回指令是根據(jù)返回值的類型區(qū)分的,包括ireturn(返回值是boolean,byte,char,short和 int),lreturn,freturn,drturn和areturn,另外一個(gè)return供void方法,實(shí)例初始化方法,類和接口的類初始化i方法使用。異常處理指令
在Java程序中顯式拋出異常的操作(throw語句)都有athrow 指令來實(shí)現(xiàn),除了用throw 語句顯示拋出異常情況外,Java虛擬機(jī)規(guī)范還規(guī)定了許多運(yùn)行時(shí)異常會(huì)在其他Java虛擬機(jī)指令檢測到異常狀況時(shí)自動(dòng)拋出。
在Java虛擬機(jī)中,處理異常不是由字節(jié)碼指令來實(shí)現(xiàn)的,而是采用異常表來完成的。同步指令
方法級的同步是隱式的,無需通過字節(jié)碼指令來控制,它實(shí)現(xiàn)在方法調(diào)用和返回操作中。虛擬機(jī)從方法常量池中的方法標(biāo)結(jié)構(gòu)中的 ACC_SYNCHRONIZED標(biāo)志區(qū)分是否是同步方法。方法調(diào)用時(shí),調(diào)用指令會(huì)檢查該標(biāo)志是否被設(shè)置,若設(shè)置,執(zhí)行線程持有moniter,然后執(zhí)行方法,最后完成方法時(shí)釋放moniter。
同步一段指令集序列,通常由synchronized塊標(biāo)示,JVM指令集中有monitorenter和monitorexit來支持synchronized語義。
結(jié)構(gòu)化鎖定是指方法調(diào)用期間每一個(gè)monitor退出都與前面monitor進(jìn)入相匹配的情形。JVM通過以下兩條規(guī)則來保證結(jié)結(jié)構(gòu)化鎖成立(T代表一線程,M代表一個(gè)monitor):
1)T在方法執(zhí)行時(shí)持有M的次數(shù)必須與T在方法完成時(shí)釋放的M次數(shù)相等 2)任何時(shí)刻都不會(huì)出現(xiàn)T釋放M的次數(shù)比T持有M的次數(shù)多的情況
注:
大多數(shù)的指令有前綴和(或)后綴來表明其操作數(shù)的類型。如下表
| 前/后綴 | 操作數(shù)類型 | | :------ | :------ | | i | 整數(shù) | | l | 長整數(shù) | | s | 短整數(shù) | | b | 字節(jié) | | c | 字符 | | f | 單精度浮點(diǎn)數(shù) | | d | 雙精度浮點(diǎn)數(shù) | | z | 布爾值 | | a | 引用 |
引用自深入理解java虛擬機(jī) 字節(jié)碼指令簡介
JAVA源碼
package com.taobao.film; /** * @author - zhupin(kaiqiang.gkq@alibaba-inc.com) */ public class Demo { private static final String HELLO_CONST = "Hello"; private static String CONST = null; static { CONST = HELLO_CONST + "%s!"; } public static void main(String[] args) { if (args != null && args.length == 1) { System.out.println(String.format(CONST, args[0])); } } }
對應(yīng)字節(jié)碼
簡記 ms:操作數(shù)棧最大深度 ml:局部變量表最大容量 s:操作數(shù)棧 l:局部變量表
// class version 49.0 (49) // access flags 0x21 public class com/taobao/film/Demo { // compiled from: Demo.java // access flags 0x1A 常量,類被裝載時(shí)分配空間 private static final String HELLO_CONST = "Hello"; private final static Ljava/lang/String; HELLO_CONST = "Hello" // access flags 0xA 靜態(tài)屬性,類被裝載時(shí)分配空間 private static String CONST ; private static Ljava/lang/String; CONST // access flags 0x1 沒有重載構(gòu)造函數(shù),默認(rèn)構(gòu)造函數(shù)借助字節(jié)碼能干什么()V public ()V L0 // 標(biāo)簽表示方法的字節(jié)碼中的位置。標(biāo)簽用于跳轉(zhuǎn),goto和切換指令,以及用于嘗試catch塊。標(biāo)簽指定剛剛之后的指令。注意,在標(biāo)簽和它指定的指令(例如其他標(biāo)簽,堆棧映射幀,行號等)之間可以有其他元素。 LINENUMBER 6 L0 //異常的棧信息中對應(yīng)的line number,可刪除不影響運(yùn)行 ALOAD 0 //this INVOKESPECIAL java/lang/Object. ()V //this.super() RETURN L1 LOCALVARIABLE this Lcom/taobao/film/Demo; L0 L1 0 MAXSTACK = 1 //最大棧深度1,壓棧局部變量this MAXLOCALS = 1 //局部變量數(shù)1,局部變量this // access flags 0x9 簡記 ms:操作數(shù)棧最大深度 ml:局部變量表最大容量 s:操作數(shù)棧 l:局部變量表 public static main([Ljava/lang/String;)V L0 LINENUMBER 15 L0 ALOAD 0 //非靜態(tài)方法,局部變量0即方法入?yún)⒌谝粋€(gè)參數(shù)args reference; ms=1,ml=1,s=[args ref],l=[args ref] IFNULL L1 //if(args ref==null)跳轉(zhuǎn) L1 return ALOAD 0 //load args ms=1,ml=1,s=[args ref],l=[args ref] ARRAYLENGTH//計(jì)算args.length將結(jié)果入操作數(shù)棧 ms=1,ml=1,s=[args ref],l=[args ref] ICONST_1 //數(shù)字常量1 ms=2,ml=1,s=[1,args ref],l=[args ref] IF_ICMPNE L1 //if(args.length == 1) 跳轉(zhuǎn) L1 return L2 //ms=2,ml=1,s=[],l=[args ref] LINENUMBER 16 L2 GETSTATIC java/lang/System.out : Ljava/io/PrintStream;//訪問System.out,入操作數(shù)棧 ms=2,ml=1,s=[System.out],l=[args ref] GETSTATIC com/taobao/film/Demo.CONST : Ljava/lang/String;//訪問CONST,入操作數(shù)棧 ms=2,ml=1,s=[CONST,System.out],l=[args ref] {//這里一段,實(shí)現(xiàn)的是new Object[]{args[0]},String.format(String format, Object... args) ICONST_1//1 ms=3,ml=1,s=[1,CONST,System.out],l=[args ref] ANEWARRAY java/lang/Object// new Object[1]; ms=3,ml=1,s=[objects ref,CONST,System.out],l=[args ref] DUP // ms=4,ml=1,s=[objects ref,objects ref,CONST,System.out],l=[args ref] ICONST_0//0 ms=5,ml=1,s=[0,objects ref,objects ref,CONST,System.out],l=[args ref] ALOAD 0 //args reference入操作數(shù)棧; ms=6,ml=1,s=[args ref,0,objects ref,objects ref,CONST,System.out],l=[args ref] ICONST_0 //0 ms=7,ml=1,s=[0,args ref,0,objects ref,objects ref,CONST,System.out],l=[args ref] AALOAD //args[0] ms=7,ml=1,s=[args[0],0,objects ref,objects ref,CONST,System.out],l=[args ref] AASTORE //objects[0]=args[0]; ms=7,ml=1,s=[objects ref,CONST,System.out],l=[args ref] } INVOKESTATIC java/lang/String.format (Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;//String.format(CONST,objects) ms=7,ml=1,s=[objects ref,CONST,System.out],l=[args ref] INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V //System.out.println(...); ms=7,ml=1,s=[],l=[args ref] L1 LINENUMBER 18 L1 RETURN L3 LOCALVARIABLE args [Ljava/lang/String; L0 L3 0 MAXSTACK = 7 MAXLOCALS = 1 // access flags 0x8 V classload init,靜態(tài)代碼初始塊,類被裝在時(shí)init static ()V L0 LINENUMBER 8 L0 ACONST_NULL //壓棧常量NULL PUTSTATIC com/taobao/film/Demo.CONST : Ljava/lang/String; //靜態(tài)屬性賦值,CONST=null L1 LINENUMBER 11 L1 LDC "Hello%s!" //加載常量Hello%s! 編譯優(yōu)化,hello+%s編譯優(yōu)化為常量Hello%s! PUTSTATIC com/taobao/film/Demo.CONST : Ljava/lang/String;//賦值Hello%s! L2 LINENUMBER 12 L2 RETURN MAXSTACK = 1 //只有賦值操作,操作數(shù)棧深度1 MAXLOCALS = 0 //靜態(tài)方法無局部變量 }
回答這個(gè)問題前我們首先得明白字節(jié)碼是作用于運(yùn)行期,所以一般用來在運(yùn)行期改變類的行為,基本上都會(huì)結(jié)合代理模式使用。
常見字節(jié)碼框架ASM
BCEL
CGLIB
Javassist
Byte Buddy
本文來介紹下ASM框架的使用(了解一點(diǎn)底層字節(jié)碼操作,對了解JVM執(zhí)行過程以及我們常用框架的實(shí)現(xiàn)原理都會(huì)有新的認(rèn)識(shí))
ClassReader用來讀取原有的字節(jié)碼,ClassWriter用于寫入字節(jié)碼,ClassVisitor、FieldVisitor、MethodVisitor、AnnotationVisitor訪問修改對應(yīng)組件。(一般不要通過ClassReader讀取再通過Vistor修改類型為再ClassWriter回寫,覆蓋原有類行為,采用繼承會(huì)更安全)
注:
Diving into Bytecode Manipulation
[字節(jié)碼操縱技術(shù)探秘
](http://www.infoq.com/cn/artic...
以下代碼效果等同于上面的Demo示例
import java.io.FileOutputStream; import java.lang.reflect.Method; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; public class DemoDump implements Opcodes { public static byte[] dump() throws Exception { ClassWriter cw = new ClassWriter(0); FieldVisitor fv; MethodVisitor mv; AnnotationVisitor av0; cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, "com/taobao/film/Demo", null, "java/lang/Object", null); cw.visitSource("Demo.java", null); { fv = cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "HELLO_CONST", "Ljava/lang/String;", null, "Hello"); fv.visitEnd(); } { fv = cw.visitField(ACC_PRIVATE + ACC_STATIC, "CONST", "Ljava/lang/String;", null, null); fv.visitEnd(); } { mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null); mv.visitCode(); Label l0 = new Label(); mv.visitLabel(l0); mv.visitLineNumber(6, l0); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", " ", "()V", false); mv.visitInsn(RETURN); Label l1 = new Label(); mv.visitLabel(l1); mv.visitLocalVariable("this", "Lcom/taobao/film/Demo;", null, l0, l1, 0); mv.visitMaxs(1, 1); mv.visitEnd(); } { mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); mv.visitCode(); Label l0 = new Label(); mv.visitLabel(l0); mv.visitLineNumber(15, l0); mv.visitVarInsn(ALOAD, 0); Label l1 = new Label(); mv.visitJumpInsn(IFNULL, l1); mv.visitVarInsn(ALOAD, 0); mv.visitInsn(ARRAYLENGTH); mv.visitInsn(ICONST_1); mv.visitJumpInsn(IF_ICMPNE, l1); Label l2 = new Label(); mv.visitLabel(l2); mv.visitLineNumber(16, l2); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitFieldInsn(GETSTATIC, "com/taobao/film/Demo", "CONST", "Ljava/lang/String;"); mv.visitInsn(ICONST_1); mv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); mv.visitInsn(DUP); mv.visitInsn(ICONST_0); mv.visitVarInsn(ALOAD, 0); mv.visitInsn(ICONST_0); mv.visitInsn(AALOAD); mv.visitInsn(AASTORE); mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "format", "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", false); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitLabel(l1); mv.visitLineNumber(18, l1); mv.visitInsn(RETURN); Label l3 = new Label(); mv.visitLabel(l3); mv.visitLocalVariable("args", "[Ljava/lang/String;", null, l0, l3, 0); mv.visitMaxs(7, 1); mv.visitEnd(); } { mv = cw.visitMethod(ACC_STATIC, " ", "()V", null, null); mv.visitCode(); Label l0 = new Label(); mv.visitLabel(l0); mv.visitLineNumber(8, l0); mv.visitInsn(ACONST_NULL); mv.visitFieldInsn(PUTSTATIC, "com/taobao/film/Demo", "CONST", "Ljava/lang/String;"); Label l1 = new Label(); mv.visitLabel(l1); mv.visitLineNumber(11, l1); mv.visitLdcInsn("Hello%s!"); mv.visitFieldInsn(PUTSTATIC, "com/taobao/film/Demo", "CONST", "Ljava/lang/String;"); Label l2 = new Label(); mv.visitLabel(l2); mv.visitLineNumber(12, l2); mv.visitInsn(RETURN); mv.visitMaxs(1, 0); mv.visitEnd(); } cw.visitEnd(); return cw.toByteArray(); } public static class MyClassLoader extends ClassLoader { public MyClassLoader() { super(); } public MyClassLoader(ClassLoader cl) { super(cl); } public Class> defineClass(String name, byte[] b) { return defineClass(name, b, 0, b.length); } } public static void main(String[] args) throws Exception { byte[] bytes = dump(); FileOutputStream fileOutputStream = new FileOutputStream("D:Demo.class"); fileOutputStream.write(bytes); fileOutputStream.close(); MyClassLoader classLoader = new MyClassLoader(Thread.currentThread().getContextClassLoader()); Class> testClass = classLoader.defineClass("com.taobao.film.Demo", bytes); // invoke static Method staticMain = testClass.getMethod("main", String[].class); staticMain.invoke(null, new Object[] {new String[] {"zhupin"}}); } }
XML快速序列化
為無狀態(tài)邏輯類的指定函數(shù)產(chǎn)生一個(gè)代理,代理接口接受字符串?dāng)?shù)組,轉(zhuǎn)換后調(diào)用原函數(shù)
知名框架中的使用fastjson hotcode asm
dubbo hsf javaassist
hibernate cglib javaassist
spring aop cglib (通過設(shè)置-Dcglib.debugLocation=D://tmp 開啟classdump)
mockito cglib
建議最早接觸字節(jié)碼編程還是在使用IL(C# Emit),也在大型項(xiàng)目中使用過ASM獲得了不錯(cuò)的效果。不過個(gè)人覺得操作字節(jié)碼編程還是要保持謹(jǐn)慎態(tài)度。下面是我的一點(diǎn)小建議:
操作字節(jié)碼作用于運(yùn)行期,對于開發(fā)人員是完全透明的。
字節(jié)碼編程的可閱讀可維護(hù)性比較差,不要濫用字節(jié)碼編程。
能通過設(shè)計(jì)模式實(shí)現(xiàn)的場景盡量通過設(shè)計(jì)模式實(shí)現(xiàn);
字節(jié)碼編程中復(fù)雜的邏輯也盡量使用java實(shí)現(xiàn),在字節(jié)碼中調(diào)用;
使用字節(jié)碼解決一些框架性的問題,不要用于處理易變邏輯;
字節(jié)碼編程從邏輯塊著手,優(yōu)先明確程序跳轉(zhuǎn)Label,再補(bǔ)充邏輯執(zhí)行
借助工具
推薦使用intellj idea插件ASM Bytecode Outline,目前生成的字節(jié)碼對應(yīng)到 ASM 5.x。
使用decompile工具校驗(yàn)生成代碼是否正確。
更多文章請?jiān)L問我的博客
轉(zhuǎn)載請注明出處
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/67221.html
摘要:全稱應(yīng)用性能管理監(jiān)控后面我會(huì)通過一系列的文章來介紹的原理框架設(shè)計(jì)與實(shí)現(xiàn)等等。在應(yīng)用構(gòu)建期間,通過修改字節(jié)碼的方式來進(jìn)行字節(jié)碼插樁就是實(shí)現(xiàn)自動(dòng)化的方案之一。 showImg(https://segmentfault.com/img/bVbbRX6?w=1995&h=1273); 歡迎關(guān)注微信公眾號:BaronTalk,獲取更多精彩好文! 一. 前言 性能問題是導(dǎo)致 App 用戶流失的罪魁...
摘要:運(yùn)行時(shí)修改字節(jié)碼需求在運(yùn)行時(shí)動(dòng)態(tài)修改某個(gè)類的字節(jié)碼文件,不重啟服務(wù)器。方案字節(jié)碼修改框架有很多第三方的字節(jié)碼修改框架,由于前期接觸了產(chǎn)品所以決定使用框架,相對而言更為簡單,但是效率較低。 運(yùn)行時(shí)修改字節(jié)碼 需求:在運(yùn)行時(shí)動(dòng)態(tài)修改某個(gè)類的字節(jié)碼文件,不重啟服務(wù)器。 方案:asm/javaassist + agent+Instrumentation 1.字節(jié)碼修改框架 有很多第三方的字節(jié)碼...
每篇一句 胡適:多談些問題,少聊些主義 前言 Spring MVC和MyBatis作為當(dāng)下最為流行的兩個(gè)框架,大家平時(shí)開發(fā)中都在用。如果你往深了一步去思考,你應(yīng)該會(huì)有這樣的疑問: 在使用Spring MVC的時(shí)候,你即使不使用注解,只要參數(shù)名和請求參數(shù)的key對應(yīng)上了,就能自動(dòng)完成數(shù)值的封裝 在使用MyBatis(接口模式)時(shí),接口方法向xml里的SQL語句傳參時(shí),必須(當(dāng)然不是100%的必須,...
摘要:但是這種方式對于接口和抽象方法是不管用的,因?yàn)槌橄蠓椒]有方法體,也就沒有局部變量,自然也就沒有局部變量表了是通過接口跟語句綁定然后生成代理類來實(shí)現(xiàn)的,因此它無法通過解析字節(jié)碼來獲取方法參數(shù)名。 聲明:本文屬原創(chuàng)文章,首發(fā)于公號:程序員自學(xué)之道,轉(zhuǎn)載請注明出處! 發(fā)現(xiàn)問題 對Java字節(jié)碼有一定了解的朋友應(yīng)該知道,Java 在編譯的時(shí)候,默認(rèn)會(huì)將方法參數(shù)名丟棄,因此我們無法在運(yùn)行時(shí)獲取...
閱讀 2291·2021-11-24 10:18
閱讀 2721·2021-11-19 09:59
閱讀 1713·2019-08-30 15:53
閱讀 1189·2019-08-30 15:53
閱讀 1071·2019-08-30 14:19
閱讀 2482·2019-08-30 13:14
閱讀 3005·2019-08-30 13:00
閱讀 1938·2019-08-30 11:11