你需要知道的那些 Java 位元組碼知識

餓了麼物流技術團隊發表於2019-04-01

作者簡介

茂功,蜂鳥物流最早的一批骨幹,前後參與/主導多個重點系統設計與開發工作,目前負責代理商基礎服務、網格商圈、配送範圍產線,平時喜歡專研技術,主攻Java,擅長線上排障,穩定性治理。

1. class檔案中的資料型別

每個class檔案都是由8個位元組為單位的位元組流構成,class檔案格式採用類似於C語言結構體的偽結構來描述,在這種偽結構中只有兩種資料型別:無符號數和表。

  • 無符號數
    無符號數使用u1、u2、u4和u8分別表示1個位元組、2個位元組、4個位元組和8個位元組的無符號數。

  • 表是由無符號數和其他表作為資料項構成的資料結構。表經常以“_info”字尾表示。

2. class檔案結構

class檔案結構如下表:

型別 名稱 數量 說明
u4 magic 1 魔數
u2 minor_version 1 副版本號
u2 major_version 1 主版本號
u2 constant_pool_count 1 常量池計數器
cp_info constant_pool constant_pool_count-1 常量池
u2 access_flags 1 類的訪問標誌
u2 this_class 1 類在常量池中的索引
u2 super_class 1 父類在常量池中的索引
u2 interfaces_count 1 介面計數器,直接父介面的數量
u2 interfaces interfaces_count 介面表
u2 fields_count 1 欄位計數器
field_info fields fields_count 欄位表
u2 methods_count 1 方法計數器
method_info methods methods_count 方法表
u2 attributes_count 1 屬性計數器
attribute_info attributes attributes_count 屬性表

下面根據一個HelloWorld程式具體分析下class檔案。
原始碼HelloWorld.java

package com.xh.hello;

public class HelloWorld {
    private static int abc = 123;

    public static void main(String[] args) {
        printABC();
    }

    private static void printABC() {
        System.out.println(abc);
    }
}
複製程式碼

使用javac編譯該原始檔javac com/xh/hello/HelloWorld.java,得到HelloWorld.class檔案。使用十六進位制檔案檢視器檢視此檔案內容。

cafe babe 0000 0034 0023 0a00 0700 140a
0006 0015 0900 1600 1709 0006 0018 0a00
1900 1a07 001b 0700 1c01 0003 6162 6301
0001 4901 0006 3c69 6e69 743e 0100 0328
2956 0100 0443 6f64 6501 000f 4c69 6e65
4e75 6d62 6572 5461 626c 6501 0004 6d61
696e 0100 1628 5b4c 6a61 7661 2f6c 616e
672f 5374 7269 6e67 3b29 5601 0008 7072
696e 7441 4243 0100 083c 636c 696e 6974
3e01 000a 536f 7572 6365 4669 6c65 0100
0f48 656c 6c6f 576f 726c 642e 6a61 7661
0c00 0a00 0b0c 0010 000b 0700 1d0c 001e
001f 0c00 0800 0907 0020 0c00 2100 2201
0017 636f 6d2f 7868 2f68 656c 6c6f 2f48
656c 6c6f 576f 726c 6401 0010 6a61 7661
2f6c 616e 672f 4f62 6a65 6374 0100 106a
6176 612f 6c61 6e67 2f53 7973 7465 6d01
0003 6f75 7401 0015 4c6a 6176 612f 696f
2f50 7269 6e74 5374 7265 616d 3b01 0013
6a61 7661 2f69 6f2f 5072 696e 7453 7472
6561 6d01 0007 7072 696e 746c 6e01 0004
2849 2956 0021 0006 0007 0000 0001 000a
0008 0009 0000 0004 0001 000a 000b 0001
000c 0000 001d 0001 0001 0000 0005 2ab7
0001 b100 0000 0100 0d00 0000 0600 0100
0000 0300 0900 0e00 0f00 0100 0c00 0000
2000 0000 0100 0000 04b8 0002 b100 0000
0100 0d00 0000 0a00 0200 0000 0700 0300
0800 0a00 1000 0b00 0100 0c00 0000 2600
0200 0000 0000 0ab2 0003 b200 04b6 0005
b100 0000 0100 0d00 0000 0a00 0200 0000
0b00 0900 0c00 0800 1100 0b00 0100 0c00
0000 1e00 0100 0000 0000 0610 7bb3 0004
b100 0000 0100 0d00 0000 0600 0100 0000
0400 0100 1200 0000 0200 13
複製程式碼

