Java class文件格式

本文主要通过分析class文件来了解类、方法、成员变量在字节码文件中如何被描述的,以及字节码文件的常量池包含哪些内容。
在分析字节码文件之前,需要清楚认识到Java字节码的格式是严格限定的,所有的class文件的内容都会按照如下格式排列:
在这里插入图片描述

说明一下,如上的u2、u4表示2个字节、4个字节的无符号数,比如magic它的类型是u4,表示占用4个字节。

本文用来验证字节码文件的格式的Java代码如下,使用JDK 1.8编译,二进制文件参考《附录一》:

//包名称
package jvm;

//类
public class Demo  {
    private Integer num  = 1000;
    public static String message="hello world";
    public int inc(){
        return num+1;
    }
}

魔数与Class文件版本

//class文件头8个字节
cafe babe 0000 0034

class文件严格按照顺序排列,每个Class文件的头4个字节称为魔数(Magic Number)它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。如上class文件字节码的前4个字节是cafe babe

紧接着魔数是次版本号(Minor Version)和 主版本号(Major Version),它们分别占用2个字节,版本号的作用是用来标记字节码文件最高能被什么版本的JDK虚拟机运行。例子中的Minor Version、Major Version分别是00000034,转换称10进制后分别是0、52

常量池

//部分常量池内容
0023 0a00 0800 160a
0017 0018 0900 0700 190a 0017 001a 0800

紧接着主版本之后就是常量池,在常量池的入口放置一项u2类型的数据,代表常量池计数值(constant_pool_count)。常量池计数值不是从0开始,而是从1开始,这样做的理由的方便某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池的含义。例子中常量池计数值0023转换成十进制是35

计数值之后就是具体的常量,class文件在描述常量时,都会使用一个·u1类型的tag来存储常量的类型,JVM解析字节码文件时,可以通过tag就可以知道常量的结构。例子中的第一个常量的tag0a,换算成十进制是10,参考《附录三》,10表示一个方法符号引用,然后从《附录二》中找出方法符合引用(Methodref_info)的结构:

条目类型描述
tagu1值为10
indexu2指向声明方法的类描述符的索引项
indexu2指向名称以及类型描述符的索引项

如上是方法符合引用的存储结构,在字节码文件中,就是按照方面的结构来描述一个方法,所以tag之后是占用u2字节的类描述符和名称描述符,在例子中它们的值分别是00 08 00 16,换算成十进制为822,即指向常量池中第8,22。为了方便阅读,借助javap工具输出字节码常量的的内容:javap -v Demo

Constant pool:
   #1 = Methodref          #8.#22         // java/lang/Object."<init>":()V
   #2 = Methodref          #23.#24        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   #3 = Fieldref           #7.#25         // jvm/Demo.num:Ljava/lang/Integer;
   #4 = Methodref          #23.#26        // java/lang/Integer.intValue:()I
   #5 = String             #27            // hello world
   #6 = Fieldref           #7.#28         // jvm/Demo.message:Ljava/lang/String;
   #7 = Class              #29            // jvm/Demo
   #8 = Class              #30            // java/lang/Object
   #9 = Utf8               num
  #10 = Utf8               Ljava/lang/Integer;
  #11 = Utf8               message
  #12 = Utf8               Ljava/lang/String;
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               inc
  #18 = Utf8               ()I
  #19 = Utf8               <clinit>
  #20 = Utf8               SourceFile
  #21 = Utf8               Demo.java
  #22 = NameAndType        #13:#14        // "<init>":()V
  #23 = Class              #31            // java/lang/Integer
  #24 = NameAndType        #32:#33        // valueOf:(I)Ljava/lang/Integer;
  #25 = NameAndType        #9:#10         // num:Ljava/lang/Integer;
  #26 = NameAndType        #34:#18        // intValue:()I
  #27 = Utf8               hello world
  #28 = NameAndType        #11:#12        // message:Ljava/lang/String;
  #29 = Utf8               jvm/Demo
  #30 = Utf8               java/lang/Object
  #31 = Utf8               java/lang/Integer
  #32 = Utf8               valueOf
  #33 = Utf8               (I)Ljava/lang/Integer;
  #34 = Utf8               intValue

