一文看懂Java字節碼

前言

隨着Java語言的不斷的發展,Java的應用場景慢慢被擴大,各類優雅解決問題的技術也不斷衍生,如AOP技術,清晰理解Java運行原理就顯得頗有必要,本篇文章重點講解Java字節碼相關知識。java

字節碼基礎

Java文件經過編譯器生成的是class字節碼文件,字節碼文件也有文件本身的格式,這裏不詳細展開,直接經過Java本身帶的工具查看一下。 首先咱們的測試類文件以下:segmentfault

public class Person {

	public String name;
	public int age;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
}
複製代碼

定義了一個Person類,裏面有name和age的屬性,編譯後生成Person.class文件,直接使用Java工具dump這個class文件,dump命令以下:數組

javap -v -p Person.class
複製代碼

dump生成的內容以下:bash

public class com.sec.resourceparse.Person
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref #5.#27 // java/lang/Object."<init>":()V
   #2 = Fieldref #4.#28 // com/sec/resourceparse/Person.name:Ljava/lang/String;
   #3 = Fieldref #4.#29 // com/sec/resourceparse/Person.age:I
   #4 = Class #30 // com/sec/resourceparse/Person
   #5 = Class #31 // java/lang/Object
   #6 = Utf8 name
   #7 = Utf8 Ljava/lang/String;
   #8 = Utf8 age
   #9 = Utf8 I
  #10 = Utf8 <init>
  #11 = Utf8 ()V
  #12 = Utf8 Code
  #13 = Utf8 LineNumberTable
  #14 = Utf8 LocalVariableTable
  #15 = Utf8 this
  #16 = Utf8 Lcom/sec/resourceparse/Person;
  #17 = Utf8 getName
  #18 = Utf8 ()Ljava/lang/String;
  #19 = Utf8 setName
  #20 = Utf8 (Ljava/lang/String;)V
  #21 = Utf8 getAge
  #22 = Utf8 ()I
  #23 = Utf8 setAge
  #24 = Utf8 (I)V
  #25 = Utf8 SourceFile
  #26 = Utf8 Person.java
  #27 = NameAndType #10:#11 // "<init>":()V
  #28 = NameAndType #6:#7 // name:Ljava/lang/String;
  #29 = NameAndType #8:#9 // age:I
  #30 = Utf8 com/sec/resourceparse/Person
  #31 = Utf8 java/lang/Object
{
  public java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC

  public int age;
    descriptor: I
    flags: ACC_PUBLIC

  public com.sec.resourceparse.Person();
    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/sec/resourceparse/Person;

  public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2 // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/sec/resourceparse/Person;

  public void setName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2 // Field name:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 13: 0
        line 14: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/sec/resourceparse/Person;
            0       6     1  name   Ljava/lang/String;
複製代碼

這裏截取了部份內容,先簡單看一下,首先是類信息的介紹:工具

public class com.sec.resourceparse.Person
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
複製代碼

類名,編譯的JDK版本,以及訪問修飾符
而後字符串池:測試

Constant pool:
   #1 = Methodref #5.#27 // java/lang/Object."<init>":()V
   #2 = Fieldref #4.#28 // com/sec/resourceparse/Person.name:Ljava/lang/String;
   #3 = Fieldref #4.#29 // com/sec/resourceparse/Person.age:I
   #4 = Class #30 // com/sec/resourceparse/Person
   #5 = Class #31 // java/lang/Object
   #6 = Utf8 name
   #7 = Utf8 Ljava/lang/String;
複製代碼

這裏包含整個類裏面的字符串,包含聲明的類信息,屬性等
最後是方法的信息:ui

public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2 // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/sec/resourceparse/Person;
複製代碼

這裏主要是方法名,訪問修飾符,以及操做棧執行流程信息
看完整個類的class文件,下面介紹字節碼相關的基礎知識。this

訪問修飾符

上述字節碼中類,屬性以及方法中均有flag信息,這個就是修飾符,在字節碼中類訪問修飾符及對應值以下所示:spa

標誌符名稱 標誌符值 釋義
ACC_PUBLIC 0x0001 Public 類型
ACC_FINAL 0x0010 Final類型
ACC_SUPER 0x0020 是否容許使用invokespecial字節碼指令的新語義
ACC_INTERFACE 0x0200 接口修飾符
ACC_ABSTRACT 0x0400 abstract修飾符
ACC_SYNTHETIC 0x1000 標誌這個類並不是由用戶代碼生成
ACC_ANNOTATION 0x2000 註解修飾符
**ACC_ENUM 0x400 枚舉修飾符

上面介紹的是類的訪問修飾符,那麼屬性以及方法的也是相似的,只是相對而言比較簡單,這裏就不繼續展開了。.net

類型對照表

JAVA中有基本類型,數組,以及對象,字節碼中對類型的表示有所區別,對照表以下所示:

類型 字節碼錶示 釋義
byte B 字節
boolean Z bool
char C 字符
short S 短整型
int I 整型
float F 浮點數
long J 長整型
double D 浮點數
void V 空返回值
Ljava/lang/Object; 對象類型
數組 [] [

其中類是以L開頭,中間是類路徑,最後以;結尾,上面的數組是單個數組,要結合其餘類型一塊兒使用,如int[]的字節碼是[I,int[][]的字節碼是[[I.

方法解析

上面已經介紹了訪問修飾符以及JAVA字節碼中類型對照,下面講解一下方法的解析,拿上面的方法舉例,以下所示:

public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2 // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/sec/resourceparse/Person;
複製代碼
  • descriptor:方法描述,描述的是方法參數以及返回值類型,其格式爲: (參數類型)返回值類型,這裏表示方法爲無參且返回值爲String
  • flags: 爲方法的訪問修飾符,這裏表示爲Public
  • Code:具體方法棧的描述
  • stack:棧分配最大深度
  • locals:方法內局部變量個數
  • args_size:方法參數數量
  • LineNumberTable:方法行數信息(不關注,沒有細看)
  • LocalVariableTable:局部變量對照表

這裏簡單解釋一下,類方法最少有一個參數,這個參數就是類對象自己,至關於this關鍵字,並且下標是0。

字節碼指令

上面已經介紹了字節碼相關的基礎知識,可是沒有詳細說明字節碼指令相關內容,本節就重點介紹字節碼指令內容,字節碼指令主要分爲以下幾類:

  • 存儲與加載類指令
    加載參數到操做棧,或者將操做棧中的數據存到局部變量中,主要包括load系列指令,store和push等指令
  • 對象操做指令
    對象指令主要包括new生成對象,從對象中獲取屬性等操做,如getField和putField以及getStatic和putStatic等
  • 棧管理指令 pop和dup等壓棧和推出棧指令
  • 運算指令
    運算指令主要是對數據進行加減乘除等指令,這裏也只會在操做棧中執行
  • 控制跳轉指令
    ifelse等條件判斷指令,還有goto等
  • 方法調用和返回指令
    主要包括invoke系列指令和return系列指令,其中invoke是執行方法的指令,return是返回系列指令

操做棧流程

上面已經基本介紹完字節碼全部的內容了,這裏實戰講解方法操做流程。先記住下面這個點:
JAVA方法執行都是基於棧進行的,方法調用指令調用後都會出棧,若是方法有返回值,則將返回值壓棧

先分析一個簡單的:

public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2 // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/sec/resourceparse/Person;
複製代碼

1.aload_0:這裏是將第0個參數,壓棧,參數的類型是對象(前面分析過是this)

2.getfield:從當前棧頂對象獲取name的屬性,而且將其壓入棧中

3.areturn:當前棧頂是一個String類型的值,因此返回的使用要使用areturn

再介紹一個稍微複雜一點的列子:

public class Manager {

    public static void main(String [] args) {
        String resPath = "/Users/Desktop/resources.arsc";
        FileInputStream ins = null;
        ByteArrayOutputStream ous = null;
        try {
            ins = new FileInputStream(new File(resPath));
            ous = new ByteArrayOutputStream();
            int length = -1;
            byte data[] = new byte[4 * 1024];
            while ((length = ins.read(data)) != -1) {
                ous.write(data, 0, length);
            }
            byte[] resData = ous.toByteArray();
            ParseUtils.parseRes(resData);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

對應的字節碼以下所示:

public com.sec.resourceparse.Manager();
    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 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/sec/resourceparse/Manager;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=5, locals=7, args_size=1
         0: ldc           #2 // String /Users/Desktop/resources.arsc
         2: astore_1
         3: aconst_null
         4: astore_2
         5: aconst_null
         6: astore_3
         7: new           #3 // class java/io/FileInputStream
        10: dup
        11: new           #4 // class java/io/File
        14: dup
        15: aload_1
        16: invokespecial #5 // Method java/io/File."<init>":(Ljava/lang/String;)V
        19: invokespecial #6 // Method java/io/FileInputStream."<init>":(Ljava/io/File;)V
        22: astore_2
        23: new           #7 // class java/io/ByteArrayOutputStream
        26: dup
        27: invokespecial #8 // Method java/io/ByteArrayOutputStream."<init>":()V
        30: astore_3
        31: iconst_m1
        32: istore        4
        34: sipush        4096
        37: newarray       byte
        39: astore        5
        41: aload_2
        42: aload         5
        44: invokevirtual #9 // Method java/io/FileInputStream.read:([B)I
        47: dup
        48: istore        4
        50: iconst_m1
        51: if_icmpeq     66
        54: aload_3
        55: aload         5
        57: iconst_0
        58: iload         4
        60: invokevirtual #10 // Method java/io/ByteArrayOutputStream.write:([BII)V
        63: goto          41
        66: aload_3
        67: invokevirtual #11 // Method java/io/ByteArrayOutputStream.toByteArray:()[B
        70: astore        6
        72: aload         6
        74: invokestatic  #12 // Method com/sec/resourceparse/ParseUtils.parseRes:([B)V
        77: goto          87
        80: astore        4
        82: aload         4
        84: invokevirtual #14 // Method java/lang/Exception.printStackTrace:()V
        87: return

複製代碼

這個Manager中只聲明瞭一個static的main方法,可是字節碼中有一個init的方法,其實就是默認的無參構造方法,先看一下這個方法的字節碼:

public com.sec.resourceparse.Manager();
    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 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/sec/resourceparse/Manager;
複製代碼

1.aload_0:將this對象壓入棧中
2.invokespecial:調用棧頂對象的特殊方法init方法,因爲init的返回值類型爲V,因此調用後棧頂就爲空
3.return:因爲棧頂沒有值,因此直接執行return指令就能夠了

再重點看下另一個方法:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=5, locals=7, args_size=1
         0: ldc           #2 // String /Users/Desktop/resources.arsc
         2: astore_1
         3: aconst_null
         4: astore_2
         5: aconst_null
         6: astore_3
         7: new           #3 // class java/io/FileInputStream
        10: dup
        11: new           #4 // class java/io/File
        14: dup
        15: aload_1
        16: invokespecial #5 // Method java/io/File."<init>":(Ljava/lang/String;)V
        19: invokespecial #6 // Method java/io/FileInputStream."<init>":(Ljava/io/File;)V
        22: astore_2
        23: new           #7 // class java/io/ByteArrayOutputStream
        26: dup
        27: invokespecial #8 // Method java/io/ByteArrayOutputStream."<init>":()V
        30: astore_3
        31: iconst_m1
        32: istore        4
        34: sipush        4096
        37: newarray       byte
        39: astore        5
        41: aload_2
        42: aload         5
        44: invokevirtual #9 // Method java/io/FileInputStream.read:([B)I
        47: dup
        48: istore        4
        50: iconst_m1
        51: if_icmpeq     66
        54: aload_3
        55: aload         5
        57: iconst_0
        58: iload         4
        60: invokevirtual #10 // Method java/io/ByteArrayOutputStream.write:([BII)V
        63: goto          41
        66: aload_3
        67: invokevirtual #11 // Method java/io/ByteArrayOutputStream.toByteArray:()[B
        70: astore        6
        72: aload         6
        74: invokestatic  #12 // Method com/sec/resourceparse/ParseUtils.parseRes:([B)V
        77: goto          87
        80: astore        4
        82: aload         4
        84: invokevirtual #14 // Method java/lang/Exception.printStackTrace:()V
        87: return
複製代碼

方法解釋:

  1. ([Ljava/lang/String;)V:參數是一個String的一維數組,無返回值
  2. flags:訪問修飾符爲static 和 public的

堆棧操做解釋:
0:壓棧一個String類型的對象,值爲:"/Users/Desktop/resources.arsc"

2-6:

  1. 彈出棧頂元素而且存在局部變量1中
  2. 將null壓入棧中
  3. 彈出棧頂元素null,而且存在局部變量2中
  4. 將null壓入棧中
  5. 彈出棧頂元素null,而且存在局部變量3中

上面操做結束後,方法棧和局部變量以下所示:

7-30:

  1. new一個java/io/FileInputStream對象,而且壓入棧中
  2. dup:將上面產生的對象再壓入棧中,當前棧有2個FileInputStream對象了
  3. new一個java/io/File對象,而且壓入棧中
  4. dup:將上面產生的File對象再壓入棧中,當前棧有2個File對象了
  5. aload_1:將局部變量1壓入棧中,也就是String值壓入棧中
  6. invokespecial:調用File的init方法,參數是String,無返回值
    說明: 5-6就是將建立出來的File對象,調用其構造方法的過程,這裏應該弄清楚爲何建立對象後要壓2次棧了
  7. invokespecial:調用FileInputStream對象的方法,參數是File,無返回值
  8. astore_2:將棧頂元素存到局部變量2中
  9. new一個java/io/ByteArrayOutputStream對象,而且壓入棧中
  10. dup:將上面產生的對象再壓入棧中,當前棧中有2個ByteArrayOutputStream對象
  11. invokespecial:調用ByteArrayOutputStream的init方法
  12. 將棧頂元素存到局部變量3中

31-42

  1. iconst_m1:將-1壓入棧中
  2. istore 4:將棧頂彈出,存到局部變量4中
  3. sipush 4096:將4096 int類型的數壓入棧頂
  4. newarray :取出棧的數,建立數組,並壓入棧中
  5. astore 5:彈出棧的元素而且存入局部變量5中
  6. aload_2:將局部變量2壓入棧中
  7. aload 5:將局部變量5壓入棧中
  8. invokevirtual:執行FileInputStream的read方法,參數爲byte數組,返回值爲int
  9. dup:複製棧頂元素,而且壓入棧中
  10. istore 4:彈出棧頂元素而且存到局部變量4中
  11. iconst_m1:將-1再壓入棧中
  12. if_icmpeq 66:比較棧頂2個int數是否相等,相等就直接跳到66行,負責執行下面的邏輯

上面邏輯基本就這樣分析,這個方法比較長,就不繼續向下分析,都是同樣的步驟

操做棧流程的關鍵: 全部的操做都伴隨着壓棧和出棧的邏輯,如方法調用,使用到的在棧中的類和參數會被出棧,若是方法有返回值,則將返回值壓棧。

總結

字節碼知識仍是比較重要的,理解字節碼知識能清晰的理解JVM運行機制,同時爲後面AOP直接操做字節碼打下基礎。

參考:
segmentfault.com/a/119000000… my.oschina.net/ta8210/blog…

相關文章
相關標籤/搜索