使用 javap -verbose com.xh.hello.HelloWorld指令解析該類,得到如下內容,配合class檔案一起分析。

Classfile /Users/maogong.han/java_tmp/com/xh/hello/HelloWorld.class
  Last modified 2019-3-21; size 555 bytes
  MD5 checksum 4b275a3e082827230300dcb233141209
  Compiled from "HelloWorld.java"
public class com.xh.hello.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#20         // java/lang/Object."<init>":()V
   #2 = Methodref          #6.#21         // com/xh/hello/HelloWorld.printABC:()V
   #3 = Fieldref           #22.#23        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Fieldref           #6.#24         // com/xh/hello/HelloWorld.abc:I
   #5 = Methodref          #25.#26        // java/io/PrintStream.println:(I)V
   #6 = Class              #27            // com/xh/hello/HelloWorld
   #7 = Class              #28            // java/lang/Object
   #8 = Utf8               abc
   #9 = Utf8               I
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               printABC
  #17 = Utf8               <clinit>
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #10:#11        // "<init>":()V
  #21 = NameAndType        #16:#11        // printABC:()V
  #22 = Class              #29            // java/lang/System
  #23 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;
  #24 = NameAndType        #8:#9          // abc:I
  #25 = Class              #32            // java/io/PrintStream
  #26 = NameAndType        #33:#34        // println:(I)V
  #27 = Utf8               com/xh/hello/HelloWorld
  #28 = Utf8               java/lang/Object
  #29 = Utf8               java/lang/System
  #30 = Utf8               out
  #31 = Utf8               Ljava/io/PrintStream;
  #32 = Utf8               java/io/PrintStream
  #33 = Utf8               println
  #34 = Utf8               (I)V
{
  public com.xh.hello.HelloWorld();
    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=0, locals=1, args_size=1
         0: invokestatic  #2                  // Method printABC:()V
         3: return
      LineNumberTable:
        line 7: 0
        line 8: 3

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        123
         2: putstatic     #4                  // Field abc:I
         5: return
      LineNumberTable:
        line 4: 0
}
SourceFile: "HelloWorld.java"

複製程式碼

2.1 魔數與class檔案版本號

  • 魔數是class檔案的前4個位元組,是一個固定值:0xcafebabe。該值唯一的作用就是表示檔案是否可以被JVM接受,不嚴格的說就是表示該檔案是否是class檔案。
  • 版本號

你需要知道的那些 Java 位元組碼知識

2.2 常量池

  • 常量池計數器
    常量池計數器表示常量池中的項的個數,在class檔案中的位置是主版本號之後的2個位元組,也就是第9和第10個位元組。常量池計數器是從1開始的,在constant_pool表中,只有索引大於0且小於constant_pool_count的項才是有效的。
    本例中constant_pool_count的值是0x0023,換算成十進位制是35,表示constant_pool中有34項,有效索引是1到34,剛好是用javap解析出來的34個常量。
  • 常量池
    class檔案中,constant_pool_count之後緊接著就是常量池的內容。每個常量池項(cp_info)都是由一個u1型別的tag和一個具體型別的表構成,具體型別由tag的值決定。如下表:
tag值 對應的型別
7 CONSTANT_Class_info
9 CONSTANT_Fieldref_info
10 CONSTANT_Methodref_info
11 CONSTANT_InterfaceMethodref_info
8 CONSTANT_String_info
3 CONSTANT_Integer_info
4 CONSTANT_Float_info
5 CONSTANT_Long_info
6 CONSTANT_Double_info
12 CONSTANT_NameAndType_info
1 CONSTANT_Utf8_info
15 CONSTANT_MethodHandle_info
16 CONSTANT_MethodType_info
18 CONSTANT_InvokeDynamic_info

擷取上文反編譯出來的常量池部分資訊,來分析常量池中的第一個常量。

#1 = Methodref          #7.#20         // java/lang/Object."<init>":()V
#7 = Class              #28            // java/lang/Object
#10 = Utf8               <init>
#11 = Utf8               ()V
#20 = NameAndType        #10:#11        // "<init>":()V
#28 = Utf8               java/lang/Object

複製程式碼

你需要知道的那些 Java 位元組碼知識

