日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

JAVA 中的字節(jié)碼,英文名為 bytecode, 是 Java 代碼編譯后的中間代碼格式。JVM 需要讀取并解析字節(jié)碼才能執(zhí)行相應(yīng)的任務(wù)。

從技術(shù)人員的角度看,Java 字節(jié)碼是 JVM 的指令集。JVM 加載字節(jié)碼格式的 class 文件,校驗(yàn)之后通過 JIT 編譯器轉(zhuǎn)換為本地機(jī)器代碼執(zhí)行。 簡單說字節(jié)碼就是我們編寫的 Java 應(yīng)用程序大廈的每一塊磚,如果沒有字節(jié)碼的支撐,大家編寫的代碼也就沒有了用武之地,無法運(yùn)行。也可以說,Java 字節(jié)碼就是 JVM 執(zhí)行的指令格式。

那么我們?yōu)槭裁葱枰莆账兀?/p>

不管用什么編程語言,對于卓越而有追求的程序員,都能深入去探索一些技術(shù)細(xì)節(jié),在需要的時(shí)候,可以在代碼被執(zhí)行前解讀和理解中間形式的代碼。對于 Java 來說,中間代碼格式就是 Java 字節(jié)碼。 了解字節(jié)碼及其工作原理,對于編寫高性能代碼至關(guān)重要,對于深入分析和排查問題也有一定作用,所以我們要想深入了解 JVM 來說,了解字節(jié)碼也是夯實(shí)基礎(chǔ)的一項(xiàng)基本功。同時(shí)對于我們開發(fā)人員來時(shí),不了解平臺的底層原理和實(shí)現(xiàn)細(xì)節(jié),想要職業(yè)進(jìn)階絕對不是長久之計(jì),畢竟我們都希望成為更好的程序員, 對吧?

任何有實(shí)際經(jīng)驗(yàn)的開發(fā)者都知道,業(yè)務(wù)系統(tǒng)總不可能沒有 BUG,了解字節(jié)碼以及 Java 編譯器會生成什么樣的字節(jié)碼,才能說具備扎實(shí)的 JVM 功底,會在排查問題和分析錯(cuò)誤時(shí)非常有用,也能更好地解決問題。

而對于工具領(lǐng)域和程序分析來說, 字節(jié)碼就是必不可少的基礎(chǔ)知識了,通過修改字節(jié)碼來調(diào)整程序的行為是司空見慣的事情。想了解分析器(Profiler),Mock 框架,AOP 等工具和技術(shù)這一類工具,則必須完全了解 Java 字節(jié)碼。

4.1 Java 字節(jié)碼簡介

有一件有趣的事情,就如名稱所示, Java bytecode 由單字節(jié)(byte)的指令組成,理論上最多支持 256 個(gè)操作碼(opcode)。實(shí)際上 Java 只使用了 200 左右的操作碼, 還有一些操作碼則保留給調(diào)試操作。

操作碼, 下面稱為 指令, 主要由類型前綴和操作名稱兩部分組成。

例如,'i' 前綴代表 ‘integer’,所以,'iadd' 很容易理解, 表示對整數(shù)執(zhí)行加法運(yùn)算。

根據(jù)指令的性質(zhì),主要分為四個(gè)大類:

  1. 棧操作指令,包括與局部變量交互的指令
  2. 程序流程控制指令
  3. 對象操作指令,包括方法調(diào)用指令
  4. 算術(shù)運(yùn)算以及類型轉(zhuǎn)換指令

此外還有一些執(zhí)行專門任務(wù)的指令,比如同步(synchronization)指令,以及拋出異常相關(guān)的指令等等。下文會對這些指令進(jìn)行詳細(xì)的講解。

4.2 獲取字節(jié)碼清單

可以用 javap 工具來獲取 class 文件中的指令清單。 javap 是標(biāo)準(zhǔn) JDK 內(nèi)置的一款工具, 專門用于反編譯 class 文件。

讓我們從頭開始, 先創(chuàng)建一個(gè)簡單的類,后面再慢慢擴(kuò)充。

package demo.jvm0104;

public class HelloByteCode {
    public static void main(String[] args) {
        HelloByteCode obj = new HelloByteCode();
    }
}

代碼很簡單, main 方法中 new 了一個(gè)對象而已。然后我們編譯這個(gè)類:

javac demo/jvm0104/HelloByteCode.java

使用 javac 編譯 ,或者在 IDEA 或者 Eclipse 等集成開發(fā)工具自動(dòng)編譯,基本上是等效的。只要能找到對應(yīng)的 class 即可。

javac 不指定 -d 參數(shù)編譯后生成的 .class 文件默認(rèn)和源代碼在同一個(gè)目錄。

注意: javac 工具默認(rèn)開啟了優(yōu)化功能, 生成的字節(jié)碼中沒有局部變量表(LocalVariableTable),相當(dāng)于局部變量名稱被擦除。如果需要這些調(diào)試信息, 在編譯時(shí)請加上 -g 選項(xiàng)。有興趣的同學(xué)可以試試兩種方式的區(qū)別,并對比結(jié)果。

JDK 自帶工具的詳細(xì)用法, 請使用: javac -help 或者 javap -help 來查看; 其他類似。

然后使用 javap 工具來執(zhí)行反編譯, 獲取字節(jié)碼清單:

javap -c demo.jvm0104.HelloByteCode
# 或者: 
javap -c demo/jvm0104/HelloByteCode
javap -c demo/jvm0104/HelloByteCode.class

javap 還是比較聰明的, 使用包名或者相對路徑都可以反編譯成功, 反編譯后的結(jié)果如下所示:

Compiled from "HelloByteCode.java"
public class demo.jvm0104.HelloByteCode {
  public demo.jvm0104.HelloByteCode();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class demo/jvm0104/HelloByteCode
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: return
}

OK,我們成功獲取到了字節(jié)碼清單, 下面進(jìn)行簡單的解讀。

4.3 解讀字節(jié)碼清單

可以看到,反編譯后的代碼清單中, 有一個(gè)默認(rèn)的構(gòu)造函數(shù) public
demo.jvm0104.HelloByteCode(), 以及 main 方法。

剛學(xué) Java 時(shí)我們就知道, 如果不定義任何構(gòu)造函數(shù),就會有一個(gè)默認(rèn)的無參構(gòu)造函數(shù),這里再次驗(yàn)證了這個(gè)知識點(diǎn)。好吧,這比較容易理解!我們通過查看編譯后的 class 文件證實(shí)了其中存在默認(rèn)構(gòu)造函數(shù),所以這是 Java 編譯器生成的, 而不是運(yùn)行時(shí)JVM自動(dòng)生成的。

自動(dòng)生成的構(gòu)造函數(shù),其方法體應(yīng)該是空的,但這里看到里面有一些指令。為什么呢?

再次回顧 Java 知識, 每個(gè)構(gòu)造函數(shù)中都會先調(diào)用 super 類的構(gòu)造函數(shù)對吧? 但這不是 JVM 自動(dòng)執(zhí)行的, 而是由程序指令控制,所以默認(rèn)構(gòu)造函數(shù)中也就有一些字節(jié)碼指令來干這個(gè)事情。

基本上,這幾條指令就是執(zhí)行 super() 調(diào)用;

  public demo.jvm0104.HelloByteCode();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

至于其中解析的 java/lang/Object 不用說, 默認(rèn)繼承了 Object 類。這里再次驗(yàn)證了這個(gè)知識點(diǎn),而且這是在編譯期間就確定了的。

繼續(xù)往下看 c,

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class demo/jvm0104/HelloByteCode
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: return

main 方法中創(chuàng)建了該類的一個(gè)實(shí)例, 然后就 return 了, 關(guān)于里面的幾個(gè)指令, 稍后講解。

4.4 查看 class 文件中的常量池信息

常量池 大家應(yīng)該都聽說過, 英文是 Constant pool。這里做一個(gè)強(qiáng)調(diào): 大多數(shù)時(shí)候指的是 運(yùn)行時(shí)常量池。但運(yùn)行時(shí)常量池里面的常量是從哪里來的呢? 主要就是由 class 文件中的 常量池結(jié)構(gòu)體 組成的。

要查看常量池信息, 我們得加一點(diǎn)魔法參數(shù):

javap -c -verbose demo.jvm0104.HelloByteCode

在反編譯 class 時(shí),指定 -verbose 選項(xiàng), 則會 輸出附加信息。

結(jié)果如下所示:

Classfile /XXXXXXX/demo/jvm0104/HelloByteCode.class
  Last modified 2019-11-28; size 301 bytes
  MD5 checksum 542cb70faf8b2b512a023e1a8e6c1308
  Compiled from "HelloByteCode.java"
public class demo.jvm0104.HelloByteCode
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref #4.#13 // java/lang/Object."<init>":()V
   #2 = Class #14 // demo/jvm0104/HelloByteCode
   #3 = Methodref #2.#13 // demo/jvm0104/HelloByteCode."<init>":()V
   #4 = Class #15 // java/lang/Object
   #5 = Utf8 <init>
   #6 = Utf8 ()V
   #7 = Utf8 Code
   #8 = Utf8 LineNumberTable
   #9 = Utf8 main
  #10 = Utf8 ([Ljava/lang/String;)V
  #11 = Utf8 SourceFile
  #12 = Utf8 HelloByteCode.java
  #13 = NameAndType #5:#6 // "<init>":()V
  #14 = Utf8 demo/jvm0104/HelloByteCode
  #15 = Utf8 java/lang/Object
{
  public demo.jvm0104.HelloByteCode();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1 // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new #2 // class demo/jvm0104/HelloByteCode
         3: dup
         4: invokespecial #3 // Method "<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
}
SourceFile: "HelloByteCode.java"

其中顯示了很多關(guān)于 class 文件信息: 編譯時(shí)間, MD5 校驗(yàn)和, 從哪個(gè) .java 源文件編譯得來,符合哪個(gè)版本的 Java 語言規(guī)范等等。

還可以看到 ACC_PUBLIC 和 ACC_SUPER 訪問標(biāo)志符。 ACC_PUBLIC 標(biāo)志很容易理解:這個(gè)類是 public 類,因此用這個(gè)標(biāo)志來表示。

但 ACC_SUPER 標(biāo)志是怎么回事呢? 這就是歷史原因, JDK 1.0 的 BUG 修正中引入 ACC_SUPER 標(biāo)志來修正 invokespecial 指令調(diào)用 super 類方法的問題,從 Java 1.1 開始, 編譯器一般都會自動(dòng)生成ACC_SUPER 標(biāo)志。

有些同學(xué)可能注意到了, 好多指令后面使用了 #1, #2, #3 這樣的編號。

這就是對常量池的引用。 那常量池里面有些什么呢?

Constant pool:
   #1 = Methodref #4.#13 // java/lang/Object."<init>":()V
   #2 = Class #14 // demo/jvm0104/HelloByteCode
   #3 = Methodref #2.#13 // demo/jvm0104/HelloByteCode."<init>":()V
   #4 = Class #15 // java/lang/Object
   #5 = Utf8 <init>
