解构 java class文件格式 - 以HelloWorld为例

前言

对于Java来说,“字节码(Byte Code)”的存储格式是构成“一次编写,到处运行”的平台无关性的基石。Java虚拟机不与包括Java语言在内的任何程序语言绑定,而只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。
在这里插入图片描述
本文通过彻底解构一段大家耳熟能详的代码(HelloWorld)来一窥JVM虚拟机核心——字节码的“秘密”。
需要使用的工具有:UltraEdit,以及Java bin目录下的javap。
预警:前方多图,继续阅读请谨慎

一、Class文件格式概要

Class文件格式采用的是一种类似C语言结构体的伪结构来存储数据,其只有两种数据类型:“无符号数”“表”。所谓无符号数是指基本的数据类型,以u1、u2、u4…等分别代表1个字节、2个字节、4个字节的无符号数。它可以用来描述数字索引引用数量值或者按照UTF-8编码构成的字符串值

整个Class文件本质上可以看作是一张表,其数据项按严格顺序排列构成,如图一
在这里插入图片描述
下面我们将跟着代码来解读class文件中各类型对应的字节表示。

二、HelloWorld代码

以我们最熟悉的代码行为例:

public class HelloWorld {
    
    
    public static void main(String[] args){
    
    
        System.out.println("Hello World!");
    }
}

其对应class文件内容(IDEA中翻译显示):

public class HelloWorld {
    
    
    public HelloWorld() {
    
    
    }

    public static void main(String[] args) {
    
    
        System.out.println("Hello World!");
    }
}

通过javap 工具(java自带的解析class文件工具)先从整体上看这个class文件的结构是怎样的,后面也将结合这个结构进行解释,代码一

$ javap -verbose HelloWorld.class
Classfile HelloWorld.class
  Last modified 2020-11-30; size 534 bytes
  MD5 checksum f085642c9a175e363b8cde7b90538c83
  Compiled from "HelloWorld.java"
public class HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // Hello World!
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LHelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               Hello World!
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
    
    
  public 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 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LHelloWorld;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

三、字节码解析

下面我们通过UE十六进制方式查看Class文件,并对照“图一”进行分析。

1.Magic Number

魔数,对应类型:u4,头四个字节为:0xCAFEBABE,它唯一的作用是确定这个文件是否为一个能被虚拟机接受的Class文件,如图所示:
UE 16进制内容

2.Minor Version && Major Version

Class文件的版本号,其中第5、第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version),对应类型都为u2,如图,黄色圈的是次版本号,红色圈的是主版本号。其中版本号0x0034转10进制为52,对应JDK8。
在这里插入图片描述
在这里插入图片描述

扫描二维码关注公众号,回复: 12380900 查看本文章

3.常量池

3.1 constant_pool_count

常量池数量,u2类型,对应字节如下图,0x0022 => 34,JVM规定实际数量为该值减一,为33,对照前文中使用javap打印的“代码一”中“Constant pool”也可知。
在这里插入图片描述

3.2 constant_pool

常量池中的每一项常量都是一个表,共有17种不同类型的常量,且各类型有大致相同的解读方式。
表一
在这里插入图片描述
表二
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.1.1 常量例子

接下来我们开始解读第一个常量例子
在这里插入图片描述
如图红框所示,0x0A => 10,对应表一为CONSTANT_Methodref_info,对照“代码一”,也可得到印证

#1 = Methodref          #6.#20         // java/lang/Object."<init>":()V

结合表二对CONSTANT_Methodref_info的结构描述如下:
在这里插入图片描述
tag后跟随的2个index为0x0006与0x0014,都是索引项,指向常量池元素6,20:

#6 = Class              #27            // java/lang/Object
#20 = NameAndType        #7:#8          // "<init>":()V
3.1.2 其他常量

按照例子中的方式,我们可以找出剩下的32个常量结构,并在下图用彩圈框出。其中蓝色圈框的0x01类型(CONSTANT_Utf8_info),如0x01 00 06,为了查看方便,仅框起tag和长度length部分,随后跟着的是对应长度的字符串。
在这里插入图片描述

4.访问标志、索引等

4.1 access_flags 访问标志

