<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          輕松看懂Java字節(jié)碼,看了都說好!

          共 11688字,需瀏覽 24分鐘

           ·

          2020-10-19 16:13

          ? ? ?

          ? ?正文? ?


          /???Java字節(jié)碼? ?/


          計算機(jī)只認(rèn)識0和1。這意味著任何語言編寫的程序最終都需要經(jīng)過編譯器編譯成機(jī)器碼才能被計算機(jī)執(zhí)行。所以,我們所編寫的程序在不同的平臺上運行前都要經(jīng)過重新編譯才能被執(zhí)行。而Java剛誕生的時候曾經(jīng)提過一個非常著名的宣傳口號:?"一次編寫,到處運行"。


          Write Once, Run Anywhere.


          為了實現(xiàn)該目的,Sun公司以及其他虛擬機(jī)提供商發(fā)布了許多可以運行在不同平臺上的JVM虛擬機(jī),而這些虛擬機(jī)都擁有一個共同的功能,那就是可以載入和執(zhí)行同一種與平臺無關(guān)的字節(jié)碼(ByteCode)。?


          于是,我們的源代碼不再必須根據(jù)不同平臺翻譯成0和1,而是間接翻譯成字節(jié)碼,儲存字節(jié)碼的文件再交由運行于不同平臺上的JVM虛擬機(jī)去讀取執(zhí)行,從而實現(xiàn)一次編寫,到處運行的目的。


          如今,JVM也不再只支持Java,由此衍生出了許多基于JVM的編程語言,如Groovy, Scala, Koltin等等。



          源代碼中的各種變量,關(guān)鍵字和運算符號的語義最終都會編譯成多條字節(jié)碼命令。而字節(jié)碼命令所能提供的語義描述能力是要明顯強(qiáng)于Java本身的,所以有其他一些同樣基于JVM的語言能提供許多Java所不支持的語言特性。


          /???例子? ?/


          下面以一個簡單的例子來逐步講解字節(jié)碼。


          //Main.java
          public?class?Main?{

          ????private?int?m;

          ????public?int?inc()?{
          ????????return?m?+?1;
          ????}
          }


          通過以下命令, 可以在當(dāng)前所在路徑下生成一個?Main.class?文件。


          javac?Main.java


          以文本的形式打開生成的class文件,內(nèi)容如下:


          cafe?babe?0000?0034?0013?0a00?0400?0f09
          0003?0010?0700?1107?0012?0100?016d?0100
          0149?0100?063c?696e?6974?3e01?0003?2829
          5601?0004?436f?6465?0100?0f4c?696e?654e
          756d?6265?7254?6162?6c65?0100?0369?6e63
          0100?0328?2949?0100?0a53?6f75?7263?6546
          696c?6501?0009?4d61?696e?2e6a?6176?610c
          0007?0008?0c00?0500?0601?0010?636f?6d2f
          7268?7974?686d?372f?4d61?696e?0100?106a
          6176?612f?6c61?6e67?2f4f?626a?6563?7400
          2100?0300?0400?0000?0100?0200?0500?0600
          0000?0200?0100?0700?0800?0100?0900?0000
          1d00?0100?0100?0000?052a?b700?01b1?0000
          0001?000a?0000?0006?0001?0000?0003?0001
          000b?000c?0001?0009?0000?001f?0002?0001
          0000?0007?2ab4?0002?0460?ac00?0000?0100
          0a00?0000?0600?0100?0000?0800?0100?0d00
          0000?0200?0e


          對于文件中的16進(jìn)制代碼,除了開頭的cafe babe,剩下的內(nèi)容大致可以翻譯成:啥玩意啊這......


          英雄莫慌,我們就從我們所能認(rèn)識的"cafe babe"講起吧。文件開頭的4個字節(jié)稱之為?魔數(shù),唯有以"cafe babe"開頭的class文件方可被虛擬機(jī)所接受,這4個字節(jié)就是字節(jié)碼文件的身份識別。


          目光右移,0000是編譯器jdk版本的次版本號0,0034轉(zhuǎn)化為十進(jìn)制是52,是主版本號,java的版本號從45開始,除1.0和1.1都是使用45.x外,以后每升一個大版本,版本號加一。也就是說,編譯生成該class文件的jdk版本為1.8.0。通過java -version命令稍加驗證, 可得結(jié)果。


          Java(TM)?SE?Runtime?Environment?(build?1.8.0_131-b11)
          Java?HotSpot(TM)?64-Bit?Server?VM?(build?25.131-b11,?mixed?mode)


          結(jié)果驗證成立。


          繼續(xù)往下是常量池。但我并不打算繼續(xù)直接分析這個十六進(jìn)制文件,這樣會比較繁瑣,我們通過另一種更容易讓人看懂的方式來分析這個class文件。


          反編譯字節(jié)碼文件


          使用到j(luò)ava內(nèi)置的一個反編譯工具javap可以反編譯字節(jié)碼文件。通過javap -help可了解javap的基本用法


          用法:?javap?<options>?<classes>
          其中,?可能的選項包括:
          ??-help??--help??-?????????輸出此用法消息
          ??-version?????????????????版本信息
          ??-v??-verbose?????????????輸出附加信息
          ??-l???????????????????????輸出行號和本地變量表
          ??-public ?????????????????僅顯示公共類和成員
          ??-protected ??????????????顯示受保護(hù)的/公共類和成員
          ??-package?????????????????顯示程序包/受保護(hù)的/公共類
          ???????????????????????????和成員?(默認(rèn))
          ??-p??-private?????????????顯示所有類和成員
          ??-c???????????????????????對代碼進(jìn)行反匯編
          ??-s???????????????????????輸出內(nèi)部類型簽名
          ??-sysinfo?????????????????顯示正在處理的類的
          ???????????????????????????系統(tǒng)信息?(路徑,?大小,?日期,?MD5?散列)
          ??-constants???????????????顯示最終常量
          ??-classpath?<path>????????指定查找用戶類文件的位置
          ??-cp?<path>???????????????指定查找用戶類文件的位置
          ??-bootclasspath?<path>????覆蓋引導(dǎo)類文件的位置


          輸入命令javap -verbose -p Main.class查看輸出內(nèi)容:


          Classfile?/E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/Main.class
          ??Last?modified?2018-4-7;?size?362?bytes
          ??MD5?checksum?4aed8540b098992663b7ba08c65312de
          ??Compiled?from?"Main.java"
          public?class?com.rhythm7.Main
          ??minor?version:?0
          ??major?version:?52
          ??flags:?ACC_PUBLIC,?ACC_SUPER
          Constant?pool:
          ???#1?
          =?Methodref??????????#4.#18?????????//?java/lang/Object."":()V
          ???#2?=?Fieldref???????????#3.#19?????????//?com/rhythm7/Main.m:I
          ???#3?=?Class??????????????#20????????????//?com/rhythm7/Main
          ???#4?=?Class??????????????#21????????????//?java/lang/Object
          ???#5?=?Utf8???????????????m
          ???#6?=?Utf8???????????????I
          ???#7?=?Utf8???????????????
          ???#8?=?Utf8???????????????()V
          ???#9?=?Utf8???????????????Code
          ??#10?=?Utf8???????????????LineNumberTable
          ??#11?=?Utf8???????????????LocalVariableTable
          ??#12?=?Utf8???????????????this
          ??#13?=?Utf8???????????????Lcom/rhythm7/Main;
          ??#14?=?Utf8???????????????inc
          ??#15?=?Utf8???????????????()I
          ??#16?=?Utf8???????????????SourceFile
          ??#17?=?Utf8???????????????Main.java
          ??#18?=?NameAndType????????#7:#8??????????//?"":()V
          ??#19?=?NameAndType????????#5:#6??????????//?m:I
          ??#20?=?Utf8???????????????com/rhythm7/Main
          ??#21?=?Utf8???????????????java/lang/Object
          {
          ??private?int?m;
          ????descriptor:?I
          ????flags:?ACC_PRIVATE

          ??public?com.rhythm7.Main();
          ????descriptor:?()V
          ????flags:?ACC_PUBLIC
          ????Code:
          ??????stack=1,?locals=1,?args_size=1
          ?????????0:?aload_0
          ?????????1:?invokespecial?#1??????????????????//?Method?java/lang/Object."":()V
          ?????????4:?return
          ??????LineNumberTable:
          ????????line?3:?0
          ??????LocalVariableTable:
          ????????Start??Length??Slot??Name???Signature
          ????????????0???????5?????0??this???Lcom/rhythm7/Main;

          ??public?int?inc();
          ????descriptor:?()I
          ????flags:?ACC_PUBLIC
          ????Code:
          ??????stack=2,?locals=1,?args_size=1
          ?????????0:?aload_0
          ?????????1:?getfield??????#2??????????????????//?Field?m:I
          ?????????4:?iconst_1
          ?????????5:?iadd
          ?????????6:?ireturn
          ??????LineNumberTable:
          ????????line?8:?0
          ??????LocalVariableTable:
          ????????Start??Length??Slot??Name???Signature
          ????????????0???????7?????0??this???Lcom/rhythm7/Main;
          }
          SourceFile:?"Main.java"


          字節(jié)碼文件信息


          開頭的7行信息包括:Class文件當(dāng)前所在位置,最后修改時間,文件大小,MD5值,編譯自哪個文件,類的全限定名,jdk次版本號,主版本號。然后緊接著的是該類的訪問標(biāo)志:ACC_PUBLIC,?ACC_SUPER,訪問標(biāo)志的含義如下:



          常量池


          Constant pool意為常量池。常量池可以理解成Class文件中的資源倉庫。主要存放的是兩大類常量:字面量(Literal)和符號引用(Symbolic References)。字面量類似于java中的常量概念,如文本字符串,final常量等,而符號引用則屬于編譯原理方面的概念,包括以下三種:


          • 類和接口的全限定名(Fully Qualified Name)
          • 字段的名稱和描述符號(Descriptor)
          • 方法的名稱和描述符

          不同于C/C++, JVM是在加載Class文件的時候才進(jìn)行的動態(tài)鏈接,也就是說這些字段和方法符號引用只有在運行期轉(zhuǎn)換后才能獲得真正的內(nèi)存入口地址。當(dāng)虛擬機(jī)運行時,需要從常量池獲得對應(yīng)的符號引用,再在類創(chuàng)建或運行時解析并翻譯到具體的內(nèi)存地址中。

          直接通過反編譯文件來查看字節(jié)碼內(nèi)容:

          #1?=?Methodref??????????#4.#18?????????//?java/lang/Object."":()V
          #4?=?Class??????????????#21????????????//?java/lang/Object
          #7?=?Utf8???????????????
          #8?=?Utf8???????????????()V
          #18?=?NameAndType????????#7:#8??????????//?"":()V
          #21?=?Utf8???????????????java/lang/Object

          第一個常量是一個方法定義,指向了第4和第18個常量。以此類推查看第4和第18個常量。最后可以拼接成第一個常量右側(cè)的注釋內(nèi)容:

          java/lang/Object."":()V

          這段可以理解為該類的實例構(gòu)造器的聲明,由于Main類沒有重寫構(gòu)造方法,所以調(diào)用的是父類的構(gòu)造方法。此處也說明了Main類的直接父類是Object。該方法默認(rèn)返回值是V, 也就是void,無返回值。

          同理可分析第二個常量:

          #2?=?Fieldref???????????#3.#19?????????//?com/rhythm7/Main.m:I
          #3?=?Class??????????????#20????????????//?com/rhythm7/Main
          #5?=?Utf8???????????????m
          #6?=?Utf8???????????????I
          #19?=?NameAndType????????#5:#6??????????//?m:I
          #20?=?Utf8???????????????com/rhythm7/Main

          此處聲明了一個字段m,類型為I, I即是int類型。關(guān)于字節(jié)碼的類型對應(yīng)如下:


          對于數(shù)組類型,每一位使用一個前置的"["字符來描述,如定義一個java.lang.String[][]類型的維數(shù)組,將被記錄為"[[Ljava/lang/String;"

          方法表集合


          在常量池之后的是對類內(nèi)部的方法描述,在字節(jié)碼中以表的集合形式表現(xiàn),暫且不管字節(jié)碼文件的16進(jìn)制文件內(nèi)容如何,我們直接看反編譯后的內(nèi)容。

          private?int?m;
          ??descriptor:?I
          ??flags:?ACC_PRIVATE

          此處聲明了一個私有變量m,類型為int,返回值為int

          public?com.rhythm7.Main();
          ???descriptor:?()V
          ???flags:?ACC_PUBLIC
          ???Code:
          ?????stack=1,?locals=1,?args_size=1
          ????????0:?aload_0
          ????????1:?invokespecial?#1??????????????????//?Method?java/lang/Object."":()V
          ????????4:?return
          ?????LineNumberTable:
          ???????line?3:?0
          ?????LocalVariableTable:
          ???????Start??Length??Slot??Name???Signature
          ???????????0???????5?????0??this???Lcom/rhythm7/Main;

          這里是構(gòu)造方法:Main(),返回值為void, 公開方法。code內(nèi)的主要屬性為:

          stack?最大操作數(shù)棧,JVM運行時會根據(jù)這個值來分配棧幀(Frame)中的操作棧深度,此處為1

          locals:局部變量所需的存儲空間,單位為Slot, Slot是虛擬機(jī)為局部變量分配內(nèi)存時所使用的最小單位,為4個字節(jié)大小。方法參數(shù)(包括實例方法中的隱藏參數(shù)this),顯示異常處理器的參數(shù)(try catch中的catch塊所定義的異常),方法體中定義的局部變量都需要使用局部變量表來存放。值得一提的是,locals的大小并不一定等于所有局部變量所占的Slot之和,因為局部變量中的Slot是可以重用的。
          args_size:方法參數(shù)的個數(shù),這里是1,因為每個實例方法都會有一個隱藏參數(shù)this

          attribute_info方法體內(nèi)容,0,1,4為字節(jié)碼"行號",該段代碼的意思是將第一個引用類型本地變量推送至棧頂,然后執(zhí)行該類型的實例方法,也就是常量池存放的第一個變量,也就是注釋里的"java/lang/Object."":()V", 然后執(zhí)行返回語句,結(jié)束方法。

          LineNumberTable該屬性的作用是描述源碼行號與字節(jié)碼行號(字節(jié)碼偏移量)之間的對應(yīng)關(guān)系??梢允褂?-g:none 或-g:lines選項來取消或要求生成這項信息,如果選擇不生成LineNumberTable,當(dāng)程序運行異常時將無法獲取到發(fā)生異常的源碼行號,也無法按照源碼的行數(shù)來調(diào)試程序。

          LocalVariableTable該屬性的作用是描述幀棧中局部變量與源碼中定義的變量之間的關(guān)系。可以使用 -g:none 或 -g:vars來取消或生成這項信息,如果沒有生成這項信息,那么當(dāng)別人引用這個方法時,將無法獲取到參數(shù)名稱,取而代之的是arg0, arg1這樣的占位符。start 表示該局部變量在哪一行開始可見,length表示可見行數(shù),Slot代表所在幀棧位置,Name是變量名稱,然后是類型簽名。

          同理可以分析Main類中的另一個方法"inc()": 方法體內(nèi)的內(nèi)容是:將this入棧,獲取字段#2并置于棧頂, 將int類型的1入棧,將棧內(nèi)頂部的兩個數(shù)值相加,返回一個int類型的值。

          SourceFile


          源碼文件名稱

          /???實戰(zhàn)? ?/

          分析try-catch-finally


          通過以上一個最簡單的例子,可以大致了解源碼被編譯成字節(jié)碼后是什么樣子的。下面利用所學(xué)的知識點來分析一些Java問題:

          public?class?TestCode?{
          ????public?int?foo()?{
          ????????int?x;
          ????????try?{
          ????????????x?=?1;
          ????????????return?x;
          ????????}?catch?(Exception?e)?{
          ????????????x?=?2;
          ????????????return?x;
          ????????}?finally?{
          ????????????x?=?3;
          ????????}
          ????}
          }

          試問當(dāng)不發(fā)生異常和發(fā)生異常的情況下,foo()的返回值分別是多少。使出老手段

          public?class?TestCode?{
          javac?TestCode.java
          javap?-verbose?TestCode.class

          查看字節(jié)碼的foo方法內(nèi)容:

          public?int?foo();
          ????descriptor:?()I
          ????flags:?ACC_PUBLIC
          ????Code:
          ??????stack=1,?locals=5,?args_size=1
          ?????????0:?iconst_1?//int型1入棧?->棧頂=1
          ?????????1:?istore_1?//將棧頂?shù)膇nt型數(shù)值存入第二個局部變量?->局部2=1
          ?????????2:?iload_1?//將第二個int型局部變量推送至棧頂?->棧頂=1
          ?????????3:?istore_2?//!!將棧頂int型數(shù)值存入第三個局部變量?->局部3=1

          ?????????4:?iconst_3?//int型3入棧?->棧頂=3
          ?????????5:?istore_1?//將棧頂?shù)膇nt型數(shù)值存入第二個局部變量?->局部2=3
          ?????????6:?iload_2?//!!將第三個int型局部變量推送至棧頂?->棧頂=1
          ?????????7:?ireturn?//從當(dāng)前方法返回棧頂int數(shù)值?->1

          ?????????8:?astore_2?//?->局部3=Exception
          ?????????9:?iconst_2?//?->棧頂=2
          ????????10:?istore_1?//?->局部2=2
          ????????11:?iload_1?//->棧頂=2
          ????????12:?istore_3?//!!?->局部4=2

          ????????13:?iconst_3?//?->棧頂=3
          ????????14:?istore_1?//?->局部1=3
          ????????15:?iload_3?//!!?->棧頂=2
          ????????16:?ireturn?//?->?2

          ????????17:?astore????????4?//將棧頂引用型數(shù)值存入第五個局部變量=any
          ????????19:?iconst_3?//將int型數(shù)值3入棧?->?棧頂3
          ????????20:?istore_1?//將棧頂?shù)谝粋€int數(shù)值存入第二個局部變量?->?局部2=3
          ????????21:?aload?????????4?//將局部第五個局部變量(引用型)推送至棧頂
          ????????23:?athrow?//將棧頂?shù)漠惓伋?/span>
          ??????Exception?table:
          ?????????from????to??target?type
          ?????????????0?????4?????8???Class?java/lang/Exception?//0到4行對應(yīng)的異常,對應(yīng)#8中儲存的異常
          ?????????????0?????4????17???any?//Exeption之外的其他異常
          ?????????????8????13????17???any
          ????????????17????19????17???any

          在字節(jié)碼的4,5,以及13,14中執(zhí)行的是同一個操作,就是將int型的3入操作數(shù)棧頂,并存入第二個局部變量。這正是我們源碼在finally語句塊中內(nèi)容。也就是說,JVM在處理異常時,會在每個可能的分支都將finally語句重復(fù)執(zhí)行一遍。通過一步步分析字節(jié)碼,可以得出最后的運行結(jié)果是:

          • 不發(fā)生異常時: return 1
          • 發(fā)生異常時: return 2
          • 發(fā)生非Exception及其子類的異常,拋出異常,不返回值

          以上例子來自于《深入理解Java虛擬機(jī) JVM高級特性與最佳實踐》 關(guān)于虛擬機(jī)字節(jié)碼指令表,也可以在《深入理解Java虛擬機(jī) JVM高級特性與最佳實踐-附錄B》中獲取。

          kotlin 函數(shù)擴(kuò)展的實現(xiàn)


          kotlin提供了擴(kuò)展函數(shù)的語言特性,借助這個特性,我們可以給任意對象添加自定義方法。以下示例為Object添加"sayHello"方法

          //SayHello.kt
          package?com.rhythm7

          fun?Any.sayHello()?{
          ????println("Hello")
          }

          編譯后,使用javap查看生成SayHelloKt.class文件的字節(jié)碼。

          Classfile?/E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/SayHelloKt.class
          Last?modified?2018-4-8;?size?958?bytes
          ?MD5?checksum?780a04b75a91be7605cac4655b499f19
          ?Compiled?from?"SayHello.kt"
          public?final?class?com.rhythm7.SayHelloKt
          ?minor?version:?0
          ?major?version:?52
          ?flags:?ACC_PUBLIC,?ACC_FINAL,?ACC_SUPER
          Constant?pool:
          ????//省略常量池部分字節(jié)碼
          {
          ?public?static?final?void?sayHello(java.lang.Object);
          ???descriptor:?(Ljava/lang/Object;)V
          ???flags:?ACC_PUBLIC,?ACC_STATIC,?ACC_FINAL
          ???Code:
          ?????stack=2,?locals=2,?args_size=1
          ????????0:?aload_0
          ????????1:?ldc???????????#9??????????????????//?String?$receiver
          ????????3:?invokestatic??#15?????????????????//?Method?kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
          ????????6:?ldc???????????#17?????????????????//?String?Hello
          ????????8:?astore_1
          ????????9:?getstatic?????#23?????????????????//?Field?java/lang/System.out:Ljava/io/PrintStream;
          ???????12:?aload_1
          ???????13:?invokevirtual?#28?????????????????//?Method?java/io/PrintStream.println:(Ljava/lang/Object;)V
          ???????16:?return
          ?????LocalVariableTable:
          ???????Start??Length??Slot??Name???Signature
          ???????????0??????17?????0?$receiver???Ljava/lang/Object;
          ?????LineNumberTable:
          ???????line?4:?6
          ???????line?5:?16
          ???RuntimeInvisibleParameterAnnotations:
          ?????0:
          ???????0:?#7()
          }
          SourceFile:?"SayHello.kt"

          觀察頭部發(fā)現(xiàn),koltin為文件SayHello生成了一個類,類名"com.rhythm7.SayHelloKt". 由于我們一開始編寫SayHello.kt時并不希望SayHello是一個可實例化的對象類,所以,SayHelloKt是無法被實例化的,SayHelloKt并沒有任何一個構(gòu)造器。再觀察唯一的一個方法:發(fā)現(xiàn)Any.sayHello()的具體實現(xiàn)是靜態(tài)不可變方法的形式:

          public?static?final?void?sayHello(java.lang.Object);

          所以當(dāng)我們在其他地方使用Any.sayHello()時,事實上等同于調(diào)用java的SayHelloKt.sayHello(Object)方法。

          順便一提的是,當(dāng)擴(kuò)展的方法為Any時,意味著Any是non-null的,這時,編譯器會在方法體的開頭檢查參數(shù)的非空,即調(diào)用kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull(Object value, String paramName)方法來檢查傳入的Any類型對象是否為空。

          如果我們擴(kuò)展的函數(shù)為Any?.sayHello(),那么在編譯后的文件中則不會有這段字節(jié)碼的出現(xiàn)。

          源:juejin.im/post/6844903588716609543

          版權(quán)申明:內(nèi)容來源網(wǎng)絡(luò),版權(quán)歸原創(chuàng)者所有。除非無法確認(rèn),我們都會標(biāo)明作者及出處,如有侵權(quán)煩請告知,我們會立即刪除并表示歉意。謝謝!





          感謝閱讀



          瀏覽 36
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  97人妻精品一区二区三区香蕉 | 国产精品久久久违 | 麻豆成人无码 | 国产资源影音先锋 | 中文字幕在线乱 |