整合營銷服務(wù)商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          教你用Java字節(jié)碼做點有趣的事

          教你用Java字節(jié)碼做點有趣的事

          .寫在前面

          為什么會寫這篇文章呢?主要是之前調(diào)研過日志脫敏相關(guān)的一些,具體可以參考LOG4j脫敏插件如何編寫 里面描述了日志脫敏插件編寫方法:

        1. 直接在toString中修改代碼,這種方法很麻煩,效率低,需要修改每一個要脫敏的類,或者寫個idea插件自動修改toString(),這樣不好的地方在于所有編譯器都需要開個插件,不夠通用。
        2. 在編譯時期修改抽象語法樹修改toString()方法,就像類似Lombok一樣,這個之前調(diào)研過,開發(fā)難度較大,可能后會更新如何去寫。
        3. 在加載的時候通過實現(xiàn)Instrumentation接口 asm庫,修改class文件的字節(jié)碼,但是有個比較麻煩的地方在于需要給jvm加上啟動參數(shù) -javaagent:agentjarpath,這個已經(jīng)實現(xiàn)了,但是實現(xiàn)后發(fā)現(xiàn)的確不夠通用。
        4. 其中二三兩個已經(jīng)實現(xiàn)了,開發(fā)這個的確比較有趣,自己的知識面也得到了擴展,后續(xù)會通過寫4-5篇的文章,一步一步的帶大家如何去實現(xiàn)這些有趣的工具,學(xué)會了之后,通過大家豐富的想象力相信能實現(xiàn)更多有意思的東西。

          0.1字節(jié)碼能干什么

          例如我這篇文章要介紹的通過修改字節(jié)碼去實現(xiàn)日志脫敏,其實就是修改toString的字節(jié)碼: 可以看看怎么用:

          @Desensitized
          public class StreamDemo1 {
          
          
              private User user;
              @DesFiled(MobileDesFilter.class)
              private String name;
              private String idCard;
              @DesFiled(AddressDesFilter.class)
              private List<String> mm;
              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("北京是朝陽區(qū)打撒所大所大","北京是朝陽區(qū)打撒所大所大"));
                  System.out.println(streamDemo1);
              }
              
              @Override
              public String toString() {
                  return "StreamDemo1{" +
                          "user=" + user +
                          ", name='" + name + '\'' +
                          ", idCard='" + idCard + '\'' +
                          ", mm=" + mm +
                          '}';
              }
          }
          復(fù)制代碼

          這個類很普通對吧,和其他的實體類,唯一的區(qū)別是多了一個注解: @DesFiled(MobileDesFilter.class),有了這個注解我們執(zhí)行這個main方法:他會輸出:

          StreamDemo1{user=bean.User@22d8cfe0, name='184****4777', idCard='22321321321', mm=[北京是朝陽區(qū)打*****, 北京是朝陽區(qū)打*****]}
          復(fù)制代碼

          可以看見我們明明輸入的是不帶號的手機號,為什么輸出卻帶號了呢,這就是操縱字節(jié)碼的神奇。當(dāng)然大家也可以自己擴展思維,你可以用他來做aop切面,當(dāng)然cglib做切面的確也是操縱的字節(jié)碼,你也可以用它來做你想讓它做的事

          0.2語法樹

          另一方面我也調(diào)研了lombok的實現(xiàn),對此我發(fā)現(xiàn)修改抽象語法樹,似乎更加有趣,你可以想象,你平時是否重復(fù)的給每個方法打印入?yún)⒊鰠ⅲ臅r耗力?你平時是否在為缺少關(guān)鍵的日志而感到想罵人?你平時是否害怕用寫AOP用反射打日志會影響性能?為了解決這個問題做了一個意思的工具slothLog,github地址:slothlog github https://github.com/lzggsimida123/slothlog.git (當(dāng)然也求各位大佬們給點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);
              }
          }
          復(fù)制代碼

          通過上面會輸出以下信息,將方法的出參,入?yún)⒍歼M行輸出,脫離了調(diào)試時缺少日志的苦惱

          [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
          復(fù)制代碼

          后續(xù)我會一步一步的教大家如何去完成一個類似Lombok的修改語法樹的框架,做更多有趣的事。

          0.3關(guān)于本篇

          如果你不喜歡上面這些東西,也別著急,字節(jié)碼是java的基礎(chǔ),我覺得是所有Java程序員需要必備的,當(dāng)然你也有必要了解一下。 本篇是系列的第一篇,這篇主要講的主要是字節(jié)碼是什么,通過對這篇的了解,也是后續(xù)章節(jié)的基礎(chǔ)。

          1.什么是字節(jié)碼?

          1.1機器碼

          機器碼(machine code)顧名思義也就是,機器能識別的代碼,也叫原生碼。機器碼是CPU可直接解讀的指令。機器碼與硬件等有關(guān),不同的CPU架構(gòu)支持的硬件碼也不相同。機器碼是和我們的底層硬件直接打交道,現(xiàn)在學(xué)的人也是逐漸地變少了,如果對這個感興趣的同學(xué)可以去學(xué)習(xí)一下匯編,匯編的指令會被翻譯成機器碼。

          1.2字節(jié)碼

          字節(jié)碼(Byte-code)是一種包含執(zhí)行程序、由一序列 op 代碼/數(shù)據(jù)對組成的二進制文件。字節(jié)碼是程序的中間表示形式:介于人類可讀的源碼和機器碼之間。它經(jīng)常被看作是包含一個執(zhí)行程序的二進制文件,更像一個對象模型。字節(jié)碼被這樣叫是因為通常每個操作碼 是一字節(jié)長,所以字節(jié)碼的程度是根據(jù)一字節(jié)來的。字節(jié)碼也是由,一組操作碼組成,而操作碼實際上是對棧的操作,可以移走參數(shù)和地址空間,也可以放入結(jié)果。JAVA通過JIT(即時編譯)可以將字節(jié)碼轉(zhuǎn)換為機器碼。

          字節(jié)碼的實現(xiàn)方式是通過編譯器和虛擬機器。編譯器將源碼編譯成字節(jié)碼,特定平臺上的虛擬機器將字節(jié)碼轉(zhuǎn)譯為可以直接執(zhí)行的指令。在java中一般是用Javac編譯源文件變成字節(jié)碼,也就是我們的class文件。

          從網(wǎng)絡(luò)上找到了兩張圖片,下面是java源碼編譯器生成字節(jié)碼過程:

          java虛擬機執(zhí)行引擎過程,這里會分為兩個階段:

        5. 普通的代碼(非熱)都是走的字節(jié)碼解釋器
        6. 熱代碼:多次調(diào)用的方法,多次執(zhí)行的循環(huán)體,會被JIT優(yōu)化成機器碼。
        7. ?2.字節(jié)碼執(zhí)行

          2.1JVM楨棧結(jié)構(gòu):

          方法調(diào)用在JVM中轉(zhuǎn)換成的是字節(jié)碼執(zhí)行,字節(jié)碼指令執(zhí)行的數(shù)據(jù)結(jié)構(gòu)就是棧幀(stack frame)。也就是在虛擬機棧中的棧元素。虛擬機會為每個方法分配一個棧幀,因為虛擬機棧是LIFO(后進先出)的,所以當(dāng)前線程正在活動的棧幀,也就是棧頂?shù)臈琂VM規(guī)范中稱之為“CurrentFrame”,這個當(dāng)前棧幀對應(yīng)的方法就是“CurrentMethod”。字節(jié)碼的執(zhí)行操作,指的就是對當(dāng)前棧幀數(shù)據(jù)結(jié)構(gòu)進行的操作。

          JVM的運行時數(shù)據(jù)區(qū)的結(jié)構(gòu)如下圖:

          ?我們這里主要討論棧幀的數(shù)據(jù)結(jié)構(gòu):有四個部分,局部變量區(qū),操作數(shù)棧,動態(tài)鏈接,方法的返回地址。

          2.1.1局部變量表:

          局部變量表是一組變量值存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。在Java程序被編譯成Class文件時,就在Code屬性中l(wèi)ocals變量:

          如下面代碼反編譯后就能看見locals=5。

          ?局部變量的容量以變量槽(Slot)為最小單位,32位虛擬機中一個Slot可以存放一個32位以內(nèi)的數(shù)據(jù)類型(boolean、byte、char、short、int、float、reference(引用)和returnAddress八種)。

          同時Slot對對象的引用會影響GC,(要是被引用,不會被回收)。

          系統(tǒng)不會為局部變量賦予初始值,也就是說不存在類變量那樣的準(zhǔn)備階段。

          虛擬機是使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過程的,如果是實例方法(非static),那么局部變量表的第0位索引的Slot默認(rèn)是用于傳遞方法所屬對象實例的引用,在方法中通過this訪問。

          我們上面的代碼中是4個Int的solt加一個this 的solt所以就等于5。

          2.1.2操作數(shù)棧

          Java虛擬機的解釋執(zhí)行引擎被稱為"基于棧的執(zhí)行引擎",其中所指的棧就是指-操作數(shù)棧。

          操作數(shù)棧同局部變量表一樣,也是編譯期間就能決定了其存儲空間(最大的單位長度),通過 Code屬性存儲在類或接口的字節(jié)流中。操作數(shù)棧也是個LIFO棧。 它不是通過索引來訪問,而是通過標(biāo)準(zhǔn)的棧操作—壓棧和出棧—來訪問的。比如,如果某個指令把一個值壓入到操作數(shù)棧中,稍后另一個指令就可以彈出這個值來使用。

          虛擬機在操作數(shù)棧中存儲數(shù)據(jù)的方式和在局部變量區(qū)中是一樣的:如int、long、float、double、reference和returnType的存儲。對于byte、short以及char類型的值在壓入到操作數(shù)棧之前,也會被轉(zhuǎn)換為int。

          2.1.3動態(tài)鏈接

          動態(tài)鏈接就是將符號引用所表示的方法,轉(zhuǎn)換成方法的直接引用。加載階段或第一次使用時轉(zhuǎn)化為直接引用的(將變量的訪問轉(zhuǎn)化為訪問這些變量的存儲結(jié)構(gòu)所在的運行時內(nèi)存位置)就叫做靜態(tài)解析。JVM的動態(tài)鏈接還支持運行期轉(zhuǎn)化為直接引用。也可以叫做Late Binding,晚期綁定。動態(tài)鏈接是java靈活OO的基礎(chǔ)結(jié)構(gòu)。

          注:

          符號引用就是字符串,這個字符串包含足夠的信息,以供實際使用時可以找到相應(yīng)的位置。你比如說某個方法的符號引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有類的信息,方法名,方法參數(shù)等信息。

          當(dāng)?shù)谝淮芜\行時,要根據(jù)字符串的內(nèi)容,到該類的方法表中搜索這個方法。運行一次之后,符號引用會被替換為直接引用,下次就不用搜索了。直接引用就是偏移量,通過偏移量虛擬機可以直接在該類的內(nèi)存區(qū)域中找到方法字節(jié)碼的起始位置。重寫就是動態(tài)鏈接,重載就是靜態(tài)解析。

          2.1.4方法返回地址

          **方法正常退出,JVM執(zhí)行引擎會恢復(fù)上層方法局部變量表操作數(shù)棧并把返回值壓入調(diào)用者的棧幀的操作數(shù)棧,PC計數(shù)器的值就會調(diào)整到方法調(diào)用指令后面的一條指令。**這樣使得當(dāng)前的棧幀能夠和調(diào)用者連接起來,并且讓調(diào)用者的棧幀的操作數(shù)棧繼續(xù)往下執(zhí)行。 ??方法的異常調(diào)用完成,如果異常沒有被捕獲住,或者遇到athrow字節(jié)碼指令顯示拋出,那么就沒有返回值給調(diào)用者。

          2.2字節(jié)碼指令集

          2.2.1加載和存儲指令

          加載和存儲指令用于將數(shù)據(jù)從棧幀的局部變量表和操作數(shù)棧之間來回傳輸。

          1)將一個局部變量加載到操作數(shù)棧的指令包括:iload,iload_,lload、lload、float、 fload_、dload、dload_,aload、aload。

          2)將一個數(shù)值從操作數(shù)棧存儲到局部變量表的指令: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

          2.2.2運算指令

          算術(shù)指令用于對兩個操作數(shù)棧上的值進行某種特定運算,并把結(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虛擬機沒有明確規(guī)定整型數(shù)據(jù)溢出的情況,但規(guī)定了處理整型數(shù)據(jù)時,只有除法和求余指令出現(xiàn)除數(shù)為0時會導(dǎo)致虛擬機拋出異常。

          Java虛擬機要求在浮點數(shù)運算的時候,所有結(jié)果否必須舍入到適當(dāng)?shù)木龋绻袃煞N可表示的形式與該值一樣,會優(yōu)先選擇最低有效位為零的。稱之為最接近數(shù)舍入模式。

          浮點數(shù)向整數(shù)轉(zhuǎn)換的時候,Java虛擬機使用IEEE 754標(biāo)準(zhǔn)中的向零舍入模式,這種模式舍入的結(jié)果會導(dǎo)致數(shù)字被截斷,所有小數(shù)部分的有效字節(jié)會被丟掉。

          2.2.3類型轉(zhuǎn)換指令

          類型轉(zhuǎn)換指令將兩種Java虛擬機數(shù)值類型相互轉(zhuǎn)換,這些操作一般用于實現(xiàn)用戶代碼的顯式類型轉(zhuǎn)換操作。JVM直接就支持寬化類型轉(zhuǎn)換(小范圍類型向大范圍類型轉(zhuǎn)換):

          1.int類型到long,float,double類型

          2.long類型到float,double類型

          3.float到double類型

          但在處理窄化類型轉(zhuǎn)換時,必須顯式使用轉(zhuǎn)換指令來完成,這些指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和 d2f。將int 或 long 窄化為整型T的時候,僅僅簡單的把除了低位的N個字節(jié)以外的內(nèi)容丟棄,N是T的長度。這有可能導(dǎo)致轉(zhuǎn)換結(jié)果與輸入值有不同的正負(fù)號。

          在將一個浮點值窄化為整數(shù)類型T(僅限于 int 和 long 類型),將遵循以下轉(zhuǎn)換規(guī)則:

          1)如果浮點值是NaN , 那轉(zhuǎn)換結(jié)果就是int 或 long 類型的0

          2)如果浮點值不是無窮大,浮點值使用IEEE 754 的向零舍入模式取整,獲得整數(shù)v, 如果v在T表示范圍之內(nèi),那就是v

          3)否則,根據(jù)v的符號, 轉(zhuǎn)換為T 所能表示的最大或者最小整數(shù)

          2.2.4對象創(chuàng)建和訪問指令

          雖然類實例和數(shù)組都是對象,Java虛擬機對類實例和數(shù)組的創(chuàng)建與操作使用了不同的字節(jié)碼指令。

          1)創(chuàng)建實例的指令: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ù)值存儲到數(shù)組元素中執(zhí)行:bastore,castore,castore,sastore,iastore,fastore,dastore,aastore

          6)取數(shù)組長度指令:arraylength JVM支持方法級同步和方法內(nèi)部一段指令序列同步,這兩種都是通過moniter實現(xiàn)的。

          7)檢查實例類型指令:instanceof,checkcast

          2.2.5操作數(shù)棧管理指令

          如同操作一個普通數(shù)據(jù)結(jié)構(gòu)中的堆棧那樣,Java虛擬機提供了一些用于直接操作操作數(shù)據(jù)的指令,包括:

          1)將操作數(shù)棧的棧頂一個或兩個元素出棧:pop、pop2

          2)復(fù)制棧頂一個或兩個數(shù)值并將復(fù)制值或雙份的復(fù)制值重新壓入棧頂:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。

          3)將棧最頂端的兩個數(shù)值互換:swap

          2.2.6控制轉(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,ret

          JVM中有專門的指令集處理int和reference類型的條件分支比較操作,為了可以無明顯標(biāo)示一個實體值是否是null,有專門的指令檢測null 值。boolean類型和byte類型,char類型和short類型的條件分支比較操作,都使用int類型的比較指令完成,而 long,float,double條件分支比較操作,由相應(yīng)類型的比較運算指令,運算指令會返回一個整型值到操作數(shù)棧中,隨后再執(zhí)行int類型的條件比較操作完成整個分支跳轉(zhuǎn)。各種類型的比較都最終會轉(zhuǎn)化為int類型的比較操作。

          2.2.7方法調(diào)用和返回指令

          invokevirtual指令:調(diào)用對象的實例方法,根據(jù)對象的實際類型進行分派(虛擬機分派)。

          invokeinterface指令:調(diào)用接口方法,在運行時搜索一個實現(xiàn)這個接口方法的對象,找出合適的方法進行調(diào)用。

          invokespecial:調(diào)用需要特殊處理的實例方法,包括實例初始化方法,私有方法和父類方法

          invokestatic:調(diào)用類方法(static)

          方法返回指令是根據(jù)返回值的類型區(qū)分的,包括ireturn(返回值是boolean,byte,char,short和 int),lreturn(long),freturn,drturn(double)和areturn(引用地址),另外一個return供void方法,實例初始化方法,類和接口的類初始化i方法使用。

          2.2.8異常處理指令

          在Java程序中顯式拋出異常的操作(throw語句)都有athrow 指令來實現(xiàn),除了用throw 語句顯示拋出異常情況外,Java虛擬機規(guī)范還規(guī)定了許多運行時異常會在其他Java虛擬機指令檢測到異常狀況時自動拋出。在Java虛擬機中,處理異常不是由字節(jié)碼指令來實現(xiàn)的,而是采用異常表來完成的。

          2.2.9同步指令

          方法級的同步是隱式的,無需通過字節(jié)碼指令來控制,它實現(xiàn)在方法調(diào)用和返回操作中。虛擬機從方法常量池中的方法標(biāo)結(jié)構(gòu)中的 ACC_SYNCHRONIZED標(biāo)志區(qū)分是否是同步方法。方法調(diào)用時,調(diào)用指令會檢查該標(biāo)志是否被設(shè)置,若設(shè)置,執(zhí)行線程持有moniter,然后執(zhí)行方法,最后完成方法時釋放moniter。同步一段指令集序列,通常由synchronized塊表示,JVM指令集中有monitorenter和monitorexit來支持synchronized語義。

          大多數(shù)的指令有前綴和(或)后綴來表明其操作數(shù)的類型。

          3.字節(jié)碼實例分析

          這一節(jié)將給大家分析如何一步一步的分析字節(jié)碼。

          3.1源代碼

          有如下簡單代碼,下面代碼是一個簡單的demo,有一個常量,有一個類成員變量,同時方法有三個,一個構(gòu)造方法,一個get(),一個靜態(tài)main方法,用來輸出信息。

          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());
              }
          }
          復(fù)制代碼

          3.2.反編譯

          用命令行找到我們這段代碼所在的路徑,輸入如下命令:

          javac ByteCodeDemo.java
          
          javap -p -v ByteCodeDemo
          復(fù)制代碼

          有關(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."<init>":()V
             #2=Fieldref           #3.#32         // java8/ByteCodeDemo.age:I
             #3=Class              #33            // java8/ByteCodeDemo
             #4=Methodref          #3.#34         // java8/ByteCodeDemo."<init>":(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."<init>":()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               <init>
            ....省略部分
            #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."<init>":()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 "<init>":(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."<init>":()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"
          復(fù)制代碼

          如果你是第一次用javap,那你一定會覺得這個是啥又臭又長,別著急下面我會一句一句給你翻譯,這里你需要對照上面的字節(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文件修改時間以及大小
            MD5 checksum d225c0249912bec4b11c41a0a52e6418 //md5校驗和
            Compiled from "ByteCodeDemo.java" //從哪個文件編譯而來
          public class java8.ByteCodeDemo 
            minor version: 0
            major version: 52 //java主版本  major_version.minor_version 組成我們的版本號52.0
            flags: ACC_PUBLIC, ACC_SUPER //public,ACC_SUPER用于兼容早期編譯器,新編譯器都設(shè)置該標(biāo)記,以在使用 invokespecial指令時對子類方法做特定處理。
          Constant pool:
             #1=Methodref          #14.#31        // java/lang/Object."<init>":()V
             #2=Fieldref           #3.#32         // java8/ByteCodeDemo.age:I
             #3=Class              #33            // java8/ByteCodeDemo
             .........
          復(fù)制代碼

          部分信息在后面已經(jīng)注釋解釋, 我們主要來說一下我們的Constant pool,常量池:

          在Java字節(jié)碼中,有一個常量池,用來存放不同類型的常量。由于Java設(shè)計的目的之一就是字節(jié)碼需要經(jīng)網(wǎng)絡(luò)傳輸?shù)模蚨止?jié)碼需要比較緊湊,以減少網(wǎng)絡(luò)傳輸?shù)牧髁亢蜁r間。常量池的存在則可以讓一些相同類型的值通過索引(引用)的方式從常量池中找到,而不是在不同地方有不同拷貝,縮減了字節(jié)碼的大小。

          tag中表示的數(shù)據(jù)類型,有如下11種,:

        8. CONSTANT_Class_info
        9. CONSTANT_Integer_info
        10. CONSTANT_Long\info
        11. CONSTANT_Float_info
        12. CONSTANT_Double_info
        13. CONSTANT_String_info
        14. CONSTANT_Fieldref_info
        15. CONSTANT_Methodref_info
        16. CONSTANT_InterfaceMethodref_info
        17. CONSTANT_NameAndType_info
        18. CONSTANT_Utf8_info
        19. 注:在Java字節(jié)碼中,所有boolean、byte、char、short類型都是用int類型存放,因而在常量池中沒有和它們對應(yīng)的項。 有關(guān)常量池的介紹可以參照這里:

          http://www.blogjava.net/DLevin/archive/2011/09/05/358033.html

          3.5.main方法分析

          這里把main方法單獨復(fù)制了出來,每一句話都進行了解釋。

          在看下面之前,可以自己嘗試一下是否能將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)⑹?(如果是實體方法會把this也算入?yún)?
                   0: new           #3                  // class java8/ByteCodeDemo new指令創(chuàng)建對象,這里引用了常量池的class 所以這里一共占了三行 2個字節(jié)是class 
                   //一個字節(jié)是new,所以下個行號是 0+3=3 并把當(dāng)前申請的空間地址放到棧頂
                   3: dup 															//將棧頂cpoy一份再次放入棧頂,也就是我們上面的空間地址
                   4: bipush        12									//取常量12放入棧空間
                   6: invokespecial #4                  // Method "<init>":(I)V //執(zhí)行初始化方法這個時候會用到4的棧頂,和3的棧頂,彈出
                   9: astore_1													//將棧頂放入局部變量,也就是0的空間地址,這個時候棧是空的
                  10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream; //獲取這個方法地址到棧頂
                  13: new           #6                  // class java/lang/StringBuilder 把新開辟的空間地址放到棧頂
                  16: dup																//復(fù)制一份
                  17: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V //彈出棧頂
                  20: ldc           #8                  // String name:xiaomingage://取常量到棧頂
                  22: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;//彈出棧頂兩個元素,壓入StringBuilder的引用
                  25: aload_1														// 把局部變量,也就是我們剛才的空間地址壓入
                  26: invokevirtual #10                 // Method getAge:()I //彈出棧頂,獲取年齡,把年齡壓入棧頂
                  29: invokevirtual #11                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;//彈出棧頂兩個元素,壓入StringBuilder
                  32: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;//彈出棧頂兩個元素,壓入toString
                  35: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V//彈出棧頂兩個元素,此時棧空
                  38: return //返回
                LineNumberTable: //字節(jié)碼偏移量到源代碼行號之間的聯(lián)系
                  line 29: 0 
                  line 30: 10
                  line 31: 38
          }
          復(fù)制代碼


          思考:這里看懂了之后,大家可以自己嘗試下自己寫個稍微復(fù)雜的字節(jié)碼,然后進行理解,加深一下映像。

          原文鏈接:https://juejin.cn/post/6844903641489375240

          友們,下午好!

          都說一張美美的圖能為文章增色三分!

          那如果是一個交互的圖片樣式 + 幾張美美圖呢?這能為文章增色多少呢?

          比如這種(樣式ID:90298)

          使用這種樣式,即能有效的展示圖片,還能縮小文章空間,而且還與讀者存在互動交互,想不想知道這種樣式怎么做出來呢?

          上面兩種樣式都可以在樣式中心輸入ID搜索到。

          但是,樣式中心的原樣式,都是四張圖片滑動的,直接進行換圖就可以使用了。

          但如果要像三兒上面做的兩個樣式,一個是5張圖,一個是9張圖,就要進HTML進行修改了。

          教程一(帶圖片說明的樣式)

          進入到“HTML”模式,找到<section .........> </section>這段代碼,先選擇Ctrl+C復(fù)制。

          然后在此段代碼結(jié)尾處敲回車鍵換行,再選擇Ctrl+V粘貼。

          粘貼幾次,樣式就會在原有四張的基礎(chǔ)上多出幾張,胖友們可以根據(jù)自己的需求進行多次粘貼。

          教程二

          進入到“HTML”模式,找到<img src=........./>這段代碼,先選擇Ctrl+C復(fù)制,然后在此段代碼結(jié)尾處,再Ctrl+V粘貼。

          同上個樣式,粘貼幾次,樣式就會在原有四張的基礎(chǔ)上多出幾張,胖友們可以根據(jù)自己的需求進行多次粘貼。

          為了樣式的美感,還是有三點建議給大家。

          1、圖片請保持尺寸一致。否則會導(dǎo)致圖片層次不齊。

          2、尺寸請500x500以上。否則可能會使圖片不清楚。

          3、圖片大小盡可能小點。否則瀏覽時加載會不流暢。

          更多好玩樣式,請進樣式中心搜索“滾動

          好了,本次教程就到這里~bye

          用語義化代碼更直觀,而且更方便SEO優(yōu)化。但是此HTML5新標(biāo)簽在IE6/IE7/IE8上并不能識別,需要進行JavaScript處理。以下就介紹幾種方式。

          方式一:Coding JavaScript

          <!--[if lt IE 9]>
          <script>
             (function() {
               if (! 
               /*@cc_on!@*/
               0) return;
               var e="abbr, article, aside, audio, canvas, datalist, details, dialog, eventsource, figure, footer, header, hgroup, mark, menu, meter, nav, output, progress, section, time, video".split(', ');
               var i=e.length;
               while (i--){
                   document.createElement(e[i])
               } 
            })() 
          </script>
          <![endif]-->

          如果是IE9以下的IE瀏覽器將創(chuàng)建HTML5標(biāo)簽, 這樣非IE瀏覽器就會忽視這段代碼,也就不會有無謂的http請求了。

          方法二:使用Google的html5shiv包(推薦)

          <!--[if lt IE 9]>
            <script type="text/javascript" src="http://cdn.bootcss.com/html5shiv/r29/html5.min.js"></script>
          <![endif]-->

          上面這段代碼僅會在IE瀏覽器下運行,還有一點需要注意,在頁面中調(diào)用html5.js文件必須添加在頁面的head元素內(nèi),因為IE瀏覽器必須在元素解析前知道這個元素,所以這個js文件不能在頁面底部調(diào)用。

          不管使用以上哪種方法,都要初始化新標(biāo)簽的CSS。因為HTML5在默認(rèn)情況下表現(xiàn)為內(nèi)聯(lián)元素,對這些元素進行布局我們需要利用CSS手工把它們轉(zhuǎn)為塊狀元素方便布局

          /*html5*/
          article,aside,dialog,footer,header,section,footer,nav,figure,menu{display:block}

          如果IE6/7/8 禁用腳本的用戶,那么就變成了無樣式的"白板"網(wǎng)頁,我們該怎么解決呢?

          我們可以參照facebook的做法,即引導(dǎo)用戶進入帶有noscript標(biāo)識的 “/?_fb_noscript=1”頁面,用 html4 標(biāo)簽替換 html5 標(biāo)簽,這要比為了保持兼容性而寫大量 hack 的做法更輕便一些。

          <!--[if lte IE 8]>
          <noscript>
            <style>
              .html5-wrappers{display:none!important;}
            </style>
            <div class="ie-noscript-warning">您的瀏覽器禁用了腳本,請<a href="#">查看這里</a>來啟用腳本!或者<a href="/?noscript=1">繼續(xù)訪問</a>.
            </div>
          </noscript>
          <![endif]-->

          這樣可以引導(dǎo)用戶開啟腳本,或者直接跳轉(zhuǎn)到HTML4標(biāo)簽設(shè)計的界面。


          主站蜘蛛池模板: 在线观看精品一区| 亚洲熟女乱综合一区二区| 亚洲一区二区三区不卡在线播放| 红桃AV一区二区三区在线无码AV | 无码丰满熟妇浪潮一区二区AV| 国产精品污WWW一区二区三区| 亚洲色精品VR一区区三区| 中文字幕一区二区三区在线播放| 国产伦精品一区二区三区视频猫咪| 中文字幕一区二区三区四区| 国产精品99精品一区二区三区| 精品一区二区三区免费视频| 日韩精品无码一区二区视频| 亚洲色大成网站www永久一区| av无码人妻一区二区三区牛牛| 欲色影视天天一区二区三区色香欲 | 中文字幕日韩一区二区三区不 | 波多野结衣中文字幕一区二区三区| 亚洲日本va一区二区三区| 亚洲高清毛片一区二区| 亚洲AV无码国产一区二区三区 | 午夜影视日本亚洲欧洲精品一区| 无码欧精品亚洲日韩一区| 精品国产一区二区三区www| 亚洲av高清在线观看一区二区 | 日本精品一区二区三区四区| 精品国产AⅤ一区二区三区4区| 无码人妻精品一区二区三区不卡| 精品福利一区3d动漫| 国产激情视频一区二区三区| 欧美激情国产精品视频一区二区| 一区二区视频免费观看| 精品国产一区二区三区av片| 久久一区不卡中文字幕| 东京热无码av一区二区| 国模吧一区二区三区| 日韩人妻一区二区三区蜜桃视频| 69久久精品无码一区二区| 秋霞鲁丝片一区二区三区| 激情内射日本一区二区三区| 国产在线乱子伦一区二区|