你須要知道的那些 Java 字節碼知識

做者簡介html

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

1. class文件中的數據類型

每一個class文件都是由8個字節爲單位的字節流構成,class文件格式採用相似於C語言結構體的僞結構來描述,在這種僞結構中只有兩種數據類型:無符號數和表。數組

  • 無符號數
    無符號數使用u一、u二、u4和u8分別表示1個字節、2個字節、4個字節和8個字節的無符號數。

  • 表是由無符號數和其餘表做爲數據項構成的數據結構。表常常以「_info」後綴表示。

2. class文件結構

class文件結構以下表:bash

類型 名稱 數量 說明
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文件。使用十六進制文件查看器查看此文件內容。oracle

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文件一塊兒分析。app

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文件。
  • 版本號

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

截取上文反編譯出來的常量池部分信息,來分析常量池中的第一個常量。jvm

#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

複製代碼

"#1"表示常量池中索引是1。class文件中的0x0a位置開始。類型是Methodref。Methodref類型的結構以下:ui

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,表示無參類型。this

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
複製代碼

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

  • 接口索引(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
複製代碼

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
複製代碼

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虛擬機規範》





閱讀博客還不過癮?

歡迎你們掃二維碼經過添加羣助手,加入交流羣,討論和博客有關的技術問題,還能夠和博主有更多互動

博客轉載、線下活動及合做等問題請郵件至 shadowfly_zyl@hotmail.com 進行溝通
相關文章
相關標籤/搜索