常量池结束跟着的是2个字节的访问标志(access_flags),其代表的是class整体的访问类型,值由下表对应值通过位运算’|'或计算得到。
在这里插入图片描述
如下图,0x0021 = 0x0001 | 0x0020,即 ACC_PUBLIC、 ACC_SUPER:
在这里插入图片描述
对应代码一中:

  flags: ACC_PUBLIC, ACC_SUPER

4.2 this_class 类索引、super_class 父类索引、interfaces 接口索引集合

继续参照图一,接下来的是this_class(类引用)、super_class(父类索引)、interfaces_count(接口索引集合数量)、interfaces(接口索引集合具体内容),它们的类型都是u2。

this_class : 0x0005 对应常量池

#5 = Class              #26            // HelloWorld

super_class:0x0006

#6 = Class              #27            // java/lang/Object

interfaces_count:0x0000 => 代表0个接口,于是interfaces也无需表示了。
在这里插入图片描述

5. 字段表、方法表、属性表集合

5.1 fields 字段表

其表示顺序为:

  1. fields_count,u2类型,字段数量
  2. access_flags,u2类型,访问标志
  3. name_index,u2类型,名称引用
  4. descriptor_index,u2类型,一般为字段对应的数据类型

因本文例子中无字段变量,所以fields_count对应 0x0000,且没有其它的额外信息,如图:
在这里插入图片描述

5.2 methods 方法、attributes 属性相关解析

接下来是对方法的描述,首先是 methods_count,u2类型,代表的是方法数量,0x0002 => 2,即有两个方法,其中一个是构造方法,另一个是main方法。需要注意的是,方法表和属性表可以看作是一体的,它们共同描述了一个方法的内容,即每一个方法后面都会跟随着相关的属性表。随后我们将逐一对这两个方法进行解析。

5.2.1 方法1中的方法表集合

先从代码一中看整个方法1的内容,后续内容可以此作对照:

  public 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 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LHelloWorld;

方法表属性顺序为:

  1. access_flags,u2类型,访问标志
  2. name_index,u2类型,方法名称
  3. descriptor_index,u2类型,方法返回类型
  4. attributes_count,u2类型,属性数量
  5. attributes_name_index,u2类型,属性名

如图逐项解析
access_flags:0x0001 对应 ACC_PUBLIC
name_index:0x0007 对应常量池中

#7 = Utf8               <init>

descriptor_index:0x0008 对应常量池中

#8 = Utf8               ()V

attributes_count:0x0001 => 1 个属性,
attributes_name_index:0x0009 对应常量池中的’Code’,具体内容在下一节属性表中分析。

#9 = Utf8               Code

在这里插入图片描述

5.2.2 方法1中的属性表

虚拟机规范中定义了29项属性,具体见附录1,这些属性会在不同位置出现,对于本文例子,仅包含其中三项: Code属性、LineNumberTable属性、LocalVariableTable属性。另从附录1可知,LineNumberTable属性、LocalVariableTable属性为Code属性的子属性。

5.2.2.1 Code属性:

在这里插入图片描述
其中 attribute_name_index已由前述方法中的0x0009表示了,attribute_lenght(属性总长度): 0x0000002F = 47(字节),包含了后续,Code、LineNumberTable、LocalVariableTable属性内容的总和。

max_stack(操作数栈深度最大值):0x0001 = 1

max_locals(局部变量表所需存储空间):0x0001 = 1

code_length(Java源程序表以后生成字节码指令长度):0x00000005 = 5