"#1"表示常量池中索引是1。class檔案中的0x0a位置開始。型別是Methodref。Methodref型別的結構如下:

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}
複製程式碼

Methodref中tag的值為0x0a,十進位制為10,正好表示CONSTANT_Methodref_info型別。
class_index的值為0x0007,十進位制為7,指向索引為7的常量池的項。#7是CONSTANT_Class_info型別,指向CONSTANT_Utf8_info型別的#28,表示此常量屬於java/lang/Object的。
name_and_type_index的值為0x0014,十進位制為20,指向索引為20的常量池的項,此項是NameAndType(欄位或方法)型別,方法名索引(name_index)指向常量池的#10,為一個CONSTANT_Utf8_info型別,表示方法名為"<init>";NameAndType的方法描述索引(descriptor_index)指向常量池的#11,表示無參型別。

CONSTANT_Class_info型別結構:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}
複製程式碼

tag值為7,表示是CONSTANT_Class_info型別。 name_index是指向常量池中一個型別為CONSTANT_Utf8_info的常量索引,表示類或者介面的名字。

CONSTANT_Utf8_info型別結構:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}
複製程式碼

tag值為1,表示CONSTANT_Utf8_info型別。bytes指的是字串值的bytes陣列。 bytes表示的字串和十六進位制轉換可由下程式完成:

    public static String printHexString(byte[] b) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < b.length; i++) {
            String hex = Integer.toHexString(b[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            sb.append(hex);
        }
        return sb.toString();
    }
複製程式碼

上文提到的各項型別結構和說明可參考《Java虛擬機器規範》

2.3 訪問標誌符

在常量池之後,緊挨著是佔2個位元組的訪問標誌符:0x0021。
ACC_PUBLIC(0x0001)+ACC_SUPER (0x0020)。
access_flags表示類或介面的訪問許可權。其取值和含義見下表:

標記名 含義
ACC_PUBLIC 0x0001 為public型別
ACC_FINAL 0x0010 是否為final型別,只有類可設定
ACC_SUPER 0x0020 當用到invokespecial指令時,是否需要特殊處理的父類方法
ACC_INTERFACE 0x0200 標識介面,不是類
ACC_ABSTRACT 0x0400 標識是否為abstract,是否可以例項化
ACC_SYNTHETIC 0x1000 標識並非由Java原始碼生成的程式碼,而是由編譯器生成的
ACC_ANNOTATION 0x2000 註解型別
ACC_ENUM 0x4000 列舉型別

2.4 類索引、父類索引和介面索引

訪問標記符之後,緊接著是類索引、父類索引和介面索引。
類索引和父類索引都是一個u2型別的資料,介面索引是一組u2型別資料的集合。他們的值都表示在常量池中的索引。另外這三項資料確定了類的關係:單繼承、多實現。

  • 類索引(this_class)
    類索引在常量池中的索引值為0x0006,十進位制為6,指向一個CONSTANT_Class_info型別的常量,其tag值為7,name_index指向27的索引。
#6 = Class              #27            // com/xh/hello/HelloWorld
#27 = Utf8               com/xh/hello/HelloWorld
複製程式碼

你需要知道的那些 Java 位元組碼知識

  • 父類索引(super_class)
    類索引之後,是父類索引。在常量池中的索引值為0x0007,十進位制為7,指向一個CONSTANT_Class_info型別的常量,其tag值為7,name_index指向28的索引。
#7 = Class              #28            // java/lang/Object
#28 = Utf8               java/lang/Object
複製程式碼

你需要知道的那些 Java 位元組碼知識

  • 介面索引(interfaces) 在父類索引之後的內容是介面索引計數器(interfaces_count)和介面索引表(interfaces),class檔案中interfaces_count的值為0x0000,表示未實現任何介面,這裡不在討論。

2.5 欄位

在介面索引表之後是欄位索引計數器和欄位索引表。欄位索引計數器是一個u2型別的數值,class檔案中的值為0x0001,表示有一個欄位。
欄位表中的每項都表示指向常量池中的一個索引,該索引指向一個field_info結構的資料。欄位表描述當前類或介面宣告的所有欄位,但不包括從父類或介面中繼承過來的。

field_info {
  u2 access_flags;
  u2 name_index;
  u2 descriptor_index;
  u2 attributes_count;
  attribute_info attrubutes[attributes_count];
}
複製程式碼
  • access_flags項定義欄位的訪問許可權和基礎屬性,如下表:

欄位access_flags表:

標記名 說明
ACC_PUBLIC 0x0001 public,欄位可以被從任何package訪問
ACC_PRIVATE 0x0002 private,欄位只可以被該類自身訪問
ACC_PROTECTED 0x0004 protected,欄位可以被子類訪問
ACC_STATIC 0x0008 static,靜態欄位
ACC_FINAL 0x0010 final,欄位定義後無法修改
ACC_VOLATILE 0x0040 volatile欄位
ACC_TRANSIENT 0x0080 transient,是否被序列化
ACC_SYNTHETIC 0x1000 是否編譯器自動生成
ACC_ENUM 0x4000 是否為列舉
  • name_index項是常量池的一個索引,該索引指向一個CONSTANT_Utf8_info型別,表示欄位的非全限定名。
  • descriptor_index項是常量池的一個索引,該索引指向一個CONSTANT_Utf8_info型別,表示欄位的描述符。
    欄位描述符如下表:
字元 型別 說明
B byte
C char
D double
F float
I int
J long
S short
Z boolean
LClassname reference 一個Classname的例項
[ reference 一個一維陣列
  • attribute_info表示的是欄位的附加屬性。

本例class檔案中access_flags的值為0x000a:ACC_PRIVATE(0x0002) + ACC_STATIC(0x0008)。name_index的值為0x0008,指向常量池的索引為8。descriptor_index的值為0x0009,指向常量池的索引為9。附加屬性的值為0x0000,表示沒有屬性。綜上該欄位是一個被private和static修飾的int型別的欄位,名稱是"abc"。

#8 = Utf8               abc
#9 = Utf8               I
複製程式碼

你需要知道的那些 Java 位元組碼知識

2.6 方法區域

欄位之後,緊接著是方法區域。有方法計數器(methods_count)和方法表(methods)。方法計數器是一個u2型別的數值,本例class中值為0x0004,表示有4個方法。方法表中每一項都是method_info結構。

method_info {
  u2 access_flags;
  u2 name_index;
  u2 descriptor_index;
  u2 attributes_count;
  attribute_info attributes[attributes_count];
}
複製程式碼
  • access_flags表示方法的訪問許可權和基本屬性。如下表:
    方法access_flags表:
標記名 說明
ACC_PUBLIC 0x0001 public,方法可以被從任何package訪問
ACC_PRIVATE 0x0002 private,方法只可以被該類自身訪問
ACC_PROTECTED 0x0004 protected,方法可以被子類訪問
ACC_STATIC 0x0008 static,靜態方法
ACC_FINAL 0x0010 final,方法不能被重寫
ACC_SYNCHRONIZED 0x0020 synchronized,方法加同步
ACC_BRIDGE 0x0040 bridge,方法由編譯器生成
ACC_VARARGS 0x0080 方法有可變引數
ACC_NATIVE 0x0100 native,方法引用非Java語言的本地方法
ACC_ABSTRACT 0x0400 abstract,抽象方法
ACC_STRICT 0x0800 strictfp,方法使用FP-strict浮點格式
ACC_SYNTHETIC 0x1000 方法在原始檔中不出現,由編譯器產生
  • name_index項是常量池的一個索引,該索引指向一個CONSTANT_Utf8_info型別,表示方法的非全限定名或者初始化方法的名字(<init>或<clinit>方法)。
  • descriptor_index項是常量池的一個索引,該索引指向一個CONSTANT_Utf8_info型別,表示方法的描述符。
  • attributes_count和attributes分別表示方法附加屬性的計數器和附加屬性表。

這裡分析第一個方法。方法計數器0x0004之後,是第一個方法的access_flags,值為0x0001,表示public型別。接下來是name_index,值為0x0001,指向常量池索引為1的項,該項表示的是java/lang/Object."<init>":()V方法。接下來是descriptor_index,值為0x000a,指向常量池索引為10的項,表示方法的非全限定名。接下來是attributes_count,值為0x000b,表示有11個附加屬性,之後是這11個附加屬性的資料,包含code和操作符,這裡不在展開,之後會專門寫解析的內容。

#1 = Methodref          #7.#20         // java/lang/Object."<init>":()V
#7 = Class              #28            // java/lang/Object
#10 = Utf8               <init>
#11 = Utf8               ()V
#20 = NameAndType        #10:#11        // "<init>":()V
#28 = Utf8               java/lang/Object
複製程式碼

你需要知道的那些 Java 位元組碼知識

2.7 屬性

這裡主要記錄檔案的屬性。有屬性計數器attributes_count和屬性表attributes。
本例class檔案中,attributes_count值為0x0001,表示有一個屬性。屬性表中的每一項都常量池中的一個索引,該索引處的格式為:

attribute_info {
  u2 attribute_name_index;
  u4 attribute_length;
  u2 source_file_index;
}
複製程式碼

attribute_name_index的值為0x0012,指向索引為18的常量池的項,該項是一個CONSTANT_Utf8_info結構,表示“SourceFile”。然後是attribute_length的值為0x00000002,表示緊跟其後的有2個位元組,source_file_index值為0x0013,指向索引為19的常量池項,該項是一個CONSTANT_Utf8_info結構,表示“HelloWorld.java”。至此class檔案簡單分析完畢。

#18 = Utf8               SourceFile
#19 = Utf8               HelloWorld.java
複製程式碼

2.8 附class檔案結構備註

魔數:
cafe babe 

副版本號和主版本號:
0000 0034 

常量池:
0023 0a00 0700 140a
0006 0015 0900 1600 1709 0006 0018 0a00
1900 1a07 001b 0700 1c01 0003 6162 6301
0001 4901 0006 3c69 6e69 743e 0100 0328
2956 0100 0443 6f64 6501 000f 4c69 6e65
4e75 6d62 6572 5461 626c 6501 0004 6d61
696e 0100 1628 5b4c 6a61 7661 2f6c 616e
672f 5374 7269 6e67 3b29 5601 0008 7072
696e 7441 4243 0100 083c 636c 696e 6974
3e01 000a 536f 7572 6365 4669 6c65 0100
0f48 656c 6c6f 576f 726c 642e 6a61 7661
0c00 0a00 0b0c 0010 000b 0700 1d0c 001e
001f 0c00 0800 0907 0020 0c00 2100 2201
0017 636f 6d2f 7868 2f68 656c 6c6f 2f48
656c 6c6f 576f 726c 6401 0010 6a61 7661
2f6c 616e 672f 4f62 6a65 6374 0100 106a
6176 612f 6c61 6e67 2f53 7973 7465 6d01
0003 6f75 7401 0015 4c6a 6176 612f 696f
2f50 7269 6e74 5374 7265 616d 3b01 0013
6a61 7661 2f69 6f2f 5072 696e 7453 7472
6561 6d01 0007 7072 696e 746c 6e01 0004
2849 2956 

訪問標記符:
0021 

類在常量池中的索引:
0006 
父類在常量池中的索引:
0007 
介面索引計數器:
0000 

欄位索引計數器:
0001 
第一個欄位field_info:
000a access_flags
0008 name_index
0009 descriptor_index
0000 附加屬性

方法計數器:
0004 
方法表:
0001 000a 000b 0001
000c 0000 001d 0001 0001 0000 0005 2ab7
0001 b100 0000 0100 0d00 0000 0600 0100
0000 0300 0900 0e00 0f00 0100 0c00 0000
2000 0000 0100 0000 04b8 0002 b100 0000
0100 0d00 0000 0a00 0200 0000 0700 0300
0800 0a00 1000 0b00 0100 0c00 0000 2600
0200 0000 0000 0ab2 0003 b200 04b6 0005
b100 0000 0100 0d00 0000 0a00 0200 0000
0b00 0900 0c00 0800 1100 0b00 0100 0c00
0000 1e00 0100 0000 0000 0610 7bb3 0004
b100 0000 0100 0d00 0000 0600 0100 0000
04

屬性計數器:
00 01
第一個屬性:
00 12 attribute_name_index
00 0000 02 attribute_length
00 13 source_file_index
複製程式碼

3. 參考資料

  1. The Java Virtual Machine Instruction Set
  2. 《Java虛擬機器規範》





閱讀部落格還不過癮?

歡迎大家掃二維碼通過新增群助手,加入交流群,討論和部落格有關的技術問題,還可以和博主有更多互動

你需要知道的那些 Java 位元組碼知識
部落格轉載、線下活動及合作等問題請郵件至 shadowfly_zyl@hotmail.com 進行溝通

相關文章