从上面的输出可以看出,常量池的第一个常量是#1 = Methodref 指向了常量池中的第822 项,即第一个方法引用表示Objectinit()方法。

我们再看常量池的内容,在常量池中主要存放两大类常量:字面量和符号引用。字面量是utf-8的常量,如27 = Utf8 hello world;符号引用主要是对类、接口、方法、成员变量的描述,如:Methodref 、Fieldref、Class

访问标志

在常量池结束后是访问标志(access flags),它占用2个字节,访问标志用来描述类或者接口的信息,如类的作用域、是否有实现接口,是否是注解等等。访问标记的值没有存储到常量池,因为访问标志的值是已知的多个因素共同作用之后产生的值,JVM可以通过结果反推因素,如下是访问标志的影响因素:
在这里插入图片描述
从前面解析出来的常量池可以得知常量池的最后一项是一个字面量intValue,将intValue转换成16进制后它的值为69 6e74 5661 6c75 65,搜索二进制字节码文件,它的位置如下所示:

0869 6e74 5661 6c75 6500 21

已知Demo是一个publicJava类,用的JDK1.8,所以ACC_PUBLIC、ACC_SUPER为真,即0x0001|0x0020=0X0021,所以紧接着常量池之后的第一个u2值为00 21

类索引、父索引与接口索引集合

00 0700 0800 00

类索引、父类索引和接口索引都按照顺序排列在访问标记之后,类索引和父类索引用分别用u2类型的索引值表示,它们各自指向常量池中的描述符。对于接口,由于java允许多implements,所以用u2类型来记录类实现的接口数量,如果类没有父接口,那么计数器的值为0,并且不会用额外的字节去描述空接口。

类索引

类索引是一个大小为u2,查看字节码文件,紧接着访问标志的两个字节为00 07,指向常量池中的第7项,如下是常量池的内容

//省略了部分值 
#7 = Class              #29            // jvm/Demo
#29 = Utf8              jvm/Demo

如上所示,常量池的第7项是一个Class符号引用,它会指向常量池中的第29项,29项是一个字面量,它的值是jvm/Demo,意思是jvm包下的Demo类。

父索引

父索引与类索引一样,紧接着类索引的两个字节,它的值为00 08,常量池中的第8项如下:

#8 = Class              #30    // java/lang/Object
#30 = Utf8               java/lang/Object

如上所示,常量池的第8项也是一个class符号引用,指向常量池中的第30项,即Demo的父类是Object

接口索引

Demo类没有索引,所以接口索引的数量是0000

字段表集合

00 0200 0200 0900 0a00 0000 0900 0b00 0c00 0000

字段表用于描述类或者接口中的全局变量,一个变量的描述包括:变量的作用域(public、private、protect)、变量的名称、变量类型。如下图是一个字段在字节码中的完整结构:
在这里插入图片描述

  • access_flags访问标志,与类的访问标记的类似,如下是字段访问标志的影响因素:

在这里插入图片描述

  • name_index,变量简单名称索引,指向常量池中的字面量,表示字段的名称。

  • descriptor_index,字段的描述符,这里是指字段类型。类型的描述符的规则是:基本类型用一个大写字符来表示而对象则用字符L加对象的全限定名来表示,数组类型,每一维使用一个[来描述,如:int[]将被记录为[i

在这里插入图片描述

  • attributes_count,属性数量。
  • attributes_info,属性。在Java代码中,变量被final修饰后,都需要进行赋值操作,这个值会被放入常量池中,attributes_info用来指向常量值。

在一个类中,字段会有存在多个的情况,所以会用一个u2来计数。在Demo类中有两个变量,所以计数值为00 02,紧接着字段计数值后是字节码文件中的第一个字段变量,它先用一个u2类型来表示字段的访问标志,例子中第一个变量的访问标志是0002,即该变量的作用范围是private。 然后用一个u2类型即00 09来表示变量的名称索引,指向常量池中的第9项字面量,用来表示字段名称,这里指向了num

 #9 = Utf8     num

字段名称之后是字段的描述符,它的值为00 0a,如下所示,指向一个Ljava/lang/Integer的字面量

 #10 = Utf8               Ljava/lang/Integer;

变量num没有被final修饰所以它的属性数量为00 00,所以前面描述了private Integer num变量。

Demo的另一个字段0009 000b 000c0009表示public static00b指向常量池中的11项:

 #11 = Utf8               message

000c指向常量池中12项,表示字段的类型是String:

#12 = Utf8               Ljava/lang/String;

0000,变量message没有属性集合。

方法表集合

Class文件中对方法的描述与字段的描述是一样的,如下图所示:
在这里插入图片描述

  • access_flag的影响因素如下:

在这里插入图片描述

  • desc_index,方法的描述符与字段变量的描述符不一样,描述一个方法要包括它的返回类型,参数列表、参数类型等。

  • attributes_info,方法的属性用来保存方法体中的执行指令。

接着通过例子来分析class文件如何来描述一个方法:

0300 0100 0d00 0e00 0100 0f00
0000 2b00 0200 0100 0000 0f2a b700 012a
1103 e8b8 0002 b500 03b1

字段集合之后就是方法表集合,因为一个类中有可能存在多个方法,所以在入口需要一个u2来记录方法的的数量,0003代表有3个方法。

第一个方法的access_flags00 01,它的意思是表示方法是public作用域;name_index的值为00 0d,它的十进制的值为13,在常量池中指向字面量#13 = Utf8 <init>方法,表示这个方法的名称是initdesc_index的值为00 0e,换算成十进制14,在常量池中指向#14 = Utf8 ()V表示这个方法的返回类型是void,参数为空,最后整个方法为public void init(),这是编译器添加的实例构造方法。

attributes_count的值为00 01,表示init方法有一个属性,attributes_info的值为00 0f,转成十进制15,指向#15 = Utf8 CodeCode属性是用来存储方法的指令。Code属性的结构如下

在这里插入图片描述

  • attribute_name_index 指向常量池中的属性名称。
  • attribute_length,属性的大小。
  • max_stack,操作数栈的最大深度。
  • max_locals,局部变量表所需要的存储空间。
  • code_lengthJava源程序编译成字节码后的长度。
  • code,指令代码。指令代码都是u1类型的指令,当虚拟机读取字节码时,可以根据字节码找出具体的指令,然后执行指令。指令对照表参照附录。

我们接着来分析案例,attribute_length的值为00 00 2b,转成十进制后的值为43max_stack的值为00 02max_locals的值为00 01code_length的值为00 0000 0f,转换成十进制的值为15,表示后面15个字节都是init方法的指令:

2a b700 012a
1103 e8b8 0002 b500 03b1

2aaload_0,将本地变量表的第0Slot的引用类型推送到操作数栈顶。

b7invokespecial,以操作数栈顶的数据指向的对象作为方法接收者,调用此对象的实例构造方法、priavte方法或者它的父类方法。这个指令的后续会有一个u2类型来指定调用的是哪个方法。

00 01:指向常量池中的1号符号引用。如下所示,将调用Objectinit方法。

#1 = Methodref          #8.#22         // java/lang/Object."<init>":()V
#8 = Class              #30            // java/lang/Object
#13 = Utf8               <init>
#14 = Utf8               ()V
#22 = NameAndType        #13:#14        // "<init>":()V

2aaload_0:将本地变量表的第0Slot的引用类型推送到操作数栈顶。

11sipush,将一个短整数常量值推送到操作数栈顶。

03e8:它的十进制是1000。意思是将1000推送到操作数栈顶。

b8invokestatic:调用静态方法。

00 02:指向常量池中的valueOf方法

 #2 = Methodref          #23.#24        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
 #23 = Class              #31            // java/lang/Integer
 #24 = NameAndType        #32:#33        // valueOf:(I)Ljava/lang/Integer;
 #31 = Utf8               java/lang/Integer
 #32 = Utf8               valueOf
 #33 = Utf8               (I)Ljava/lang/Integer;

b5putfield,为类的实例赋值。

00 03:指向了常量池的第三项,它一个变量num,所以b5 00 03是为变量num赋值。从这里也可以看出,非静态的变量,它是在init方法中进行赋值,而被static修饰的变量没有。

#3 = Fieldref           #7.#25         // jvm/Demo.num:Ljava/lang/Integer;
 #7 = Class              #29            // jvm/Demo
 #9 = Utf8               num
 #10 = Utf8               Ljava/lang/Integer;
 #25 = NameAndType        #9:#10         // num:Ljava/lang/Integer;

b1:返回return

我们可以通过javap -v查看方法的code属性,

public jvm.Demo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: sipush        1000
         8: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        11: putfield      #3                  // Field num:Ljava/lang/Integer;
        14: return
      LineNumberTable:
        line 7: 0
        line 9: 4

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #3                  // Field num:Ljava/lang/Integer;
         4: invokevirtual #4                  // Method java/lang/Integer.intValue:()I
         7: iconst_1
         8: iadd
         9: ireturn
      LineNumberTable:
        line 14: 0

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #5                  // String hello world
         2: putstatic     #6                  // Field message:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 11: 0

如上是Demo 类中的三个方法,static {}是一个类方法clinit,用来对静态变量或者代码块进行初始化。

附录

附录一 代码的二进制文件

cafe babe 0000 0034 0023 0a00 0800 160a
0017 0018 0900 0700 190a 0017 001a 0800
1b09 0007 001c 0700 1d07 001e 0100 036e
756d 0100 134c 6a61 7661 2f6c 616e 672f
496e 7465 6765 723b 0100 076d 6573 7361
6765 0100 124c 6a61 7661 2f6c 616e 672f
5374 7269 6e67 3b01 0006 3c69 6e69 743e
0100 0328 2956 0100 0443 6f64 6501 000f
4c69 6e65 4e75 6d62 6572 5461 626c 6501
0003 696e 6301 0003 2829 4901 0008 3c63
6c69 6e69 743e 0100 0a53 6f75 7263 6546
696c 6501 0009 4465 6d6f 2e6a 6176 610c
000d 000e 0700 1f0c 0020 0021 0c00 0900
0a0c 0022 0012 0100 0b68 656c 6c6f 2077
6f72 6c64 0c00 0b00 0c01 0008 6a76 6d2f
4465 6d6f 0100 106a 6176 612f 6c61 6e67
2f4f 626a 6563 7401 0011 6a61 7661 2f6c
616e 672f 496e 7465 6765 7201 0007 7661
6c75 654f 6601 0016 2849 294c 6a61 7661
2f6c 616e 672f 496e 7465 6765 723b 0100
0869 6e74 5661 6c75 6500 2100 0700 0800
0000 0200 0200 0900 0a00 0000 0900 0b00
0c00 0000 0300 0100 0d00 0e00 0100 0f00
0000 2b00 0200 0100 0000 0f2a b700 012a
1103 e8b8 0002 b500 03b1 0000 0001 0010
0000 000a 0002 0000 0007 0004 0009 0001
0011 0012 0001 000f 0000 0022 0002 0001
0000 000a 2ab4 0003 b600 0404 60ac 0000
0001 0010 0000 0006 0001 0000 000e 0008
0013 000e 0001 000f 0000 001e 0001 0000
0000 0006 1205 b300 06b1 0000 0001 0010
0000 0006 0001 0000 000b 0001 0014 0000
0002 0015 

附录二 常量池常量类型

如下是常量池常量的具体结构:

在这里插入图片描述

附录三 常量tag:

如下是常量池中的各个常量类型的tag表示的意思:

在这里插入图片描述

附录四 虚拟机规范预定义的属性

在这里插入图片描述

附录五 虚拟机字节码指令对照表

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值