......

這是摘取的一部分內(nèi)容, 可以看到常量池中的常量定義。還可以進(jìn)行組合, 一個(gè)常量的定義中可以引用其他常量。

比如第一行: #1 = Methodref #4.#13 // java/lang/Object."<init>":()V, 解讀如下:

  • #1 常量編號, 該文件中其他地方可以引用。
  • = 等號就是分隔符.
  • Methodref 表明這個(gè)常量指向的是一個(gè)方法;具體是哪個(gè)類的哪個(gè)方法呢? 類指向的 #4, 方法簽名指向的 #13; 當(dāng)然雙斜線注釋后面已經(jīng)解析出來可讀性比較好的說明了。

同學(xué)們可以試著解析其他的常量定義。 自己實(shí)踐加上知識回顧,能有效增加個(gè)人的記憶和理解。

總結(jié)一下,常量池就是一個(gè)常量的大字典,使用編號的方式把程序里用到的各類常量統(tǒng)一管理起來,這樣在字節(jié)碼操作里,只需要引用編號即可。

4.5 查看方法信息

在 javap 命令中使用 -verbose 選項(xiàng)時(shí), 還顯示了其他的一些信息。 例如, 關(guān)于 main 方法的更多信息被打印出來:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1

可以看到方法描述: ([Ljava/lang/String;)V:

  • 其中小括號內(nèi)是入?yún)⑿畔?形參信息;
  • 左方括號表述數(shù)組;
  • L 表示對象;
  • 后面的java/lang/String就是類名稱;
  • 小括號后面的 V 則表示這個(gè)方法的返回值是 void;
  • 方法的訪問標(biāo)志也很容易理解 flags: ACC_PUBLIC, ACC_STATIC,表示 public 和 static。

還可以看到執(zhí)行該方法時(shí)需要的棧(stack)深度是多少,需要在局部變量表中保留多少個(gè)槽位, 還有方法的參數(shù)個(gè)數(shù): stack=2, locals=2, args_size=1。把上面這些整合起來其實(shí)就是一個(gè)方法:

public static void main(java.lang.String[]);

注:實(shí)際上我們一般把一個(gè)方法的修飾符+名稱+參數(shù)類型清單+返回值類型,合在一起叫“方法簽名”,即這些信息可以完整的表示一個(gè)方法。

稍微往回一點(diǎn)點(diǎn),看編譯器自動(dòng)生成的無參構(gòu)造函數(shù)字節(jié)碼:

  public demo.jvm0104.HelloByteCode();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1 // Method java/lang/Object."<init>":()V
         4: return

你會發(fā)現(xiàn)一個(gè)奇怪的地方, 無參構(gòu)造函數(shù)的參數(shù)個(gè)數(shù)居然不是 0: stack=1, locals=1, args_size=1。 這是因?yàn)樵?Java 中, 如果是靜態(tài)方法則沒有 this 引用。 對于非靜態(tài)方法, this 將被分配到局部變量表的第 0 號槽位中, 關(guān)于局部變量表的細(xì)節(jié),下面再進(jìn)行介紹。

有反射編程經(jīng)驗(yàn)的同學(xué)可能比較容易理解: Method#invoke(Object obj, Object... args); 有JavaScript編程經(jīng)驗(yàn)的同學(xué)也可以類比: fn.Apply(obj, args) && fn.call(obj, arg1, arg2);

4.6 線程棧與字節(jié)碼執(zhí)行模型

想要深入了解字節(jié)碼技術(shù),我們需要先對字節(jié)碼的執(zhí)行模型有所了解。

JVM 是一臺基于棧的計(jì)算機(jī)器。每個(gè)線程都有一個(gè)獨(dú)屬于自己的線程棧(JVM stack),用于存儲棧幀(Frame)。每一次方法調(diào)用,JVM都會自動(dòng)創(chuàng)建一個(gè)棧幀。棧幀 由 操作數(shù)棧, 局部變量數(shù)組 以及一個(gè)class 引用組成。class 引用 指向當(dāng)前方法在運(yùn)行時(shí)常量池中對應(yīng)的 class)。

我們在前面反編譯的代碼中已經(jīng)看到過這些內(nèi)容。

Java 字節(jié)碼技術(shù)詳解

 

局部變量數(shù)組 也稱為 局部變量表(LocalVariableTable), 其中包含了方法的參數(shù),以及局部變量。 局部變量數(shù)組的大小在編譯時(shí)就已經(jīng)確定: 和局部變量+形參的個(gè)數(shù)有關(guān),還要看每個(gè)變量/參數(shù)占用多少個(gè)字節(jié)。操作數(shù)棧是一個(gè) LIFO 結(jié)構(gòu)的棧, 用于壓入和彈出值。 它的大小也在編譯時(shí)確定。

有一些操作碼/指令可以將值壓入“操作數(shù)棧”; 還有一些操作碼/指令則是從棧中獲取操作數(shù),并進(jìn)行處理,再將結(jié)果壓入棧。操作數(shù)棧還用于接收調(diào)用其他方法時(shí)返回的結(jié)果值。

4.7 方法體中的字節(jié)碼解讀

看過前面的示例,細(xì)心的同學(xué)可能會猜測,方法體中那些字節(jié)碼指令前面的數(shù)字是什么意思,說是序號吧但又不太像,因?yàn)樗麄冎g的間隔不相等??纯?main 方法體對應(yīng)的字節(jié)碼:

         0: new #2 // class demo/jvm0104/HelloByteCode
         3: dup
         4: invokespecial #3 // Method "<init>":()V
         7: astore_1
         8: return

