摘要:字節(jié)碼是程序的中間表示形式介于人類可讀的源碼和機(jī)器碼之間。在中一般是用編譯源文件變成字節(jié)碼,也就是我們的文件。字節(jié)碼的執(zhí)行操作,指的就是對(duì)當(dāng)前棧幀數(shù)據(jù)結(jié)構(gòu)進(jìn)行的操作。
0.寫(xiě)在前面
為什么會(huì)寫(xiě)這篇文章呢?主要是之前調(diào)研過(guò)日志脫敏相關(guān)的一些,具體可以參考LOG4j脫敏插件如何編寫(xiě)
里面描述了日志脫敏插件編寫(xiě)方法:
直接在toString中修改代碼,這種方法很麻煩,效率低,需要修改每一個(gè)要脫敏的類,或者寫(xiě)個(gè)idea插件自動(dòng)修改toString(),這樣不好的地方在于所有編譯器都需要開(kāi)個(gè)插件,不夠通用。
在編譯時(shí)期修改抽象語(yǔ)法樹(shù)修改toString()方法,就像類似Lombok一樣,這個(gè)之前調(diào)研過(guò),開(kāi)發(fā)難度較大,可能后會(huì)更新如何去寫(xiě)。
在加載的時(shí)候通過(guò)實(shí)現(xiàn)Instrumentation接口 asm庫(kù),修改class文件的字節(jié)碼,但是有個(gè)比較麻煩的地方在于需要給jvm加上啟動(dòng)參數(shù) -javaagent:agentjarpath,這個(gè)已經(jīng)實(shí)現(xiàn)了,但是實(shí)現(xiàn)后發(fā)現(xiàn)的確不夠通用。
期中二三兩個(gè)已經(jīng)實(shí)現(xiàn)了,開(kāi)發(fā)這個(gè)的確比較有趣,自己的知識(shí)面也得到了擴(kuò)展,后續(xù)會(huì)通過(guò)寫(xiě)4-5篇的文章,一步一步的帶大家如何去實(shí)現(xiàn)這些有趣的工具,學(xué)會(huì)了之后,通過(guò)大家豐富的想象力相信能實(shí)現(xiàn)更多有意思的東西。
0.1字節(jié)碼能干什么例如我這篇文章要介紹的通過(guò)修改字節(jié)碼去實(shí)現(xiàn)日志脫敏,其實(shí)就是修改toString的字節(jié)碼:
可以看看怎么用:
@Desensitized public class StreamDemo1 { private User user; @DesFiled(MobileDesFilter.class) private String name; private String idCard; @DesFiled(AddressDesFilter.class) private Listmm; public static void main(String[] args) throws IOException { StreamDemo1 streamDemo1 = new StreamDemo1(); streamDemo1.setUser(new User()); streamDemo1.setName("18428368642"); streamDemo1.setIdCard("22321321321"); streamDemo1.setMm(Arrays.asList("北京是朝陽(yáng)區(qū)打撒所大所大","北京是朝陽(yáng)區(qū)打撒所大所大")); System.out.println(streamDemo1); } @Override public String toString() { return "StreamDemo1{" + "user=" + user + ", name="" + name + """ + ", idCard="" + idCard + """ + ", mm=" + mm + "}"; } }
這個(gè)類很普通對(duì)吧,和其他的實(shí)體類,唯一的區(qū)別是多了一個(gè)注解: @DesFiled(MobileDesFilter.class),有了這個(gè)注解我們執(zhí)行這個(gè)main方法:他會(huì)輸出:
StreamDemo1{user=bean.User@22d8cfe0, name="184****4777", idCard="22321321321", mm=[北京是朝陽(yáng)區(qū)打*****, 北京是朝陽(yáng)區(qū)打*****]}
可以看見(jiàn)我們明明輸入的是不帶號(hào)的手機(jī)號(hào),為什么輸出缺帶號(hào)了呢,這就是操縱字節(jié)碼的神奇。當(dāng)然大家也可以自己擴(kuò)展思維,你可以用他來(lái)做aop切面,當(dāng)然cglib做切面的確也是操縱的字節(jié)碼,你也可以用它來(lái)做你想讓它做的事
0.2語(yǔ)法樹(shù)另一方面我也調(diào)研了lombok的實(shí)現(xiàn),對(duì)此我發(fā)現(xiàn)修改抽象語(yǔ)法樹(shù),似乎更加有趣,你可以想象,你平時(shí)是否重復(fù)的給每個(gè)方法打印入?yún)⒊鰠ⅲ臅r(shí)耗力?你平時(shí)是否在為缺少關(guān)鍵的日志而感到想罵人?你平時(shí)是否害怕用寫(xiě)AOP用反射打日志會(huì)影響性能?為了解決這個(gè)問(wèn)題做了一個(gè)意思的工具slothLog,github地址:slothlog github https://github.com/lzggsimida... (當(dāng)然也求各位大佬們給點(diǎn)star,O(∩_∩)O哈哈~)。
@LogInfo public class DemoService { public String hello(String name, int age){ System.out.println(name + age + "hello"); return name+age; } public static void main(String[] args) { DemoService demoService = new DemoService(); demoService.hello("java", 100); } }
通過(guò)上面會(huì)輸出以下信息,將方法的出參,入?yún)⒍歼M(jìn)行輸出,脫離了調(diào)試時(shí)缺少日志的苦惱
[INFO ] 2018-07-20 20:02:42,219 DemoService.main invoke start args: {} [INFO ] 2018-07-20 20:02:42,220 DemoService.hello invoke start name: java ,age: 100 java100hello [INFO ] 2018-07-20 20:02:42,221 DemoService.hello invoke end name: java ,age: 100 , result: java100
后續(xù)我會(huì)一步一步的教大家如何去完成一個(gè)類似Lombok的修改語(yǔ)法樹(shù)的框架,做更多有趣的事。
0.3關(guān)于本篇如果你不喜歡上面這些東西,也別著急,字節(jié)碼是java的基礎(chǔ),我覺(jué)得是所有Java程序員需要必備的,當(dāng)然你也有必要了解一下。
本篇是系列的第一篇,這篇主要講的主要是字節(jié)碼是什么,通過(guò)對(duì)這篇的了解,也是后續(xù)章節(jié)的基礎(chǔ)。
機(jī)器碼(machine code)顧名思義也就是,機(jī)器能識(shí)別的代碼,也叫原生碼。機(jī)器碼是CPU可直接解讀的指令。機(jī)器碼與硬件等有關(guān),不同的CPU架構(gòu)支持的硬件碼也不相同。機(jī)器碼是和我們的底層硬件直接打交道,現(xiàn)在學(xué)的人也是逐漸的變少了,如果對(duì)這個(gè)感興趣的同學(xué)可以去學(xué)習(xí)一下匯編,匯編的指令會(huì)被翻譯成機(jī)器碼。
1.2字節(jié)碼字節(jié)碼(Byte-code)是一種包含執(zhí)行程序、由一序列 op 代碼/數(shù)據(jù)對(duì)組成的二進(jìn)制文件。字節(jié)碼是程序的中間表示形式:介于人類可讀的源碼和機(jī)器碼之間。它經(jīng)常被看作是包含一個(gè)執(zhí)行程序的二進(jìn)制文件,更像一個(gè)對(duì)象模型。字節(jié)碼被這樣叫是因?yàn)橥ǔC總€(gè)操作碼 是一字節(jié)長(zhǎng),所以字節(jié)碼的程度是根據(jù)一字節(jié)來(lái)的。字節(jié)碼也是由,一組操作碼組成,而操作碼實(shí)際上是對(duì)棧的操作,可以移走參數(shù)和地址空間,也可以放入結(jié)果。JAVA通過(guò)JIT(即時(shí)編譯)可以將字節(jié)碼轉(zhuǎn)換為機(jī)器碼。
字節(jié)碼的實(shí)現(xiàn)方式是通過(guò)編譯器和虛擬機(jī)器。編譯器將源碼編譯成字節(jié)碼,特定平臺(tái)上的虛擬機(jī)器將字節(jié)碼轉(zhuǎn)譯為可以直接執(zhí)行的指令。在java中一般是用Javac編譯源文件變成字節(jié)碼,也就是我們的class文件。
從網(wǎng)絡(luò)上找到了兩張圖片,下面是java源碼編譯器生成字節(jié)碼過(guò)程:
java虛擬機(jī)執(zhí)行引擎過(guò)程,這里會(huì)分為兩個(gè)階段:
普通的代碼(非熱)都是走的字節(jié)碼解釋器
熱代碼:多次調(diào)用的方法,多次執(zhí)行的循環(huán)體,會(huì)被JIT優(yōu)化成機(jī)器碼。
2.字節(jié)碼執(zhí)行 2.1JVM楨棧結(jié)構(gòu):方法調(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ù)臈?,JVM規(guī)范中稱之為“CurrentFrame”,這個(gè)當(dāng)前棧幀對(duì)應(yīng)的方法就是“CurrentMethod”。字節(jié)碼的執(zhí)行操作,指的就是對(duì)當(dāng)前棧幀數(shù)據(jù)結(jié)構(gòu)進(jìn)行的操作。
JVM的運(yùn)行時(shí)數(shù)據(jù)區(qū)的結(jié)構(gòu)如下圖:。
我們這里主要討論棧幀的數(shù)據(jù)結(jié)構(gòu):有四個(gè)部分,局部變量區(qū),操作數(shù)棧,動(dòng)態(tài)鏈接,方法的返回地址。
2.1.1局部變量表:局部變量表是一組變量值存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。在Java程序被編譯成Class文件時(shí),就在Code屬性中l(wèi)ocals變量:
如下面代碼反編譯后就能看見(jiàn)locals=5。
局部變量的容量以變量槽(Slot)為最小單位,32位虛擬機(jī)中一個(gè)Slot可以存放一個(gè)32位以內(nèi)的數(shù)據(jù)類型(boolean、byte、char、short、int、float、reference(引用)和returnAddress八種)。
同時(shí)Slot對(duì)對(duì)象的引用會(huì)影響GC,(要是被引用,不會(huì)被回收)。
系統(tǒng)不會(huì)為局部變量賦予初始值,也就是說(shuō)不存在類變量那樣的準(zhǔn)備階段。
虛擬機(jī)是使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過(guò)程的,如果是實(shí)例方法(非static),那么局部變量表的第0位索引的Slot默認(rèn)是用于傳遞方法所屬對(duì)象實(shí)例的引用,在方法中通過(guò)this訪問(wèn)。
我們上面的代碼中是4個(gè)Int的solt加一個(gè)this 的solt所以就等于5。
2.1.2操作數(shù)棧Java虛擬機(jī)的解釋執(zhí)行引擎被稱為"基于棧的執(zhí)行引擎",其中所指的棧就是指-操作數(shù)棧。
操作數(shù)棧同局部變量表一樣,也是編譯期間就能決定了其存儲(chǔ)空間(最大的單位長(zhǎng)度),通過(guò) Code屬性存儲(chǔ)在類或接口的字節(jié)流中。操作數(shù)棧也是個(gè)LIFO棧。?它不是通過(guò)索引來(lái)訪問(wèn),而是通過(guò)標(biāo)準(zhǔn)的棧操作—壓棧和出棧—來(lái)訪問(wèn)的。比如,如果某個(gè)指令把一個(gè)值壓入到操作數(shù)棧中,稍后另一個(gè)指令就可以彈出這個(gè)值來(lái)使用。
虛擬機(jī)在操作數(shù)棧中存儲(chǔ)數(shù)據(jù)的方式和在局部變量區(qū)中是一樣的:如int、long、float、double、reference和returnType的存儲(chǔ)。對(duì)于byte、short以及char類型的值在壓入到操作數(shù)棧之前,也會(huì)被轉(zhuǎn)換為int。
2.1.3動(dòng)態(tài)鏈接動(dòng)態(tài)鏈接就是將符號(hào)引用所表示的方法,轉(zhuǎn)換成方法的直接引用。加載階段或第一次使用時(shí)轉(zhuǎn)化為直接引用的(將變量的訪問(wèn)轉(zhuǎn)化為訪問(wè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)。
注:
符號(hào)引用就是字符串,這個(gè)字符串包含足夠的信息,以供實(shí)際使用時(shí)可以找到相應(yīng)的位置。你比如說(shuō)某個(gè)方法的符號(hào)引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有類的信息,方法名,方法參數(shù)等信息。
當(dāng)?shù)谝淮芜\(yùn)行時(shí),要根據(jù)字符串的內(nèi)容,到該類的方法表中搜索這個(gè)方法。運(yùn)行一次之后,符號(hào)引用會(huì)被替換為直接引用,下次就不用搜索了。直接引用就是偏移量,通過(guò)偏移量虛擬機(jī)可以直接在該類的內(nèi)存區(qū)域中找到方法字節(jié)碼的起始位置。重寫(xiě)就是動(dòng)態(tài)鏈接,重載就是靜態(tài)解析。
2.1.4方法返回地址方法正常退出,JVM執(zhí)行引擎會(huì)恢復(fù)上層方法局部變量表操作數(shù)棧并把返回值壓入調(diào)用者的棧幀的操作數(shù)棧,PC計(jì)數(shù)器的值就會(huì)調(diào)整到方法調(diào)用指令后面的一條指令。這樣使得當(dāng)前的棧幀能夠和調(diào)用者連接起來(lái),并且讓調(diào)用者的棧幀的操作數(shù)棧繼續(xù)往下執(zhí)行。???方法的異常調(diào)用完成,如果異常沒(méi)有被捕獲住,或者遇到athrow字節(jié)碼指令顯示拋出,那么就沒(méi)有返回值給調(diào)用者。
2.2字節(jié)碼指令集 2.2.1加載和存儲(chǔ)指令加載和存儲(chǔ)指令用于將數(shù)據(jù)從棧幀的局部變量表和操作數(shù)棧之間來(lái)回傳輸。
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)局部變量表的訪問(wèn)索引指令:wide
2.2.2運(yùn)算指令算術(shù)指令用于對(duì)兩個(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,lcmp
Java虛擬機(jī)沒(méi)有明確規(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ù)木?,如果有兩種可表示的形式與該值一樣,會(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ì)被丟掉。
2.2.3類型轉(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)換指令來(lái)完成,這些指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和 d2f。將int 或 long 窄化為整型T的時(shí)候,僅僅簡(jiǎn)單的把除了低位的N個(gè)字節(jié)以外的內(nèi)容丟棄,N是T的長(zhǎng)度。這有可能導(dǎo)致轉(zhuǎn)換結(jié)果與輸入值有不同的正負(fù)號(hào)。
在將一個(gè)浮點(diǎn)值窄化為整數(shù)類型T(僅限于 int 和 long 類型),將遵循以下轉(zhuǎn)換規(guī)則:
1)如果浮點(diǎn)值是NaN , 那轉(zhuǎn)換結(jié)果就是int 或 long 類型的0
2)如果浮點(diǎn)值不是無(wú)窮大,浮點(diǎn)值使用IEEE 754 的向零舍入模式取整,獲得整數(shù)v, 如果v在T表示范圍之內(nèi),那就是v
3)否則,根據(jù)v的符號(hào), 轉(zhuǎn)換為T(mén) 所能表示的最大或者最小正數(shù)
2.2.4對(duì)象創(chuàng)建和訪問(wèn)指令雖然類實(shí)例和數(shù)組都是對(duì)象,Java虛擬機(jī)對(duì)類實(shí)例和數(shù)組的創(chuàng)建與操作使用了不同的字節(jié)碼指令。
1)創(chuàng)建實(shí)例的指令:new
2)創(chuàng)建數(shù)組的指令:newarray,anewarray,multianewarray
3)訪問(wèn)字段指令: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ù)組長(zhǎng)度指令:arraylength JVM支持方法級(jí)同步和方法內(nèi)部一段指令序列同步,這兩種都是通過(guò)moniter實(shí)現(xiàn)的。
7)檢查實(shí)例類型指令:instanceof,checkcast
2.2.5操作數(shù)棧管理指令如同操作一個(gè)普通數(shù)據(jù)結(jié)構(gòu)中的堆棧那樣,Java虛擬機(jī)提供了一些用于直接操作操作舒展的指令,包括:
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
2.2.6控制轉(zhuǎn)移指令讓JVM有條件或無(wú)條件從指定指令而不是控制轉(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)無(wú)條件分支:goto,goto_w,jsr,jsr_w,ret
JVM中有專門(mén)的指令集處理int和reference類型的條件分支比較操作,為了可以無(wú)明顯標(biāo)示一個(gè)實(shí)體值是否是null,有專門(mén)的指令檢測(cè)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類型的比較操作。
2.2.7方法調(diào)用和返回指令invokevirtual指令:調(diào)用對(duì)象的實(shí)例方法,根據(jù)對(duì)象的實(shí)際類型進(jìn)行分派(虛擬機(jī)分派)。
invokeinterface指令:調(diào)用接口方法,在運(yùn)行時(shí)搜索一個(gè)實(shí)現(xiàn)這個(gè)接口方法的對(duì)象,找出合適的方法進(jìn)行調(diào)用。
invokespecial:調(diào)用需要特殊處理的實(shí)例方法,包括實(shí)例初始化方法,私有方法和父類方法
invokestatic:調(diào)用類方法(static)
方法返回指令是根據(jù)返回值的類型區(qū)分的,包括ireturn(返回值是boolean,byte,char,short和 int),lreturn(long),freturn,drturn(double)和areturn(引用地址),另外一個(gè)return供void方法,實(shí)例初始化方法,類和接口的類初始化i方法使用。
2.2.8異常處理指令在Java程序中顯式拋出異常的操作(throw語(yǔ)句)都有athrow 指令來(lái)實(shí)現(xiàn),除了用throw 語(yǔ)句顯示拋出異常情況外,Java虛擬機(jī)規(guī)范還規(guī)定了許多運(yùn)行時(shí)異常會(huì)在其他Java虛擬機(jī)指令檢測(cè)到異常狀況時(shí)自動(dòng)拋出。在Java虛擬機(jī)中,處理異常不是由字節(jié)碼指令來(lái)實(shí)現(xiàn)的,而是采用異常表來(lái)完成的。
2.2.9同步指令方法級(jí)的同步是隱式的,無(wú)需通過(guò)字節(jié)碼指令來(lái)控制,它實(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來(lái)支持synchronized語(yǔ)義。
大多數(shù)的指令有前綴和(或)后綴來(lái)表明其操作數(shù)的類型。如下表
前/后綴
操作數(shù)類型
i
整數(shù)
l
長(zhǎng)整數(shù)
s
短整數(shù)
b
字節(jié)
c
字符
f
單精度浮點(diǎn)數(shù)
d
雙精度浮點(diǎn)數(shù)
z
布爾值
a
引用
這一節(jié)將給大家分析如何一步一步的分析字節(jié)碼。
3.1源代碼有如下簡(jiǎn)單代碼,下面代碼是一個(gè)簡(jiǎn)單的demo,有一個(gè)常量,有一個(gè)類成員變量,同時(shí)方法有三個(gè),一個(gè)構(gòu)造方法,一個(gè)get(),一個(gè)靜態(tài)main方法,用來(lái)輸出信息。
package java8; public class ByteCodeDemo { private static final String name = "xiaoming"; private int age; public ByteCodeDemo(int age) { this.age = age; } public int getAge() { return age; } public static void main(String[] args) { ByteCodeDemo byteCodeDeomo = new ByteCodeDemo(12); System.out.println("name:" + name + "age:" + byteCodeDeomo.getAge()); } }3.2.反編譯
用命令行找到我們這段代碼所在的路徑,輸入如下命令:
javac ByteCodeDemo.java javap -p -v ByteCodeDemo
有關(guān)Javap命令可以用help或者參考javap命令,我們這里用的-p,-v輸出所有類和成員信息,以及附加信息(文件路徑,文件大小,常量池等等)
3.3.得到如下信息Classfile /Users/lizhao/Documents/RPC/test/src/main/java/java8/ByteCodeDemo.class Last modified 2018-5-8; size 861 bytes MD5 checksum d225c0249912bec4b11c41a0a52e6418 Compiled from "ByteCodeDemo.java" public class java8.ByteCodeDemo minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #14.#31 // java/lang/Object."":()V #2 = Fieldref #3.#32 // java8/ByteCodeDemo.age:I #3 = Class #33 // java8/ByteCodeDemo #4 = Methodref #3.#34 // java8/ByteCodeDemo." ":(I)V #5 = Fieldref #35.#36 // java/lang/System.out:Ljava/io/PrintStream; #6 = Class #37 // java/lang/StringBuilder #7 = Methodref #6.#31 // java/lang/StringBuilder." ":()V #8 = String #38 // name:xiaomingage: #9 = Methodref #6.#39 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #10 = Methodref #3.#40 // java8/ByteCodeDemo.getAge:()I #11 = Methodref #6.#41 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; #12 = Methodref #6.#42 // java/lang/StringBuilder.toString:()Ljava/lang/String; #13 = Methodref #43.#44 // java/io/PrintStream.println:(Ljava/lang/String;)V #14 = Class #45 // java/lang/Object #15 = Utf8 name #16 = Utf8 Ljava/lang/String; #17 = Utf8 ConstantValue #18 = String #46 // xiaoming #19 = Utf8 age #20 = Utf8 I #21 = Utf8 #22 = Utf8 (I)V #23 = Utf8 Code #24 = Utf8 LineNumberTable #25 = Utf8 getAge #26 = Utf8 ()I #27 = Utf8 main #28 = Utf8 ([Ljava/lang/String;)V #29 = Utf8 SourceFile #30 = Utf8 ByteCodeDemo.java #31 = NameAndType #21:#47 // " ":()V #32 = NameAndType #19:#20 // age:I #33 = Utf8 java8/ByteCodeDemo #34 = NameAndType #21:#22 // " ":(I)V #35 = Class #48 // java/lang/System #36 = NameAndType #49:#50 // out:Ljava/io/PrintStream; #37 = Utf8 java/lang/StringBuilder #38 = Utf8 name:xiaomingage: #39 = NameAndType #51:#52 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #40 = NameAndType #25:#26 // getAge:()I #41 = NameAndType #51:#53 // append:(I)Ljava/lang/StringBuilder; #42 = NameAndType #54:#55 // toString:()Ljava/lang/String; #43 = Class #56 // java/io/PrintStream #44 = NameAndType #57:#58 // println:(Ljava/lang/String;)V #45 = Utf8 java/lang/Object #46 = Utf8 xiaoming #47 = Utf8 ()V #48 = Utf8 java/lang/System #49 = Utf8 out #50 = Utf8 Ljava/io/PrintStream; #51 = Utf8 append #52 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #53 = Utf8 (I)Ljava/lang/StringBuilder; #54 = Utf8 toString #55 = Utf8 ()Ljava/lang/String; #56 = Utf8 java/io/PrintStream #57 = Utf8 println #58 = Utf8 (Ljava/lang/String;)V { private static final java.lang.String name; descriptor: Ljava/lang/String; flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL ConstantValue: String xiaoming private int age; descriptor: I flags: ACC_PRIVATE public java8.ByteCodeDemo(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: invokespecial #1 // Method java/lang/Object." ":()V 4: aload_0 5: iload_1 6: putfield #2 // Field age:I 9: return LineNumberTable: line 18: 0 line 19: 4 line 20: 9 public int getAge(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field age:I 4: ireturn LineNumberTable: line 23: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=1 0: new #3 // class java8/ByteCodeDemo 3: dup 4: bipush 12 6: invokespecial #4 // Method " ":(I)V 9: astore_1 10: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 13: new #6 // class java/lang/StringBuilder 16: dup 17: invokespecial #7 // Method java/lang/StringBuilder." ":()V 20: ldc #8 // String name:xiaomingage: 22: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 25: aload_1 26: invokevirtual #10 // Method getAge:()I 29: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 32: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 35: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 38: return LineNumberTable: line 27: 0 line 28: 10 line 29: 38 } SourceFile: "ByteCodeDemo.java"
如果你是第一次用javap,那你一定會(huì)覺(jué)得這個(gè)是啥又臭又長(zhǎng),別著急下面我會(huì)一句一句給你翻譯,這里你需要對(duì)照上面的字節(jié)碼指令,一步一步的帶你翻譯。
3.4.附加信息Classfile /Users/lizhao/Documents/RPC/test/src/main/java/java8/ByteCodeDemo.class //輸出了我們的class文件的完整路徑 Last modified 2018-5-8; size 861 bytes //以及class文件修改時(shí)間以及大小 MD5 checksum d225c0249912bec4b11c41a0a52e6418 //md5校驗(yàn)和 Compiled from "ByteCodeDemo.java" //從哪個(gè)文件編譯而來(lái) public class java8.ByteCodeDemo minor version: 0 major version: 52 //java主版本 major_version.minor_version 組成我們的版本號(hào)52.0 flags: ACC_PUBLIC, ACC_SUPER //public,ACC_SUPER用于兼容早期編譯器,新編譯器都設(shè)置該標(biāo)記,以在使用 invokespecial指令時(shí)對(duì)子類方法做特定處理。 Constant pool: #1 = Methodref #14.#31 // java/lang/Object."":()V #2 = Fieldref #3.#32 // java8/ByteCodeDemo.age:I #3 = Class #33 // java8/ByteCodeDemo .........
部分信息在后面已經(jīng)注釋解釋,
我們主要來(lái)說(shuō)一下我們的Constant pool,常量池:
在Java字節(jié)碼中,有一個(gè)常量池,用來(lái)存放不同類型的常量。由于Java設(shè)計(jì)的目的之一就是字節(jié)碼需要經(jīng)網(wǎng)絡(luò)傳輸?shù)?,因而字?jié)碼需要比較緊湊,以減少網(wǎng)絡(luò)傳輸?shù)牧髁亢蜁r(shí)間。常量池的存在則可以讓一些相同類型的值通過(guò)索引(引用)的方式從常量池中找到,而不是在不同地方有不同拷貝,縮減了字節(jié)碼的大小。
tag中表示的數(shù)據(jù)類型,有如下11種,:
CONSTANT_Class_info?????????????????????????????????
CONSTANT_Integer_info??????????????????????????????
CONSTANT_Longinfo???????????????????????????????????
CONSTANT_Float_info??????????????????????????????????
CONSTANT_Double_info??????????????????????????????
CONSTANT_String_info?????????????????????????????????
CONSTANT_Fieldref_info??????????????????????????????
CONSTANT_Methodref_info???????????????????????
CONSTANT_InterfaceMethodref_info??????
CONSTANT_NameAndType_info????????????????
CONSTANT_Utf8_info???????????????????????????????????
注:在Java字節(jié)碼中,所有boolean、byte、char、short類型都是用int類型存放,因而在常量池中沒(méi)有和它們對(duì)應(yīng)的項(xiàng)。
有關(guān)常量池的介紹可以參照這里:
http://www.blogjava.net/DLevi...
3.5.main方法分析這里把main方法多帶帶復(fù)制了出來(lái),每一句話都進(jìn)行了解釋。
在看下面之前,可以自己嘗試一下是否能將main方法字節(jié)碼看懂
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V //方法描述,入?yún)⑹荢tring,返回是void flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=1 //棧深最大3,局部變量2,args_size入?yún)⑹?(如果是實(shí)體方法會(huì)把this也算入?yún)? 0: new #3 // class java8/ByteCodeDemo new指令創(chuàng)建對(duì)象,這里引用了常量池的class 所以這里一共占了三行 2個(gè)字節(jié)是class //一個(gè)字節(jié)是new,所以下個(gè)行號(hào)是 0+3 = 3 并把當(dāng)前申請(qǐng)的空間地址放到棧頂 3: dup //將棧頂cpoy一份再次放入棧頂,也就是我們上面的空間地址 4: bipush 12 //取常量12放入??臻g 6: invokespecial #4 // Method "":(I)V //執(zhí)行初始化方法這個(gè)時(shí)候會(huì)用到4的棧頂,和3的棧頂,彈出 9: astore_1 //將棧頂放入局部變量,也就是0的空間地址,這個(gè)時(shí)候棧是空的 10: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; //獲取這個(gè)方法地址到棧頂 13: new #6 // class java/lang/StringBuilder 把新開(kāi)辟的空間地址放到棧頂 16: dup //復(fù)制一份 17: invokespecial #7 // Method java/lang/StringBuilder." ":()V //彈出棧頂 20: ldc #8 // String name:xiaomingage://取常量到棧頂 22: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;//彈出棧頂兩個(gè)元素,壓入StringBuilder的引用 25: aload_1 // 把局部變量,也就是我們剛才的空間地址壓入 26: invokevirtual #10 // Method getAge:()I //彈出棧頂,獲取年齡,把年齡壓入棧頂 29: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;//彈出棧頂兩個(gè)元素,壓入StringBuilder 32: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;//彈出棧頂兩個(gè)元素,壓入toString 35: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V//彈出棧頂兩個(gè)元素,此時(shí)棧空 38: return //返回 LineNumberTable: //字節(jié)碼偏移量到源代碼行號(hào)之間的聯(lián)系 line 29: 0 line 30: 10 line 31: 38 }
思考:這里看懂了之后,大家可以自己嘗試下自己寫(xiě)個(gè)稍微復(fù)雜的字節(jié)碼,然后進(jìn)行理解,加深一下映像。最后
下一篇預(yù)告,下一篇將會(huì)給大家詳細(xì)講解如何通過(guò)asm去操作字節(jié)碼,以及如何去實(shí)現(xiàn)我們上面的功能,喜歡這一系列可以關(guān)注公眾號(hào),不丟失文章
如果大家覺(jué)得這篇文章對(duì)你有幫助,或者想提前獲取后續(xù)章節(jié)文章,或者你有什么疑問(wèn)想提供1v1免費(fèi)vip服務(wù),都可以關(guān)注我的公眾號(hào):
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/76416.html
摘要:從字節(jié)碼的分析可以觀察到一個(gè)有趣的現(xiàn)象,再次看看我們的語(yǔ)句。這張表里每行的后面的數(shù)字代表源代碼的序號(hào),冒號(hào)后面的數(shù)字代表字節(jié)碼里每行指令的序號(hào)。維護(hù)了源代碼同字節(jié)指令的映射關(guān)系,確保了代碼調(diào)試的順利進(jìn)行。 javap是JDK自帶的工具: showImg(https://segmentfault.com/img/remote/1460000016730237); 這篇文章使用下面這段簡(jiǎn)單...
摘要:回溯法是一種選優(yōu)搜索法,按選優(yōu)條件向前搜索,以達(dá)到目標(biāo)。但當(dāng)探索到某一步時(shí),發(fā)現(xiàn)原先選擇并不優(yōu)或達(dá)不到目標(biāo),就退回一步重新選擇,這種走不通就退回再走的技術(shù)為回溯法。代碼請(qǐng)點(diǎn)這里這里有一個(gè)示例,展示下用回溯法怎么找到這些形狀的。 說(shuō)明 canvas元素標(biāo)簽強(qiáng)大之處在于可以直接在HTML上進(jìn)行圖形操作,具有極大的應(yīng)用價(jià)值。 canvas 可以實(shí)現(xiàn)對(duì)圖像的像素操作,這就要說(shuō)到 getImag...
摘要:我們的目標(biāo)是用爬蟲(chóng)來(lái)干一件略污事情最近聽(tīng)說(shuō)煎蛋上有好多可愛(ài)的妹子,而且爬蟲(chóng)從妹子圖抓起練手最好,畢竟動(dòng)力大嘛。服務(wù)器超載尤其是對(duì)給定服務(wù)器的訪問(wèn)過(guò)高時(shí)。個(gè)人爬蟲(chóng),如果過(guò)多的人使用,可能導(dǎo)致網(wǎng)絡(luò)或者服務(wù)器阻塞。 我們的目標(biāo)是用爬蟲(chóng)來(lái)干一件略污事情 最近聽(tīng)說(shuō)煎蛋上有好多可愛(ài)的妹子,而且爬蟲(chóng)從妹子圖抓起練手最好,畢竟動(dòng)力大嘛。而且現(xiàn)在網(wǎng)絡(luò)上的妹子很黃很暴力,一下接受太多容易營(yíng)養(yǎng)不量,但是本著...
摘要:托某奇藝選秀節(jié)目的福,嘻哈無(wú)疑是這個(gè)夏天最熱的音樂(lè)標(biāo)簽。年初的時(shí)候,我們教室實(shí)習(xí)生小還是網(wǎng)易云上一只憂郁的民謠狗。沒(méi)想到一夜之間,他的歌單已被占據(jù),儼然一只黑怕老炮兒。最后,本人親自演示,如何用搜出來(lái)的雙押詞匯加上一段簡(jiǎn)單做出一段嘻哈歌曲。 showImg(https://segmentfault.com/img/bVUqGp?w=549&h=300); 托某奇藝選秀節(jié)目的福,嘻哈無(wú)疑...
摘要:原文去年,我寫(xiě)了一篇關(guān)于優(yōu)秀資源之獲取優(yōu)秀資源的博文。在谷歌瀏覽器的團(tuán)隊(duì)中,每天的工作是整天修補(bǔ)并了解哪些是可行的,哪些是沒(méi)有用的。你需要真正利用在中的特性,不用想就知道你將得到很多來(lái)源于各種寫(xiě)作者,包括谷歌瀏覽器團(tuán)隊(duì)在內(nèi)的資源。 原文:http://code.tutsplus.com/articles/resources-for-staying-on-top-of-javascrip...
閱讀 706·2023-04-25 18:37
閱讀 2852·2021-10-12 10:12
閱讀 8526·2021-09-22 15:07
閱讀 616·2019-08-30 15:55
閱讀 3230·2019-08-30 15:44
閱讀 2252·2019-08-30 15:44
閱讀 1684·2019-08-30 13:03
閱讀 1615·2019-08-30 12:55