java(1.8)字節碼讀例(匿名內部類)

源代碼

public class ThreadDemo {
    public static void main(String[] args) {

        for(int i = 0;i<10;i++){
            final int j = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("線程" + j);
                }
            }) .start();
        }

    }
}

javac -g ThreadDemo.java命令編譯後生成兩個class文件:ThreadDemo.classThreadDemo$1.class

字節碼

利用jdk提供的反編譯工具:javap -v class文件
得到如下兩段字節碼:

1.ThreadDemo.class

Classfile /D:/work/workspace/util-demo/src/main/java/com/gsy/utildemo/thread/api/ThreadDemo.class
  Last modified 2020-3-20; size 716 bytes
  MD5 checksum 5837aed4e038d345b4e4089e35479b94
  Compiled from "ThreadDemo.java"
public class com.gsy.utildemo.thread.api.ThreadDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#27         // java/lang/Object."<init>":()V
   #2 = Class              #28            // java/lang/Thread
   #3 = Class              #29            // com/gsy/utildemo/thread/api/ThreadDemo$1
   #4 = Methodref          #3.#30         // com/gsy/utildemo/thread/api/ThreadDemo$1."<init>":(I)V
   #5 = Methodref          #2.#31         // java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
   #6 = Methodref          #2.#32         // java/lang/Thread.start:()V
   #7 = Class              #33            // com/gsy/utildemo/thread/api/ThreadDemo
   #8 = Class              #34            // java/lang/Object
   #9 = Utf8               InnerClasses
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcom/gsy/utildemo/thread/api/ThreadDemo;
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               j
  #20 = Utf8               I
  #21 = Utf8               i
  #22 = Utf8               args
  #23 = Utf8               [Ljava/lang/String;
  #24 = Utf8               StackMapTable
  #25 = Utf8               SourceFile
  #26 = Utf8               ThreadDemo.java
  #27 = NameAndType        #10:#11        // "<init>":()V
  #28 = Utf8               java/lang/Thread
  #29 = Utf8               com/gsy/utildemo/thread/api/ThreadDemo$1
  #30 = NameAndType        #10:#35        // "<init>":(I)V
  #31 = NameAndType        #10:#36        // "<init>":(Ljava/lang/Runnable;)V
  #32 = NameAndType        #37:#11        // start:()V
  #33 = Utf8               com/gsy/utildemo/thread/api/ThreadDemo
  #34 = Utf8               java/lang/Object
  #35 = Utf8               (I)V
  #36 = Utf8               (Ljava/lang/Runnable;)V
  #37 = Utf8               start
{
  public com.gsy.utildemo.thread.api.ThreadDemo();
    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
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/gsy/utildemo/thread/api/ThreadDemo;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=5, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: bipush        10
         5: if_icmpge     34
         8: iload_1
         9: istore_2
        10: new           #2                  // class java/lang/Thread
        13: dup
        14: new           #3                  // class com/gsy/utildemo/thread/api/ThreadDemo$1
        17: dup
        18: iload_2
        19: invokespecial #4                  // Method com/gsy/utildemo/thread/api/ThreadDemo$1."<init>":(I)V
        22: invokespecial #5                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
        25: invokevirtual #6                  // Method java/lang/Thread.start:()V
        28: iinc          1, 1
        31: goto          2
        34: return
      LineNumberTable:
        line 7: 0
        line 8: 8
        line 9: 10
        line 14: 25
        line 7: 28
        line 17: 34
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           10      18     2     j   I
            2      32     1     i   I
            0      35     0  args   [Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
          offset_delta = 2
          locals = [ int ]
        frame_type = 250 /* chop */
          offset_delta = 31
}

class文件大致分爲以下幾部分:

  • 文件元數據
  • 類信息(類名、繼承/實現關係、編譯器版本、訪問控制修飾符)
  • 常量池
  • 屬性信息(私有屬性信息不在內)
  • 方法信息(簽名、訪問控制符、代碼(指令、異常表、行號表(原碼:字節碼)、本地變量表、棧圖))
    由於ThreadDemo類沒有屬性信息,莫問直接從方法的code部分讀:

1.1 init()初始化方法

  • aload_0指令值將本地變量表中0 slot位的引用加載到操作數棧頂。此時本地變量表0位時this引用,這是因爲構造方法內部要通過this來調用父類引用,所以編譯器加了this;反之,方法沒有用到this,其不會被放入本地變量表0位;
  • invokespecial該指令用來調用實例構造器方法, 私有方法和父類方法;通過棧中this引用調用完成後連同參數一起彈出,這裏調用的是無參函數,僅彈出this;
  • return 返回指令
    實際上編譯器會將屬性賦值、示例代碼塊代碼加入到初始化方法中,所以經過編譯器處理的構造方法如下:
    初始化方法 = 父類初始化方法(super) + 屬性賦值 + 實例代碼塊 + 構造方法內部代碼

1.2 mian方法

  • iconst_0 將int常量0壓入操作數棧;
  • istore_1 將操作數棧頭部彈出,存入本地變量表中slot_1的位置,這裏該位置代表變量i
  • iload_1 將本地變量表中的slot_1位置的值壓入操作數棧,即變量i的值;
  • bipush 10 將byte常量10轉爲int常量並壓入操作數棧;
  • if_icmpge 34 比較操作數棧前兩位,並彈出;滿足前大於等與後(即棧頂元素)則跳轉到34指令;
  • iload_1 將本地變量表中的slot_1位置的值壓入操作數棧,即變量i的值;
  • istore_2 將操作數棧頭部彈出,存入本地變量表中slot_2的位置,這裏該位置代表變量j
  • new #2 創建生成Thread對象,將對象引用壓入操作數棧
  • dup 將棧頂元素複製一份後再壓入操作數棧;new指令後跟着dup,因爲要調用出書化方法,會消耗一個引用,不復制的的話在初始化後,無法進行引用賦值或作爲參數傳遞或方法調用,該引用會丟失;這裏是用作**start()**方法調用;
  • new #3 創建生成ThreadDemo$1對象,將對象引用壓入操作數棧;
  • dup 同上步;引用用作參數傳遞;
  • iload_2 將slot_2的int變量加載到操作數棧;
  • invokespecial #4 特殊方法調用(這裏調用的是內部類構造,但其調用的是有參構造);彈出前n+1個操作數,底部是方法調用對象的引用,後面n個對應參數列表;這一步彈出的是棧頂的參數 j = 10和一個ThreadDemo$1對象引用;
  • invokespecial #5 特殊方法調用(這裏是Thread類的有參構造);彈出一個ThreadDemo$1對象引用和一個Thread對象引用;
  • invokevirtual #6 調用**start()**方法;彈出剩下最後一個Thread對象引用;
  • iinc 1,1 將slot_1位置加int變量加1;這裏因爲i++沒有後續操作,不需要iload指令。但其實平時要是有其它操作,一般需要將slot加載到操作數棧用作運算;i++後接運算符:先iload後innc;++i後接運算符:先iinc後iload
  • goto 2 跳轉到指令2;
  • return 返回

2.ThreadDemo$1.class

Classfile /D:/work/workspace/util-demo/src/main/java/com/gsy/utildemo/thread/api/ThreadDemo$1.class
  Last modified 2020-3-22; size 950 bytes
  MD5 checksum d0c952ff06f40d24aa9f6d4bc76af6b0
  Compiled from "ThreadDemo.java"
final class com.gsy.utildemo.thread.api.ThreadDemo$1 implements java.lang.Runnable
  minor version: 0
  major version: 52
  flags: ACC_FINAL, ACC_SUPER
Constant pool:
   #1 = Fieldref           #11.#31        // com/gsy/utildemo/thread/api/ThreadDemo$1.val$j:I
   #2 = Methodref          #12.#32        // java/lang/Object."<init>":()V
   #3 = Fieldref           #33.#34        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Class              #35            // java/lang/StringBuilder
   #5 = Methodref          #4.#32         // java/lang/StringBuilder."<init>":()V
   #6 = String             #36            // 綰跨▼
   #7 = Methodref          #4.#37         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #8 = Methodref          #4.#38         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   #9 = Methodref          #4.#39         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #10 = Methodref          #40.#41        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = Class              #42            // com/gsy/utildemo/thread/api/ThreadDemo$1
  #12 = Class              #43            // java/lang/Object
  #13 = Class              #44            // java/lang/Runnable
  #14 = Utf8               val$j
  #15 = Utf8               I
  #16 = Utf8               <init>
  #17 = Utf8               (I)V
  #18 = Utf8               Code
  #19 = Utf8               LineNumberTable
  #20 = Utf8               LocalVariableTable
  #21 = Utf8               this
  #22 = Utf8               InnerClasses
  #23 = Utf8               Lcom/gsy/utildemo/thread/api/ThreadDemo$1;
  #24 = Utf8               run
  #25 = Utf8               ()V
  #26 = Utf8               SourceFile
  #27 = Utf8               ThreadDemo.java
  #28 = Utf8               EnclosingMethod
  #29 = Class              #45            // com/gsy/utildemo/thread/api/ThreadDemo
  #30 = NameAndType        #46:#47        // main:([Ljava/lang/String;)V
  #31 = NameAndType        #14:#15        // val$j:I
  #32 = NameAndType        #16:#25        // "<init>":()V
  #33 = Class              #48            // java/lang/System
  #34 = NameAndType        #49:#50        // out:Ljava/io/PrintStream;
  #35 = Utf8               java/lang/StringBuilder
  #36 = Utf8               綰跨▼
  #37 = NameAndType        #51:#52        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #38 = NameAndType        #51:#53        // append:(I)Ljava/lang/StringBuilder;
  #39 = NameAndType        #54:#55        // toString:()Ljava/lang/String;
  #40 = Class              #56            // java/io/PrintStream
  #41 = NameAndType        #57:#58        // println:(Ljava/lang/String;)V
  #42 = Utf8               com/gsy/utildemo/thread/api/ThreadDemo$1
  #43 = Utf8               java/lang/Object
  #44 = Utf8               java/lang/Runnable
  #45 = Utf8               com/gsy/utildemo/thread/api/ThreadDemo
  #46 = Utf8               main
  #47 = Utf8               ([Ljava/lang/String;)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
{
  final int val$j;
    descriptor: I
    flags: ACC_FINAL, ACC_SYNTHETIC

  com.gsy.utildemo.thread.api.ThreadDemo$1(int);
    descriptor: (I)V
    flags:
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #1                  // Field val$j:I
         5: aload_0
         6: invokespecial #2                  // Method java/lang/Object."<init>":()V
         9: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/gsy/utildemo/thread/api/ThreadDemo$1;

  public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #4                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #6                  // String 線程
        12: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: aload_0
        16: getfield      #1                  // Field val$j:I
        19: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        22: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        25: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        28: return
      LineNumberTable:
        line 11: 0
        line 12: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  this   Lcom/gsy/utildemo/thread/api/ThreadDemo$1;
}
	這裏需要注意由於源代碼在匿名內部類中使用了方法中的局部變量j,這裏會給匿名內部類生成一個val$j屬性,
	並自動生成有參構造爲屬性賦值;(這裏比較特別,使用匿名內部類是main方法,它是靜態的,也沒有訪問外部
	類任何屬性,所以內部類不需要持有外部類對象引用,不需要內部類構造參數中加一個外部類對象的引用)擴展
	一下,如果不僅使用了局部變量,還使用了外部類屬性,會怎麼樣?實際上編譯器會在外部內生成一個靜態方法 
	(*) static access$$xxx(ThreadDemo),這個方法傳入外部類對象引用,裏面通過引用返回訪問的屬性;
	同時構造內部類也會增加一個外部類ThreadDemo類型的引用,對應的構造方法會增加一個參數爲ThreadDemo類
	型的,完成屬性賦值。

2.1 init(int)初始化方法

  • aload_0 加載slot_0位置的this引用到操作數棧;
  • iload_1 加載slot_1位置變量int型變量到操作數棧,這裏不知道爲什麼,在本地變量表沒有展示,實際上slot_1是方法的形參;
  • putfield #1 給屬性賦值,用this訪問屬性,賦值爲棧頂值1;彈出棧頂兩個元素;
  • aload_0 加載slot_0位置的this引用到操作數棧;
  • invokespecial #2 調用父類的初始化方法;彈出棧頂元素;
  • return 返回
    這裏有一個疑問,爲什麼父類初始化在屬性賦值之後?歡迎答疑!

2.2 run()方法

  • getstatic #3 獲得靜態屬性,並壓入棧中;這裏指的是系統的輸出流引用;
  • new #4 生成對象;這裏是StringBuilder對象;
  • dup 棧頂複製引用;用作構造方法的調用;
  • invokespecial #5 調用初始化方法;彈出棧頂的StringBuilder引用;
  • ldc #6 加載字符串常量到棧頂;
  • invokevirtual #7 調用虛方法;這裏是append方法,有參,返回自身引用;方法調用先彈出棧頂的引用和參數,然後將返回值壓入棧,這裏是拼接了常量字符串的StringBuliler引用;
  • aload_0 加載slot_0的this引用到棧頂;
  • getfield #1 獲得屬性值;操作數棧彈出this引用,將屬性值壓入棧;這裏是val$j的值;
  • invokevirtual #8 調用append(I)函數;彈出棧頂的StringBuilder引用和參數,將返回結果拼接完全的字符串壓入棧中;此時棧中剩下一個輸出流引用和一個StringBuilder引用;
  • invokevirtual #9 調用StringBuilder的toString方法;彈出StringBuilder引用,將方法返回值String對象引用壓入棧中;此時棧中剩下一個輸出流引用和一個String對象引用;
  • invokevirtual #10 調用輸出流的print(String)方法;彈出最後兩個元素;
  • return 返回;

我們嘗試以匿名內部類爲例解讀了編譯器生成的字節碼文件(反編譯後的),但這裏面依然有許多的偶然性,需要大家探索;比如:可以將main方法改爲一個非靜態方法,就會發現內部類會增加一個外部類引用的屬性,即使內部類沒有訪問外部類的任何屬性;而本例比較特殊:匿名內部類使用在一個靜態方法中,並且匿名內部類沒有訪問外部類的任何屬性,所以在內部類中沒有添加外部類屬性;

3.結論與疑問

3.1結論

  • 在方法中使用匿名內部類時,匿名內部類中訪問方法的局部變量使用過爲匿名內部類增加一個該類型屬性,同時外部類會調用有參構造傳入改值完成賦值來實現的;
  • 如果是在非靜態方法中使用匿名內部類,無論是否匿名內部類是否訪問外部類屬性,內部類都會增加一個外部類類型的屬性,同時在外部類生成一個靜態有參方法access$xx(外部類引用);匿名內部類訪問外部類屬性的就是通過這種方法 ,將外部類引用傳入,通過返回值訪問到外部類的屬性;

3.2疑問

  • 爲什在本例中的匿名內部類的構造函數中,會出現先屬性賦值後嗲用父類後構造的情況?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章