間隔不相等的原因是, 有一部分操作碼會附帶有操作數(shù), 也會占用字節(jié)碼數(shù)組中的空間。

例如, new 就會占用三個(gè)槽位: 一個(gè)用于存放操作碼指令自身,兩個(gè)用于存放操作數(shù)。

因此,下一條指令 dup 的索引從 3 開始。

如果將這個(gè)方法體變成可視化數(shù)組,那么看起來應(yīng)該是這樣的:

Java 字節(jié)碼技術(shù)詳解

 

每個(gè)操作碼/指令都有對應(yīng)的十六進(jìn)制(HEX)表示形式, 如果換成十六進(jìn)制來表示,則方法體可表示為HEX字符串。例如上面的方法體百世成十六進(jìn)制如下所示:

Java 字節(jié)碼技術(shù)詳解

 

甚至我們還可以在支持十六進(jìn)制的編輯器中打開 class 文件,可以在其中找到對應(yīng)的字符串:

Java 字節(jié)碼技術(shù)詳解

 

(此圖由開源文本編輯軟件Atom的hex-view插件生成)

粗暴一點(diǎn),我們可以通過 HEX 編輯器直接修改字節(jié)碼,盡管這樣做會有風(fēng)險(xiǎn), 但如果只修改一個(gè)數(shù)值的話應(yīng)該會很有趣。

其實(shí)要使用編程的方式,方便和安全地實(shí)現(xiàn)字節(jié)碼編輯和修改還有更好的辦法,那就是使用 ASM 和 Javassist 之類的字節(jié)碼操作工具,也可以在類加載器和 Agent 上面做文章,下一節(jié)課程會討論 類加載器,其他主題則留待以后探討。

4.8 對象初始化指令:new 指令, init 以及 clinit 簡介

我們都知道 new是 Java 編程語言中的一個(gè)關(guān)鍵字, 但其實(shí)在字節(jié)碼中,也有一個(gè)指令叫做 new。 當(dāng)我們創(chuàng)建類的實(shí)例時(shí), 編譯器會生成類似下面這樣的操作碼:

         0: new #2 // class demo/jvm0104/HelloByteCode
         3: dup
         4: invokespecial #3 // Method "<init>":()V

當(dāng)你同時(shí)看到 new, dup 和 invokespecial 指令在一起時(shí),那么一定是在創(chuàng)建類的實(shí)例對象!

為什么是三條指令而不是一條呢?這是因?yàn)椋?/p>

  • new 指令只是創(chuàng)建對象,但沒有調(diào)用構(gòu)造函數(shù)。
  • invokespecial 指令用來調(diào)用某些特殊方法的, 當(dāng)然這里調(diào)用的是構(gòu)造函數(shù)。
  • dup 指令用于復(fù)制棧頂?shù)闹怠?/li>

由于構(gòu)造函數(shù)調(diào)用不會返回值,所以如果沒有 dup 指令, 在對象上調(diào)用方法并初始化之后,操作數(shù)棧就會是空的,在初始化之后就會出問題, 接下來的代碼就無法對其進(jìn)行處理。

這就是為什么要事先復(fù)制引用的原因,為的是在構(gòu)造函數(shù)返回之后,可以將對象實(shí)例賦值給局部變量或某個(gè)字段。因此,接下來的那條指令一般是以下幾種:

  • astore {N} or astore_{N} – 賦值給局部變量,其中 {N} 是局部變量表中的位置。
  • putfield – 將值賦給實(shí)例字段
  • putstatic – 將值賦給靜態(tài)字段

在調(diào)用構(gòu)造函數(shù)的時(shí)候,其實(shí)還會執(zhí)行另一個(gè)類似的方法 <init> ,甚至在執(zhí)行構(gòu)造函數(shù)之前就執(zhí)行了。

還有一個(gè)可能執(zhí)行的方法是該類的靜態(tài)初始化方法 <clinit>, 但 <clinit> 并不能被直接調(diào)用,而是由這些指令觸發(fā)的: new, getstatic, putstatic or invokestatic。

也就是說,如果創(chuàng)建某個(gè)類的新實(shí)例, 訪問靜態(tài)字段或者調(diào)用靜態(tài)方法,就會觸發(fā)該類的靜態(tài)初始化方法【如果尚未初始化】。

實(shí)際上,還有一些情況會觸發(fā)靜態(tài)初始化, 詳情請參考 JVM 規(guī)范: [
http://docs.oracle.com/javase/specs/jvms/se8/html/]

4.9 棧內(nèi)存操作指令

有很多指令可以操作方法棧。 前面也提到過一些基本的棧操作指令: 他們將值壓入棧,或者從棧中獲取值。 除了這些基礎(chǔ)操作之外也還有一些指令可以操作棧內(nèi)存; 比如 swap 指令用來交換棧頂兩個(gè)元素的值。下面是一些示例:

最基礎(chǔ)的是 dup 和 pop 指令。

  • dup 指令復(fù)制棧頂元素的值。
  • pop 指令則從棧中刪除最頂部的值。

還有復(fù)雜一點(diǎn)的指令:比如,swap, dup_x1 和 dup2_x1。

  • 顧名思義,swap 指令可交換棧頂兩個(gè)元素的值,例如A和B交換位置(圖中示例4);
  • dup_x1 將復(fù)制棧頂元素的值,并在棧頂插入兩次(圖中示例5);
  • dup2_x1 則復(fù)制棧頂兩個(gè)元素的值,并插入第三個(gè)值(圖中示例6)。
Java 字節(jié)碼技術(shù)詳解

 

dup_x1 和 dup2_x1 指令看起來稍微有點(diǎn)復(fù)雜。而且為什么要設(shè)置這種指令呢? 在棧中復(fù)制最頂部的值?

請看一個(gè)實(shí)際案例:怎樣交換 2 個(gè) double 類型的值?

需要注意的是,一個(gè) double 值占兩個(gè)槽位,也就是說如果棧中有兩個(gè) double 值,它們將占用 4 個(gè)槽位。

要執(zhí)行交換,你可能想到了 swap 指令,但問題是 swap 只適用于單字(one-word, 單字一般指 32 位 4 個(gè)字節(jié),64 位則是雙字),所以不能處理 double 類型,但 Java 中又沒有 swap2 指令。

怎么辦呢? 解決方法就是使用 dup2_x2 指令,將操作數(shù)棧頂部的 double 值,復(fù)制到棧底 double 值的下方, 然后再使用 pop2 指令彈出棧頂?shù)?double 值。結(jié)果就是交換了兩個(gè) double 值。 示意圖如下圖所示:

Java 字節(jié)碼技術(shù)詳解

 

dup、dup_x1、dup2_x1 指令補(bǔ)充說明

指令的詳細(xì)說明可參考 JVM 規(guī)范:

dup 指令

官方說明是:復(fù)制棧頂?shù)闹?,并將?fù)制的值壓入棧。

操作數(shù)棧的值變化情況(方括號標(biāo)識新插入的值):

..., value →
..., value [,value]

dup_x1 指令

官方說明是:復(fù)制棧頂?shù)闹担?fù)制的值插入到最上面 2 個(gè)值的下方。

操作數(shù)棧的值變化情況(方括號標(biāo)識新插入的值):

..., value2, value1 →
..., [value1,] value2, value1

dup2_x1 指令

官方說明是:復(fù)制棧頂 1 個(gè) 64 位/或 2 個(gè) 32 位的值, 并將復(fù)制的值按照原始順序,插入原始值下面一個(gè) 32 位值的下方。

操作數(shù)棧的值變化情況(方括號標(biāo)識新插入的值):

# 情景 1: value1, value2, and value3 都是分組 1 的值(32 位元素)
..., value3, value2, value1 →
..., [value2, value1,] value3, value2, value1

# 情景 2: value1 是分組 2 的值(64 位,long 或double), value2 是分組 1 的值(32 位元素)
..., value2, value1 →
..., [value1,] value2, value1

Table 2.11.1-B 實(shí)際類型與 JVM 計(jì)算類型映射和分組

實(shí)際類型

JVM 計(jì)算類型

類型分組

boolean

int

1

byte

int

1

char

int

1

short

int

1

int

int

1

float

float

1

reference

reference

1

returnAddress

returnAddress

1

long

long

2

double

double

2

4.10 局部變量表

stack 主要用于執(zhí)行指令,而局部變量則用來保存中間結(jié)果,兩者之間可以直接交互。

讓我們編寫一個(gè)復(fù)雜點(diǎn)的示例:

第一步,先編寫一個(gè)計(jì)算移動(dòng)平均數(shù)的類:

package demo.jvm0104;
//移動(dòng)平均數(shù)
public class MovingAverage {
    private int count = 0;
    private double sum = 0.0D;
    public void submit(double value){
        this.count ++;
        this.sum += value;
    }
    public double getAvg(){
        if(0 == this.count){ return sum;}
        return this.sum/this.count;
    }
}

第二步,然后寫一個(gè)類來調(diào)用:

package demo.jvm0104;
public class LocalVariableTest {
    public static void main(String[] args) {
        MovingAverage ma = new MovingAverage();
        int num1 = 1;
        int num2 = 2;
        ma.submit(num1);
        ma.submit(num2);
        double avg = ma.getAvg();
    }
}

其中 main 方法中向 MovingAverage 類的實(shí)例提交了兩個(gè)數(shù)值,并要求其計(jì)算當(dāng)前的平均值。

然后我們需要編譯(還記得前面提到, 生成調(diào)試信息的 -g 參數(shù)嗎)。

