class文件解析

前言

操做java字節碼,免不了要對字節碼文件有一個詳細的認識。本文主要記錄學習小冊 《JVM字節碼從入門到精通》 的筆記,以供參考。java

查看class文件

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}
複製代碼

打開命令行,將此文件經過 javac 命令編譯成 jvm 能識別的 class 文件數組

javac Hello.java
複製代碼

而後用 xxd 命令以 16 進制的方式查看這個 class 文件bash

xxd Hello.class
複製代碼

16進制以下:數據結構

00000000: cafe babe 0000 0034 001d 0a00 0600 0f09  .......4........
00000010: 0010 0011 0800 120a 0013 0014 0700 1507  ................
00000020: 0016 0100 063c 696e 6974 3e01 0003 2829  .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e  V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 046d 6169  umberTable...mai
00000050: 6e01 0016 285b 4c6a 6176 612f 6c61 6e67  n...([Ljava/lang
00000060: 2f53 7472 696e 673b 2956 0100 0a53 6f75  /String;)V...Sou
00000070: 7263 6546 696c 6501 000a 4865 6c6c 6f2e  rceFile...Hello.
00000080: 6a61 7661 0c00 0700 0807 0017 0c00 1800  java............
00000090: 1901 000b 4865 6c6c 6f20 576f 726c 6407  ....Hello World.
000000a0: 001a 0c00 1b00 1c01 0021 636f 6d2f 7869  .........!com/xi
000000b0: 6173 6d2f 6173 6d64 656d 6f2f 636c 6173  asm/asmdemo/clas
000000c0: 7374 6573 742f 4865 6c6c 6f01 0010 6a61  stest/Hello...ja
000000d0: 7661 2f6c 616e 672f 4f62 6a65 6374 0100  va/lang/Object..
000000e0: 106a 6176 612f 6c61 6e67 2f53 7973 7465  .java/lang/Syste
000000f0: 6d01 0003 6f75 7401 0015 4c6a 6176 612f  m...out...Ljava/
00000100: 696f 2f50 7269 6e74 5374 7265 616d 3b01  io/PrintStream;.
00000110: 0013 6a61 7661 2f69 6f2f 5072 696e 7453  ..java/io/PrintS
00000120: 7472 6561 6d01 0007 7072 696e 746c 6e01  tream...println.
00000130: 0015 284c 6a61 7661 2f6c 616e 672f 5374  ..(Ljava/lang/St
00000140: 7269 6e67 3b29 5600 2100 0500 0600 0000  ring;)V.!.......
00000150: 0000 0200 0100 0700 0800 0100 0900 0000  ................
00000160: 1d00 0100 0100 0000 052a b700 01b1 0000  .........*......
00000170: 0001 000a 0000 0006 0001 0000 0003 0009  ................
00000180: 000b 000c 0001 0009 0000 0025 0002 0001  ...........%....
00000190: 0000 0009 b200 0212 03b6 0004 b100 0000  ................
000001a0: 0100 0a00 0000 0a00 0200 0000 0500 0800  ................
000001b0: 0600 0100 0d00 0000 0200 0e              ...........
複製代碼

一個字節是8位,兩個十六進制數表示一個字節。
jvm

不少文件都以魔數來進行文件類型的區分,class 文件的頭四個字節稱爲魔數,是0xCAFEBABE,這個魔數是 jvm 識別 class文件的標識,虛擬機在加載class 文件前會先檢查這四個字節,若是不是則拒絕加載。函數

class文件是二進制塊,想直接看懂它比較難,javap 命令能夠窺探 class 文件內部細節,其中 javap -c xxx 是用來對class文件進行反編譯學習

xiasmdeMacBook-Pro:test xiasm$ javap -c Hello
警告: 二進制文件Hello包含com.xiasm.asmdemo.classtest.Hello

1  Compiled from "Hello.java"
2  public class com.xiasm.asmdemo.classtest.Hello {
3    public com.xiasm.asmdemo.classtest.Hello();
4      Code:
5         0: aload_0
6         1: invokespecial #1 // Method java/lang/Object."<init>":()V
7         4: return
8
9    public static void main(java.lang.String[]);
10     Code:
11        0: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
12        3: ldc           #3 // String Hello World
13        5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14        8: return
15 }
複製代碼

上面代碼前面的數字表示從方法開始算起的字節碼偏移量
ui

代碼前面的行號是我本身加上的,能夠看到,3-7行雖然沒有寫Hello類的構造函數,可是編譯器仍是爲咱們自動加上了。this

  • 第5行:aload_x 操做碼用來把 對象引用 加載到 操做數棧,非靜態的函數都有第一個默認參數,那就是 this,這裏的 aload_0 就是把 this 入棧
  • 第6行:invokespecial #1 invokespecial指令調用實例初始化方法、私有方法、父類方法,#1 指的是常量池中的第一個,這裏是方法引用java/lang/Object."":()V,也即構造器函數
  • 第7行:return,這個操做碼屬於 ireturn、lreturn、freturn、dreturn、areturn 和 return 操做碼組中的一員,其中 i 表示 int,返回整數,同類的還有 l 表示 long,f 表示 float,d 表示 double,a 表示 對象引用。沒有前綴類型字母的 return 表示返回 void

到此,構造器函數就結束了,接下來是 main 函數:spa

  • 第11行:getstatic #2 getstatic獲取指定類的靜態域,並將其值壓入棧頂,#2 表明常量池中的第 2 個,這裏表示的是java/lang/System.out:Ljava/io/PrintStream;,其實就是java.lang.System 類的靜態變量 out(類型是 PrintStream)
  • 第12行:ldc #3 ldc表示將int, float或String型常量值從常量池中推送至棧頂,#3 表明常量池的第三個(字符串 Hello, World)
  • 第13行:invokevirtual #4 invokevirutal 指令調用一個對象的實例方法,#4表示 PrintStream.println(String) 函數引用,並把棧頂兩個元素出棧

class文件結構剖析

理解 class 文件結構是理解字節碼的基石,class 文件結構比較複雜。

Java 虛擬機規定義了 u一、u二、u4 三種數據結構來表示 一、二、4 字節無符號整數,相同類型的若干條數據集合用表(table)的形式來存儲。表是一個變長的結構,由表明長度的表頭(n)和 緊隨着的 n 個數據項組成。class 文件採用相似 C 語言的結構體來存儲數據,以下所示:

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
複製代碼

class文件由下面十個部分組成

  • 魔數(Magic Number)
  • 版本號(Minor&Major Version)
  • 常量池(Constant Pool)
  • 類訪問標記(Access Flags)
  • 類索引(This Class)
  • 超類索引(Super Class)
  • 接口表索引(Interfaces)
  • 字段表(Fields)
  • 方法表(Methods)
  • 屬性表(Attributes)

魔數、主副版本號

這裏的主版本是 52(0x34),虛擬機解析這個類時就知道這是一個 Java 8 編譯出的類,若是類文件的版本號高於 JVM 自身的版本號,加載該類會被直接拋出java.lang.UnsupportedClassVersionError異常

常量池

常量池是class文件中最複雜的數據結構,它是一個變長度的數據項

u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
複製代碼

主要分爲兩部分,一個是常量池大小,一個是常量池的集合數據

  • 常量池大小的由兩個字節表示。假設爲值爲 n,常量池真正有效的索引是 1 ~ n-1。0 屬於保留索引,用來表示不指向任何常量池項。
  • 常量池項(cp_info)集合,最多包含 n-1 個。爲何是最多呢?Long 和 Double 類型的常量會佔用兩個索引位置,若是常量池包含了這兩種類型,實際的常量池項的元素個數比 n-1 要小。每個常量池項都由兩部分構成,下文會說到

Java 虛擬機目前一共定義了 14 種常量類型,這些常量名都以 "CONSTANT" 開頭,以 "info" 結尾,以下表所示:

常量類型 常量值
CONSTANT_Utf8_info 1
CONSTANT_Integer_info 3
CONSTANT_Float_info 4
CONSTANT_Long_info 5
CONSTANT_Double_info 6
CONSTANT_Class_info 7
CONSTANT_String_info 8
CONSTANT_Fieldref_info 9
CONSTANT_Methodref_info 10
CONSTANT_InterfaceMethodref_info 11
CONSTANT_NameAndType_info 12
CONSTANT_MethodHandle_info 15
CONSTANT_MethodType_info 16
CONSTANT_InvokeDynamic_info 18

每一個常量項都由兩部分構成,表示類型的 tag 和表示 內容 的字節數組

cp_info {
    u1 tag;
    u1 info[];
}
複製代碼

再看calss文件的十六進制表示

常量池大小佔2個字節,因此就是0x001d,轉換成10進制就是29

查看類文件的常量池,能夠在 javap 命令上加上 -v 選項,下面是 Hello.class文件的常量池

xiasmdeMacBook-Pro:test xiasm$ javap -v Hello
警告: 二進制文件Hello包含com.xiasm.asmdemo.classtest.Hello
Classfile /Users/xiasm/Desktop/asm_test/Hello.class
  Last modified 2020-1-16; size 443 bytes
  MD5 checksum a9f2551fb88a0a34395ac7cf0a0eedd3
  Compiled from "Hello.java"
public class com.xiasm.asmdemo.classtest.Hello
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref #6.#15 // java/lang/Object."<init>":()V
   #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String #18 // Hello World
   #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class #21 // com/xiasm/asmdemo/classtest/Hello
   #6 = Class #22 // java/lang/Object
   #7 = Utf8 <init>
   #8 = Utf8 ()V
   #9 = Utf8 Code
  #10 = Utf8 LineNumberTable
  #11 = Utf8 main
  #12 = Utf8 ([Ljava/lang/String;)V
  #13 = Utf8 SourceFile
  #14 = Utf8 Hello.java
  #15 = NameAndType #7:#8 // "<init>":()V
  #16 = Class #23 // java/lang/System
  #17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
  #18 = Utf8 Hello World
  #19 = Class #26 // java/io/PrintStream
  #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
  #21 = Utf8 com/xiasm/asmdemo/classtest/Hello
  #22 = Utf8 java/lang/Object
  #23 = Utf8 java/lang/System
  #24 = Utf8 out
  #25 = Utf8 Ljava/io/PrintStream;
  #26 = Utf8 java/io/PrintStream
  #27 = Utf8 println
  #28 = Utf8 (Ljava/lang/String;)V
  
  //... 省略其餘信息
SourceFile: "Hello.java"
複製代碼

Hello.java文件裏沒有Long或Double類型的常量,因此n-1=28個常量項,沒毛病。

類訪問標記

常量池以後存儲的是訪問標記(Access flags),用來標識一個類是是否是final、abstract 等,由兩個字節表示總共能夠有 16 個標記位可供使用,目前只使用了其中的 8 個,以下:

類、超類、接口索引表

這三個部分用來肯定類的繼承關係,this_class 表示類索引,super_name 表示父類索引,interfaces 表示類或者接口的直接父接口。以 this_class 爲例,它是一個兩字節組成。

字段表

緊隨接口索引表以後的是字段表(Fields),類中定義的字段會被存儲到這個集合中,包括類中定義的靜態和非靜態的字段,不包括方法內部定義的變量,字段表也是一個變長結構,以下圖所示:

每一個字段 field_info 的格式以下:

field_info {
    u2             access_flags; 
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
複製代碼

字段結構分爲四部分:

  • access_flags:表示字段的訪問標記,是 public、private 仍是 protected,是不是 static,是不是 final 等
  • name_index:字段名的索引值,指向常量池的的字符串常量
  • descriptor_index:字段描述符的索引,指向常量池的字符串常量
  • attributes_count、attribute_info:表示屬性的個數和屬性集合

方法表

在字段表後面的是方法表,類中定義的方法會被存儲在這裏,與前面介紹的字段表很相似,方法表也是一個變長結構

{
    u2             methods_count;
    method_info    methods[methods_count];
}
複製代碼

由表示方法個數的 methods_count 和對應個數的方法項集合組成,以下圖所示:

方法 method_info 結構分爲四部分:

  • access_flags:表示方法的訪問標記,是 public、private 仍是 protected,是不是 static,是不是 final 等
  • name_index:方法名的索引值,指向常量池的的字符串常量
  • descriptor_index:方法描述符的索引,指向常量池的字符串常量
  • attributes_count、attribute_info:表示方法相關屬性的個數和屬性集合,包含了不少有用的信息,好比方法內部的字節碼就是存放在 Code 屬性中

屬性表

在方法表以後的結構是 class 文件的最後一步部分屬性表。屬性出現的地方比較普遍,不止出如今字段和方法中,在頂層的 class 文件中也會出現。注意,此屬性表存放的不是咱們理解的類裏面的成員屬性,而是class文件定義的屬性,如 ConstantValue 屬性、Code 屬性等。

字節碼/操做碼 概述

Java 虛擬機的指令由一個字節長度的操做碼(opcode)和緊隨其後的可選的操做數(operand)構成。「字節碼」這個名字的由來也是由於操做碼的長度用一個字節表示

<opcode> [<operand1>, <operand2>]
複製代碼

好比將整型常量 100 壓棧到棧頂的指令是bipush 100,其中 bipush 就是操做碼,100 就是操做數。

由於操做碼長度只有 1 個字節長度,這使得編譯後的字節碼文件很是小巧緊湊,但同時也直接限制了整個 JVM 操做碼指令集的數量最多隻能有 256 個,目前已經使用了 200+

字節碼並非某種虛擬 CPU 的機器碼,而是一種介於源碼和機器碼中間的一種抽象表示方法,不過字節碼經過 JIT(Just in time)技術能夠被進一步編譯成機器碼。

相關文章
相關標籤/搜索