code(字节码指令具体内容,见附录2): 0x2AB70001B1
0x2A: aload_0 读入指令,将第0个变量槽中为reference类型的本地变量压入到栈顶
0xB7: invokespecial指令,以栈顶reference类型数据所指向对象作为方法接收者,调用此对象的实例构造器方法
0x0001: invokespecial指令的参数,代表一个符号引用(对应常量池#1元素,实例构造器“()”方法的符号引用)
0xB1: return指令

exception_table_length:0x0000 => 0 ,无异常相关信息,所以exception_table也无存在必要

attributes_count:0x0002 => 2,后续包含2个attribute属性(从属于Code的LineNumberTable,LocalVariableTable),这2个attribute的具体内容将在下一节展开
在这里插入图片描述

5.2.2.2 LineNumberTable属性

在这里插入图片描述
attribute_name_index(属性名称):0x000A => 对应常量池#10,即“LineNumberTable”
attribute_length(属性长度):0x00000006 => 6 (字节)
line_number_table_length:0x0001 = 1
line_number_table包含start_pc(字节码行号),line_number(Java源码行号)两个u2类型数据项
start_pc:0x0000 = 0
line_number:0x0001 = 1
在这里插入图片描述

5.2.2.3 LocalVariableTable属性

在这里插入图片描述
在这里插入图片描述

attribute_name_index(属性名):0x000B => 对应常量池#11,即“LocalVariableTable”
attribute_length(属性总长度):0x00000C => 12字节
local_variable_table_length(栈帧与源码对应表长度):0x0001 = 1
start_pc(局部变量生命周期开始的字节码偏移量):0x0000 = 0
length(局部变量作用范围覆盖长度):0x0005 = 5
name_index(局部变量名称):0x000C => 对应常量池#12,即“this”
descriptor_index(局部变量描述符):0x000D => 对应常量池#13,即“LHelloWorld;”
index(局部变量在栈帧的局部变量表中变量槽的位置,即“Slot”栏位):0x0000 = 0
在这里插入图片描述

5.2.3 方法2中的方法表集合
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;

依照5.2.1中方式得知,
access_flag: 0x0009 => ACC_PUBLIC + ACC_STATIC
name_index: 0x000E => main
descriptor_index: 0x000F => ([Ljava/lang/String;)V
attributes_count: 0x0001 = 1
attribute_name_index: 0x0009 = Code
在这里插入图片描述

5.2.4 方法2中的属性表
5.2.4.1 Code属性:

attribute_length(Code总长度,包含子属性):0x00000037 = 55(字节)
max_stack:0x0002 => 2
max_locals:0x0001 => 1
code_length:0x00000005 => 9字节
code:0xB2 00 02 12 03 B6 00 04 B1
0xB2:getstatic 指令,获取静态域 0x0002(对应常量池#2元素),压入到栈顶
0x12:ldc指令,操作常量池#3元素,压入到栈顶
0xB6:invokevirtual指令,调用实例方法 0x0004(对应常量池#4元素,即println方法)
0xB1:return指令
在这里插入图片描述

5.2.4.2 LineNumberTable属性

attribute_name_index:0x000A => LineNumberTable
attribute_length:0x0000000A => 10 (字节)
line_number_table_length:0x0002 => 2

  1. start_pc:0x0000 => 0
    line_number:0x0003 => 3
  2. start_pc:0x0008 => 8
    line_number:0x0004 => 4
    在这里插入图片描述
5.2.4.3 LocalVariableTable属性

attribute_name_index: 0x000B = LocalVariableTable
attribute_length: 0x0000000C = 12
local_variable_table_length: 0x0001 = 1
start_pc: 0x0000 = 0
length: 0x0005 = 9
name_index: 0x0010 = args
descriptor_index: 0x0011 = [Ljava/lang/String;
index: 0x0000 = 0
在这里插入图片描述

5.3 SourceFile属性

SourceFile属性是隶属于整个Class文件的,用于记录生成这个Class文件的源代码文件名称。
在这里插入图片描述
attributes_count: 0x0001 => 只有1个
attribute_name_index: 0x0012 => 对应常量池#18,即“SourceFile”
attribute_length: 0x00000002 =>该属性内容长度为2
sourcefile_index: 0x0013 => 对应常量池#19,即“HelloWorld.java”
在这里插入图片描述

四、结语

至此,我们已将整个Class文件按字节逐一进行了解析,由此可以帮助我们对于Class文件结构有了更形象的认识,也对JVM字节码有了更深入的理解。Java技术能够一直保持着非常良好的向后兼容性,以及卓越的跨平台能力,Class文件结构的稳定的确功不可没。

五、附录

1. 虚拟机规范预定义的属性表

在这里插入图片描述
在这里插入图片描述

2. 虚拟机字节码指令表

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
作者:侯嘉逊

猜你喜欢

转载自blog.csdn.net/vipshop_fin_dev/article/details/110304052