javac -g demo/jvm0104/*.java

然后使用 javap 反編譯:

javap -c -verbose demo/jvm0104/LocalVariableTest

看 main 方法對應(yīng)的字節(jié)碼:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=6, args_size=1
         0: new           #2                  // class demo/jvm0104/MovingAverage
         3: dup
         4: invokespecial #3                  // Method demo/jvm0104/MovingAverage."<init>":()V
         7: astore_1
         8: iconst_1
         9: istore_2
        10: iconst_2
        11: istore_3
        12: aload_1
        13: iload_2
        14: i2d
        15: invokevirtual #4                  // Method demo/jvm0104/MovingAverage.submit:(D)V
        18: aload_1
        19: iload_3
        20: i2d
        21: invokevirtual #4                  // Method demo/jvm0104/MovingAverage.submit:(D)V
        24: aload_1
        25: invokevirtual #5                  // Method demo/jvm0104/MovingAverage.getAvg:()D
        28: dstore        4
        30: return
      LineNumberTable:
        line 5: 0
        line 6: 8
        line 7: 10
        line 8: 12
        line 9: 18
        line 10: 24
        line 11: 30
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1    ma   Ldemo/jvm0104/MovingAverage;
           10      21     2  num1   I
           12      19     3  num2   I
           30       1     4   avg   D


  • 編號 0 的字節(jié)碼 new, 創(chuàng)建 MovingAverage 類的對象;
  • 編號 3 的字節(jié)碼 dup 復(fù)制棧頂引用值。
  • 編號 4 的字節(jié)碼 invokespecial 執(zhí)行對象初始化。
  • 編號 7 開始, 使用 astore_1 指令將引用地址值(addr.)存儲(store)到編號為1的局部變量中: astore_1 中的 1 指代 LocalVariableTable 中ma對應(yīng)的槽位編號,
  • 編號8開始的指令: iconst_1 和 iconst_2 用來將常量值1和2加載到棧里面, 并分別由指令 istore_2 和 istore_3 將它們存儲到在 LocalVariableTable 的槽位 2 和槽位 3 中。
         8: iconst_1
         9: istore_2
        10: iconst_2
        11: istore_3

請注意,store 之類的指令調(diào)用實(shí)際上從棧頂刪除了一個(gè)值。 這就是為什么再次使用相同值時(shí),必須再加載(load)一次的原因。

例如在上面的字節(jié)碼中,調(diào)用 submit 方法之前, 必須再次將參數(shù)值加載到棧中:

        12: aload_1
        13: iload_2
        14: i2d
        15: invokevirtual #4                  // Method demo/jvm0104/MovingAverage.submit:(D)V

調(diào)用 getAvg() 方法后,返回的結(jié)果位于棧頂,然后使用 dstore 將 double 值保存到本地變量4號槽位,這里的d表示目標(biāo)變量的類型為double。

        24: aload_1
        25: invokevirtual #5                  // Method demo/jvm0104/MovingAverage.getAvg:()D
        28: dstore        4

關(guān)于 LocalVariableTable 有個(gè)有意思的事情,就是最前面的槽位會被方法參數(shù)占用。

在這里,因?yàn)?main 是靜態(tài)方法,所以槽位0中并沒有設(shè)置為 this 引用的地址。 但是對于非靜態(tài)方法來說, this 會將分配到第 0 號槽位中。

再次提醒: 有過反射編程經(jīng)驗(yàn)的同學(xué)可能比較容易理解: Method#invoke(Object obj, Object... args); 有JavaScript編程經(jīng)驗(yàn)的同學(xué)也可以類比: fn.apply(obj, args) && fn.call(obj, arg1, arg2);

理解這些字節(jié)碼的訣竅在于:

給局部變量賦值時(shí),需要使用相應(yīng)的指令來進(jìn)行 store,如 astore_1。store 類的指令都會刪除棧頂值。 相應(yīng)的 load 指令則會將值從局部變量表壓入操作數(shù)棧,但并不會刪除局部變量中的值。

4.11 流程控制指令

流程控制指令主要是分支和循環(huán)在用, 根據(jù)檢查條件來控制程序的執(zhí)行流程。

一般是 If-Then-Else 這種三元運(yùn)算符(ternary operator), Java中的各種循環(huán),甚至異常處的理操作碼都可歸屬于 程序流程控制。

然后,我們再增加一個(gè)示例,用循環(huán)來提交給 MovingAverage 類一定數(shù)量的值:

package demo.jvm0104;
public class ForLoopTest {
    private static int[] numbers = {1, 6, 8};
    public static void main(String[] args) {
        MovingAverage ma = new MovingAverage();
        for (int number : numbers) {
            ma.submit(number);
        }
        double avg = ma.getAvg();
    }
}

同樣執(zhí)行編譯和反編譯:

javac -g demo/jvm0104/*.java
javap -c -verbose demo/jvm0104/ForLoopTest

因?yàn)?numbers 是本類中的 static 屬性, 所以對應(yīng)的字節(jié)碼如下所示:

         0: new           #2                  // class demo/jvm0104/MovingAverage
         3: dup
         4: invokespecial #3                  // Method demo/jvm0104/MovingAverage."<init>":()V
         7: astore_1
         8: getstatic     #4                  // Field numbers:[I
        11: astore_2
        12: aload_2
        13: arraylength
        14: istore_3
        15: iconst_0
        16: istore        4
        18: iload         4
        20: iload_3
        21: if_icmpge     43
        24: aload_2
        25: iload         4
        27: iaload
        28: istore        5
        30: aload_1
        31: iload         5
        33: i2d
        34: invokevirtual #5                  // Method demo/jvm0104/MovingAverage.submit:(D)V
        37: iinc          4, 1
        40: goto          18
        43: aload_1
        44: invokevirtual #6                  // Method demo/jvm0104/MovingAverage.getAvg:()D
        47: dstore_2
        48: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           30       7     5 number   I
            0      49     0  args   [Ljava/lang/String;
            8      41     1    ma   Ldemo/jvm0104/MovingAverage;
           48       1     2   avg   D

位置 [8~16] 的指令用于循環(huán)控制。 我們從代碼的聲明從上往下看, 在最后面的LocalVariableTable 中:

  • 0 號槽位被 main 方法的參數(shù) args 占據(jù)了。
  • 1 號槽位被 ma 占用了。
  • 5 號槽位被 number 占用了。
  • 2 號槽位是for循環(huán)之后才被 avg 占用的。

那么中間的 2,3,4 號槽位是誰霸占了呢? 通過分析字節(jié)碼指令可以看出,在 2,3,4 槽位有 3 個(gè)匿名的局部變量(astore_2, istore_3, istore 4等指令)。

  • 2號槽位的變量保存了 numbers 的引用值,占據(jù)了 2號槽位。
  • 3號槽位的變量, 由 arraylength 指令使用, 得出循環(huán)的長度。
  • 4號槽位的變量, 是循環(huán)計(jì)數(shù)器, 每次迭代后使用 iinc 指令來遞增。

如果我們的 JDK 版本再老一點(diǎn), 則會在 2,3,4 槽位發(fā)現(xiàn)三個(gè)源碼中沒有出現(xiàn)的變量: arr$, len$, i$, 也就是循環(huán)變量。

循環(huán)體中的第一條指令用于執(zhí)行 循環(huán)計(jì)數(shù)器與數(shù)組長度 的比較:

        18: iload         4
        20: iload_3
        21: if_icmpge     43

這段指令將局部變量表中 4號槽位 和 3號槽位的值加載到棧中,并調(diào)用 if_icmpge 指令來比較他們的值。

【if_icmpge 解讀: if, integer, compare, great equal】, 如果一個(gè)數(shù)的值大于或等于另一個(gè)值,則程序執(zhí)行流程跳轉(zhuǎn)到pc=43的地方繼續(xù)執(zhí)行。

在這個(gè)例子中就是, 如果4號槽位的值 大于或等于 3號槽位的值, 循環(huán)就結(jié)束了,這里 43 位置對于的是循環(huán)后面的代碼。如果條件不成立,則循環(huán)進(jìn)行下一次迭代。

在循環(huán)體執(zhí)行完,它的循環(huán)計(jì)數(shù)器加 1,然后循環(huán)跳回到起點(diǎn)以再次驗(yàn)證循環(huán)條件:

        37: iinc          4, 1   // 4號槽位的值加1
        40: goto          18     // 跳到循環(huán)開始的地方

4.12 算術(shù)運(yùn)算指令與類型轉(zhuǎn)換指令

Java 字節(jié)碼中有許多指令可以執(zhí)行算術(shù)運(yùn)算。實(shí)際上,指令集中有很大一部分表示都是關(guān)于數(shù)學(xué)運(yùn)算的。對于所有數(shù)值類型(int, long, double, float),都有加,減,乘,除,取反的指令。

那么 byte 和 char, boolean 呢? JVM 是當(dāng)做 int 來處理的。另外還有部分指令用于數(shù)據(jù)類型之間的轉(zhuǎn)換。

算術(shù)操作碼和類型

當(dāng)我們想將 int 類型的值賦值給 long 類型的變量時(shí),就會發(fā)生類型轉(zhuǎn)換。

類型轉(zhuǎn)換操作碼

在前面的示例中, 將 int 值作為參數(shù)傳遞給實(shí)際上接收 double 的 submit() 方法時(shí),可以看到, 在實(shí)際調(diào)用該方法之前,使用了類型轉(zhuǎn)換的操作碼:

        31: iload         5
        33: i2d
        34: invokevirtual #5                  // Method demo/jvm0104/MovingAverage.submit:(D)V

也就是說, 將一個(gè) int 類型局部變量的值, 作為整數(shù)加載到棧中,然后用 i2d 指令將其轉(zhuǎn)換為 double 值,以便將其作為參數(shù)傳給submit方法。

唯一不需要將數(shù)值load到操作數(shù)棧的指令是 iinc,它可以直接對 LocalVariableTable 中的值進(jìn)行運(yùn)算。 其他的所有操作均使用棧來執(zhí)行。

4.13 方法調(diào)用指令和參數(shù)傳遞

前面部分稍微提了一下方法調(diào)用: 比如構(gòu)造函數(shù)是通過 invokespecial 指令調(diào)用的。

這里列舉了各種用于方法調(diào)用的指令:

  • invokestatic,顧名思義,這個(gè)指令用于調(diào)用某個(gè)類的靜態(tài)方法,這也是方法調(diào)用指令中最快的一個(gè)。
  • invokespecial, 我們已經(jīng)學(xué)過了, invokespecial 指令用來調(diào)用構(gòu)造函數(shù),但也可以用于調(diào)用同一個(gè)類中的 private 方法, 以及可見的超類方法。
  • invokevirtual,如果是具體類型的目標(biāo)對象,invokevirtual用于調(diào)用公共,受保護(hù)和打包私有方法。
  • invokeinterface,當(dāng)要調(diào)用的方法屬于某個(gè)接口時(shí),將使用 invokeinterface 指令。

那么 invokevirtual 和 invokeinterface 有什么區(qū)別呢?這確實(shí)是個(gè)好問題。 為什么需要 invokevirtual 和 invokeinterface 這兩種指令呢? 畢竟所有的接口方法都是公共方法, 直接使用 invokevirtual 不就可以了嗎?

這么做是源于對方法調(diào)用的優(yōu)化。JVM 必須先解析該方法,然后才能調(diào)用它。

  • 使用 invokestatic 指令,JVM 就確切地知道要調(diào)用的是哪個(gè)方法:因?yàn)檎{(diào)用的是靜態(tài)方法,只能屬于一個(gè)類。
  • 使用 invokespecial 時(shí), 查找的數(shù)量也很少, 解析也更加容易, 那么運(yùn)行時(shí)就能更快地找到所需的方法。

使用 invokevirtual 和 invokeinterface 的區(qū)別不是那么明顯。想象一下,類定義中包含一個(gè)方法定義表, 所有方法都有位置編號。下面的示例中:A 類包含 method1 和 method2 方法; 子類B繼承A,繼承了 method1,覆寫了 method2,并聲明了方法 method3。

請注意,method1 和 method2 方法在類 A 和類 B 中處于相同的索引位置。

class A
    1: method1
    2: method2
class B extends A
    1: method1
    2: method2
    3: method3

那么,在運(yùn)行時(shí)只要調(diào)用 method2,一定是在位置 2 處找到它。

現(xiàn)在我們來解釋invokevirtual 和 invokeinterface 之間的本質(zhì)區(qū)別。

假設(shè)有一個(gè)接口 X 聲明了 methodX 方法, 讓 B 類在上面的基礎(chǔ)上實(shí)現(xiàn)接口 X:

class B extends A implements X
    1: method1
    2: method2
    3: method3
    4: methodX

新方法 methodX 位于索引 4 處,在這種情況下,它看起來與 method3 沒什么不同。

但如果還有另一個(gè)類 C 也實(shí)現(xiàn)了 X 接口,但不繼承 A,也不繼承 B:

class C implements X
    1: methodC
    2: methodX

類 C 中的接口方法位置與類 B 的不同,這就是為什么運(yùn)行時(shí)在 invokinterface 方面受到更多限制的原因。 與 invokinterface 相比, invokevirtual 針對具體的類型方法表是固定的,所以每次都可以精確查找,效率更高(具體的分析討論可以參見參考材料的第一個(gè)鏈接)。

4.14 JDK7 新增的方法調(diào)用指令 invokedynamic

Java 虛擬機(jī)的字節(jié)碼指令集在 JDK7 之前一直就只有前面提到的 4 種指令(invokestatic,invokespecial,invokevirtual,invokeinterface)。隨著 JDK 7 的發(fā)布,字節(jié)碼指令集新增了invokedynamic指令。這條新增加的指令是實(shí)現(xiàn)“動(dòng)態(tài)類型語言”(Dynamically Typed Language)支持而進(jìn)行的改進(jìn)之一,同時(shí)也是 JDK 8 以后支持的 lambda 表達(dá)式的實(shí)現(xiàn)基礎(chǔ)。

為什么要新增加一個(gè)指令呢?

我們知道在不改變字節(jié)碼的情況下,我們在 Java 語言層面想調(diào)用一個(gè)類 A 的方法 m,只有兩個(gè)辦法:

  • 使用A a=new A(); a.m(),拿到一個(gè) A 類型的實(shí)例,然后直接調(diào)用方法;
  • 通過反射,通過 A.class.getMethod 拿到一個(gè) Method,然后再調(diào)用這個(gè)Method.invoke反射調(diào)用;

這兩個(gè)方法都需要顯式的把方法 m 和類型 A 直接關(guān)聯(lián)起來,假設(shè)有一個(gè)類型 B,也有一個(gè)一模一樣的方法簽名的 m 方法,怎么來用這個(gè)方法在運(yùn)行期指定調(diào)用 A 或者 B 的 m 方法呢?這個(gè)操作在 JavaScript 這種基于原型的語言里或者是 C# 這種有函數(shù)指針/方法委托的語言里非常常見,Java 里是沒有直接辦法的。Java 里我們一般建議使用一個(gè) A 和 B 公有的接口 IC,然后 IC 里定義方法 m,A 和 B 都實(shí)現(xiàn)接口 IC,這樣就可以在運(yùn)行時(shí)把 A 和 B 都當(dāng)做 IC 類型來操作,就同時(shí)有了方法 m,這樣的“強(qiáng)約束”帶來了很多額外的操作。

而新增的 invokedynamic 指令,配合新增的方法句柄(Method Handles,它可以用來描述一個(gè)跟類型 A 無關(guān)的方法 m 的簽名,甚至不包括方法名稱,這樣就可以做到我們使用方法 m 的簽名,但是直接執(zhí)行的時(shí)候調(diào)用的是相同簽名的另一個(gè)方法 b),可以在運(yùn)行時(shí)再決定由哪個(gè)類來接收被調(diào)用的方法。在此之前,只能使用反射來實(shí)現(xiàn)類似的功能。該指令使得可以出現(xiàn)基于 JVM 的動(dòng)態(tài)語言,讓 jvm 更加強(qiáng)大。而且在 JVM 上實(shí)現(xiàn)動(dòng)態(tài)調(diào)用機(jī)制,不會破壞原有的調(diào)用機(jī)制。這樣既很好的支持了 Scala、Clojure 這些 JVM 上的動(dòng)態(tài)語言,又可以支持代碼里的動(dòng)態(tài) lambda 表達(dá)式。

RednaxelaFX 評論說:

簡單來說就是以前設(shè)計(jì)某些功能的時(shí)候把做法寫死在了字節(jié)碼里,后來想改也改不了了。 所以這次給 lambda 語法設(shè)計(jì)翻譯到字節(jié)碼的策略是就用 invokedynamic 來作個(gè)弊,把實(shí)際的翻譯策略隱藏在 JDK 的庫的實(shí)現(xiàn)里(metafactory)可以隨時(shí)改,而在外部的標(biāo)準(zhǔn)上大家只看到一個(gè)固定的 invokedynamic。

參考材料

  • Why Should I Know About Java Bytecode: https://jrebel.com/rebellabs/rebel-labs-report-mastering-java-bytecode-at-the-core-of-the-jvm/
  • 輕松看懂Java字節(jié)碼: https://juejin.im/post/5aca2c366fb9a028c97a5609
  • invokedynamic指令:https://www.cnblogs.com/wade-luffy/p/6058087.html
  • Java 8的Lambda表達(dá)式為什么要基于invokedynamic?:https://www.zhihu.com/question/39462935
  • Invokedynamic:https://www.jianshu.com/p/ad7d572196a8
  • JVM之動(dòng)態(tài)方法調(diào)用:invokedynamic: https://ifeve.com/jvm%E4%B9%8B%E5%8A%A8%E6%80%81%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%EF%BC%9Ainvokedynamic/

分享到:
標(biāo)簽:Java
用戶無頭像

網(wǎng)友整理

注冊時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定