Java Class文件详解

9 篇文章 0 订阅

Class类文件的结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列才Class文件之中,中间没有添加任何分隔符。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分隔成若干个8位字节进行存储。

根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两个数据类型:无符号数和表。

无符号数:属于基本的数据类型,以u1丶u2丶u4丶u8分别代表1个字节丶2个字节丶4个字节丶8个字节的无符号数,无符号数可以用来描述数字丶索引引用丶数量值或者按照UTF-8编码构成字符串值。

:由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质就是一张表。

Class文件的数据项构成

类型名称数量
u4magic1
u2minor_version1
u2major_version1
u2constant_pool_count1
cp_infoconstant_poolconstant_pool_count-1
u2access_flags1
u2this_class1
u2super_class1
u2interfaces_count1
u2interfacesinterfaces_count
u2fields_count1
field_infofieldsfields_count
u2methods_count1
methods_infomethodsmethods_count
u2attributes_count1
attributes_infoattributesattributes_count

案例代码

package org.petsch.clazz;

public class Test {

    private int m;

    public int inc(){
        return m + 1;
    }
}

使用十六进制打开Class文件

cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 5465 7374 2e6a 6176 610c
0007 0008 0c00 0500 0601 0015 6f72 672f
7065 7473 6368 2f63 6c61 7a7a 2f54 6573
7401 0010 6a61 7661 2f6c 616e 672f 4f62
6a65 6374 0021 0003 0004 0000 0001 0002
0005 0006 0000 0002 0001 0007 0008 0001
0009 0000 001d 0001 0001 0000 0005 2ab7
0001 b100 0000 0100 0a00 0000 0600 0100
0000 0300 0100 0b00 0c00 0100 0900 0000
1f00 0200 0100 0000 072a b400 0204 60ac
0000 0001 000a 0000 0006 0001 0000 0008
0001 000d 0000 0002 000e 

魔数

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。(值为:0xCAFEBABE 咖啡宝贝)

cafe babe 0000 0034 0013 0a00 0400 0f09

版本号

紧接着魔数后面的是Class文件的版本号,第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。

cafe babe 0000 0034 0013 0a00 0400 0f09
0x0034转十进制为52,代表当前使用的JDK 1.8版本。

注:Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容,但是不能运行以后版本的Class文件。

常量池

紧接着是常量池入口。常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项之一,同时它还是在Class文件中第一个出现的表类型数据项目。

  • 常量池容量计数值(constant_pool_count):由于常量池中常量的数量是不固定的,所以需要在常量池的入口放置一项u2类型的数据,代表着常量池容量计数值。这个容量计数是从1开始的而不是0。

cafe babe 0000 0034 0013 0a00 0400 0f09
0x0013转十进制为19,代表常量池中有18项常量,索引值范围为1~18。

:在Class文件格式规范制定的时候,设计者将第0项常量空出来是有特殊考虑的,这样做是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”,这种情况就可以把索引值设置为0来表示。

常量池中主要存放两大类常量:

  • 字面量(Literal):字面量比较接近于Java语言层面的常量概念,如:

    • 文本字符串。
    • 声明为final的常量值等。
  • 符号引用(Symbolic Reference):符号引用则属于编译原理方面的概念,包括下面三类常量:

    • 类和接口的全限定名(Fully Qualified Name)。

    全限定名:以案例代码为例,“org/petsch/clazz/Test”就是这个类的全限定名,把类全名中的“.”替换成“/”。

    • 字段的名称和描述符(Descriptor)。
    • 方法的名称和描述符。

常量池中每一项常量都是一个表,在JDK 1.7共有14种结构各不相同的表结构数据。这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量属于那种常量类型。

常量池中的常量项结构
常量池
接下来,我们拿第一项常量的标志位进行分析。

cafe babe 0000 0034 0013 0a00 0400 0f09
0x0000000a转十进制为10,表示标志位为10,这个常量属于CONSTANT_Methodref_info类型,此类型的常量代表指向声明方法描述符CONSTANT_Class_info的索引项。CONSTANT_Class_info该类型常量一共占用了5u,即0a 00 0400 0f,所以下一项的标志位为09,这次类推其余的常量可以通过类似的方式计算出来。

接下来使用分析Class文件字节码的工具:javap,来验证下:

public class org.petsch.clazz.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // org/petsch/clazz/Test.m:I
   #3 = Class              #17            // org/petsch/clazz/Test
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               Test.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // m:I
  #17 = Utf8               org/petsch/clazz/Test
  #18 = Utf8               java/lang/Object
{
  public org.petsch.clazz.Test();
    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 int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 8: 0
}
SourceFile: "Test.java"

可以看出,上面的打印的常量池有18项,并且第一项和计算的结果一致。

访问标志

常量池后面紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,如:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。

标志名称标志值含义
ACC_PUBLIC0x0001是否为public类型
ACC_FINAL0x0010是否被声明为final,只有类可设置
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语意
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类为假
ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生的
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举
ACC_MODULE0x4000标识这是一个模块

看看我们的案例代码,可以得出ACC_PUBLIC丶ACC_SUPER标志为真,其他的标志为假,既access_flags的值应该为:0x0001|0x0020 = 0x0021。(如果有多个,就使用按位或运算符 |


6a65 6374 0021 0003 0004 0000 0001 0002

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

访问标志之后,分别是类索引丶父类索引和接口索引集合,在Class文件中由这三项数据来确定这个类的继承关系。

  • 类索引丶父类索引:类索引和父类索引用两个u2类型的索引值表示,类索引用于确定这个类的全限定名。父类索引用于确定这个类的父类的全限定名。它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过该常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

类索引

6a65 6374 0021 0003 0004 0000 0001 0002

0x0003转为十进制为3,即是指向的是常量池中的第3个常量。
Constant pool:
#3 = Class #17 // org/petsch/clazz/Test
#17 = Utf8 org/petsch/clazz/Test

父类索引

6a65 6374 0021 0003 0004 0000 0001 0002

0x0004转为十进制为4,即是指向的是常量池中的第4个常量。
Constant pool:
#4 = Class #18 // java/lang/Object
#18 = Utf8 java/lang/Object

  • 接口索引集合:是一组u2类型的数据集合,用于描述这个类实现了哪些接口
    • 接口计数器(interfaces_count):是接口索引集合入口的第一项,是u2类型的数据,表示索引表的容量。如果该类没有实现任何接口,则计数器值为0,后面接口的索引表不再占用任何字节。


    6a65 6374 0021 0003 0004 0000 0001 0002

    十进制为0,代表我们的案例代码中的类没有实现任何接口。

字段表集合:

紧接着的是字段表,字段表(field_info)用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

字段表结构

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attributes_infdoattributesattributes_count

name_indexdescriptor_index它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。
简单名称:是指没有类型和参数修饰的方法或者字段名称。
描述符:用来描述字段的数据类型丶方法的参数列表和返回值。

描述符标志字符含义

标志字符含义
B基本类型byte
C基本类型char
D基本类型double
F基本类型float
I基本类型int
J基本类型Long
S基本类型short
Z基本类型boolean
V特殊类型void
L对象类型,如Ljava/lang/Object

注:对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为“[[Ljava/lang/String;”。
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述。如方法void inc()的描述符为“()V”,int inc()的描述符为(I)I。

字段访问标志

标志名称标志值含义
ACC_PUBLIC0x0001字段是否为public
ACC_PRIVATE0x0002字段是否为private
ACC_PROTECTED0x0004字段是否为protected
ACC_STATIC0x0008字段是否为static
ACC_FINAL0x0010字段是否为final
ACC_VOLATILE0x0040字段是否为volatile
ACC_TRANSTENT0x0080字段是否为transient
ACC_SYNCHETIC0x1000字段是否为由编译器自动产生
ACC_ENUM0x4000字段是否为enum


6a65 6374 0021 0003 0004 0000 0001 0002

首先是fields_count,类型是u2。即如上,十进制则是1,表示只有一个字段表数据。


6a65 6374 0021 0003 0004 0000 0001 0002

接下来紧接着的是acess_flags标志,值为0x0002,从上面的字段访问标志表可以得出是private修饰符。


0005 0006 0000 0002 0001 0007 0008 0001

接下来是name_index,0x0005的十进制为5,表示常量池的第5项常量,其值为“m”
Constant pool:
#5 = Utf8 m


0005 0006 0000 0002 0001 0007 0008 0001

接下来是descriptor_index,指向常量池的第六项常量,其值为“I”
Constant pool:
#6 = Utf8 I

从上面的推断,可以得出原代码的定义的字段为“private int m;”

注:由于案例代码中的m字段,它的属性表计数器为0,也就是没有需要额外描述的信息,所以字段表的推断就结束了。下面介绍属性表的进一步推断。
别外,在Java语言中字段是无法重载的,两个字段的数据类型丶修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来说,如果两个字段的描述符不一致,那字段重名就是合法的。

方法表集合

紧接后面的是方法表,方法表结构如同字段一样,依次包括了访问标志丶名称索引丶描述符索引丶属性表集合。

方法表结构

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attributes_infoattributesattributes_count

方法访问标志

标志名称标志值含义
ACC_PUBLIC0x0001方法是否为public
ACC_PRIVATE0x0002方法是否为private
ACC_PROTECTED0x0004方法是否为protected
ACC_STATIC0x0008方法是否为static
ACC_FINAL0x0010方法是否为final
ACC_SYNCHRONIZED0x0020方法是否为synchronized
ACC_BRIDGE0x0040方法是否由编译器生产的桥接方法
ACC_VARARGS0x0080方法是否接受不定参数
ACC_NATIVE0x0100方法是否为native
ACC_ABSTRACT0x0400方法是否为abstract
ACC_STRICT0x0800方法是否为strictfp
ACC_SYNTHETIC0x1000方法是否由编译器自动生成

注:方法里面的代码,存放在方法属性表集会中一个名为“Code”的属性里面。具体请看下面的属性表


0005 0006 0000 0002 0001 0007 0008 0001

值为0x0002,代表集会中有两个方法,一个是编译器添加的实例构造器<init>和源码中的inc()方法。


0005 0006 0000 0002 0001 0007 0008 0001

第一个方法的访问标志为0x0001,也就是只有ACC_PUBLIC标志为真。


0005 0006 0000 0002 0001 0007 0008 0001

名称索引值为0x0007,也就是指向常量池的第7项常量。方法名为“<init>”
Constant pool:
#7 = Utf8 <init>


0005 0006 0000 0002 0001 0007 0008 0001

描述符索引值为0x0008,也就是指向常量池的第8项常量。对应的常量为“()V”
Constant pool:
#8 = Utf8 ()V


0005 0006 0000 0002 0001 0007 0008 0001

属性表计数器attributes_count的值为为0x0001,表示此方法的属性表集合有一项属性。


0009 0000 001d 0001 0001 0000 0005 2ab7

属性名称索引为0x0009,对应常量是“Code”,说明此属性是方法的字节码描述。
Constant pool:
#9 = Utf8 Code

:与字段表集合相对应的,如果父类方法在子类中灭有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,如类构造器“<clinit>”方法和实例构造器“<init>”方法。别外,在Class文件格式中,只要描述符不是完全一致的两个方法是可以共存的。

属性表集合

紧接后面的是属性表。在Class文件丶字段表和方法表都都有可以携带自己的属性表集合,以用于描述某些场景专有的信息。

:在Class文件中属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复。虚拟机运行时会忽略掉不认识的属性。

虚拟机规范预定义的属性
在这里插入图片描述

对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性来说明属性值所占用的位数即可。

属性表结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1infoattribute_length

Code属性

Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性中。Code属性出现在方法表的属性集合中,但并未所有方法表都有这个属性,如接口或者抽象类中就不存在Code属性。

Code属性表的结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2max_stack1
u2max_locals1
u4code_length1
u1codecode_length
u2exception_table_length1
exception_infoexception_tableexception_table_length
u2attributes_count1
attributes_infoattributesattributes_count
  • attribute_name_index:是一项指向CONSTANT_Utf8_info类型常量的索引,常量值固定为“Code”,代表该属性的属性名称。
  • attribute_length:代表了属性值的长度,由于属性名称索引类型是u2,属性长度类型是u4,总共占用了u6,所以属性值的长度固定为整个属性表长度减去6个字节。
  • max_stack:代表了操作数栈(Operand Stacks)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。
  • max_locals:代表了局部变量表所需的存储空间。单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte丶char丶float丶int丶short丶boolean等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double丶long这种64位的数据类型则需要两个Slot。方法参数丶显式异常处理器的参数丶方法体中定义的局部变量都需要使用局部变量表来存放。另外,并不是把方法中所用的局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot是可以重用的,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,Java编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。
  • code_length:代表字节码长度。另外,code_length是个u4类型的长度值,理论上最大值可以达到2^32-1,但是虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令,即它实际只使用了u2的长度,如果超过这个限制,Javac编译器会拒绝编译。
  • code:是用于存储字节码指令的一系列字节流。每个字节码指令就是一个u1类型的单字码,当虚拟机读取到code中一个字节码时,就可以找出对应指令。u1类型的取值范围是0~255,也就是说一个可以表达256条指令。

attribute_name_index前面已经分析过了,对应的是“Code”。接下来是attribute_length

0009 0000 001d 0001 0001 0000 0005 2ab7

十进制为29,即长度为29,往后的29个字节都是“Code”属性。

0009 0000 001d 0001 0001 0000 0005 2ab7
0001 b100 0000 0100 0a00 0000 0600 0100
0000 03
00 0100 0b00 0c00 0100 0900 0000

接下来是max_stack

0009 0000 001d 0001 0001 0000 0005 2ab7

代表着操作数栈深度的最大值为0x0001。

max_locals

0009 0000 001d 0001 0001 0000 0005 2ab7

局部变量表的容量为0x0001。

code_length

0009 0000 001d 0001 0001 0000 0005 2ab7

字节码长度为0x0005。

接下来到code了,上面分析到字节码的长度为5

0009 0000 001d 0001 0001 0000 0005 2ab7
0001 b1
00 0000 0100 0a00 0000 0600 0100

我们要根据字节码指令表翻译出“2ab7 0001 b1”。
2A:查表的出0x2A对应的指令是aload_0,含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
B7:0xB7对应的指令为invokespecial,作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法丶private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪个方法,它指向常量池中的CONSTANT_Methodref_info类型常量,即此方法的方法符号引用。
00 0A:这是invokespecial的参数,查常量池得0x000A对应的常量为实例构造器“<init>”方法的符号引用。
B1:对应的指令为return,含义是返回此方法,并且返回值为void。

javap计算字节码指令

{
  public org.petsch.clazz.Test();
    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 int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 8: 0
}

注:使用javap中输出的“args_size”的值,为什么是1呢,<init>()和inc()方法很明显都是没有参数的。还有“locals”的值也是1呢?那是因为,在任何实例方法里面,都可以通过“this”关键字访问到此方法所属的对象。它的实现原理是,通过Javac编译器的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。这个仅对实例方法有效,如果方法声明为static,那args_size的值就是0。

字节码指令之后就是这个方法的显式异常处理表集合,异常表对于Code属性来说并不是必须存在的。

异常表的结构

类型名称数量
u2start_pc1
u2end_pc1
u2handler_pc1
u2catch_type1

这些字段的含义为如果当字节码在第start_pc行到第end_pc行之间(不含第end_pc行)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info类型常量的索引),则转到handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转向到handler_pc处进行处理。

案例代码2

public int inc(){
        int x;
        try {
            x = 1;
            return x;
        }catch (Exception e){
            x = 2;
            return x;
        }finally {
            x = 3;
        }
    }
 
 // 字节码以及异常表
  public int inc();
    Code:
      stack=1, locals=5, args_size=1
         0: iconst_1 	// try块中x=1入栈
         1: istore_1	// 将x=1存到本地变量表索引为1的位置
         2: iload_1		// 将存在本地变量表中索引为1的数值推至栈顶
         3: istore_2	// 将栈顶元素存到本地变量表索引为2的位置,即保存try中的返回值
         4: iconst_3	// final块中的x=3入栈
         5: istore_1	// 将x=3放入到本地变量表索引为1的位置
         6: iload_2		// 将存放在本地变量表中索引为2的数值推至栈顶
         7: ireturn		// 结束方法返回int,即返回x=1
         8: astore_2	// 将栈顶引用类型数值存到本地变量表索引为2的位置
         9: iconst_2	// catch中的x=2入栈
        10: istore_1	// 将x=2存到本地变量表索引为1的位置
        11: iload_1		// 将存在本地变量表中索引为1的数值推至栈顶
        12: istore_3	// 将栈顶元素存到本地变量表索引为3的位置,即保存catch中的返回值
        13: iconst_3	// final块中的x=3入栈
        14: istore_1	// 将x=3放入到本地变量表索引为1的位置
        15: iload_3		// 将存在本地变量表中索引为3的数值推到栈顶
        16: ireturn		// 当前方法返回int,即x=2
        17: astore        4  // 将栈顶引用类型数值存到本地变量表索引为4的位置
        19: iconst_3	// final块中的x=3入栈
        20: istore_1	// 将x=3放入到本地变量表索引为1的位置
        21: aload         4	// 将存在本地变量表中索引为4的数值推至栈顶
        23: athrow		// 将栈顶的数值作为异常或错误抛出
      Exception table:
         from    to  target type
             0     4     8   Class java/lang/Exception
             0     4    17   any
             8    13    17   any
            17    19    17   any

编译器为这段代码生成了4条异常表记录,对应4条可能出现的代码执行路径。(忽略第4条,这条奇怪的记录,为什么为产生有兴趣的人,可以去深究下)

  • 如果try语句块中出现属于Exception或者其子类的异常,则转到catch语句块处理。
  • 如果try语句块中出现不属于Exception或者其子类的异常,则转到finally语句块处理。
  • 如果catch语句块中出现任何异常,则转到finally语句块处理。

分析下字节码层面的执行过程

字节码中第0 ~ 3行所做的操作就是将整数1赋值给变量x,并且将此时x的值复制一份副本到本地变量表中,如果此时没有出现异常,则会继续执行到4 ~ 5行,将变量x赋值为3,然后将之前保存在本地变量表中的整数1读入到操作栈顶,最后ireturn指令会以int形式返回操作栈顶的数据,方法结束。如果出现了异常,PC寄存器指针将转到第8行,第8 ~ 16行所做的事情就是将2赋值给变量x,然后将变量x的值保存到本地变量表中,再将3赋值给变量x,最后将保存在本地变量表中的整数2读入到操作栈顶中。如果出现异常,且不属于Exception,则会转到第17行,执行下去,最后将栈顶的异常抛出,方法结束。

Exception属性

Exception属性是在方法表中与Code属性平级的一项属性,不要与前面的异常表混淆。其作用是列举出方法中可能抛出的受查异常(Checked Exception),也就是方法描述时在throws关键字后面列举的异常。

Exception属性结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2number_of_exceptions1
u2exceptions_index_tablenumber_of_exceptions
  • number_of_exceptions:表示方法可能抛出number_of_exceptions种受查异常,每一种受查异常使用一个exceptions_index_table项表示。
  • exceptions_index_table:是一个指向常量池中CONSTANT_Class_info类型常量的索引,代表了该受查异常的类型。

LineNumberTable属性

LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,可以使用-g:none-g:lines选项来取消生成这项信息。

注:如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,无法按照源码行来设置断点。

LineNumberTable属性结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2line_number_table_length1
line_number_infoline_number_tableline_number_table_length
  • line_number_table:是一个数量为line_number_table_length丶类型为line_number_info的集会,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。

LocalVariableTable属性

LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义之间的关系,它也不是运行时必需的属性,可以使用-g:none-g:vars选项来取消生成这项信息。

注:如果没有生成这项属性,最大的影响就是其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0丶arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但会对代码编写带来不便,而且在调试期间无法根据参数名称从上下文中获取参数。

LocalVariableTable属性结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u4local_variable_table_length1
local_variable_infolocal_variable_tablelocal_variable_table_length
  • local_variable_info:代表一个栈帧与源码中的局部变量的关联、

local_variable_info项目结构

类型名称数量
u2start_pc1
u2length1
u2name_index1
u2descriptor_index1
u2index1
  • start_pc丶length:分别代表了这个局部变量的生命周期开始的字节码偏移量以其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。
  • name_index丶descriptor_index:都是指向常量池中CONSTANT_Utf8_info类型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符。
  • index:是这个局部变量在栈帧局部变量表中Slot的位置。

注:在JDK 1.5引入泛型之后,增加了一个属性:LocalVariableTypeTable,这个属性结构与LocalVariableTable非常相似,仅仅是把记录的字段描述符的descriptor_index替换成字段的特征签名(Signature)。

SourceFile属性

SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可以选的,可以使用-g:none-g:source选项来取消生成这项信息。对于大多数的类来说,类名和文件名是一致的,但是有些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。

SourceFile属性结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2sourcefile_index1
  • sourcefile_index:是指向常量池中CONSTANT_Utf8_info类型常量的索引,常量值是源码文件的文件名。

ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量才可以使用这项属性。

注:虚拟机对于static类型的变量和非static类型变量的赋值方式是不同的。对于非static类型的变量的赋值是在实例构造器<init>方法中进行的,而对于类变量,则有两种方法可以选择:在类构造器<clinit>方法中或者使用ConstantValue属性。如果同时使用final和static来修饰一个变量,并且这个变量的数据类型是基于本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化。

ConstantValue属性结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2constantvalue_index1
  • constantvalue_index:代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是CONSTANT_Long_info丶CONSTANT_Float_info丶CONSTANT_Double_info丶CONSTANT_Integer_info丶CONSTANT_String_info常量中的一种。

InnerClasses属性

InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。

InnerClasses属性结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2number_of_classes1
inner_classes_infoinner_classesnumber_of_classes
  • number_of_classes:代表需要记录多少个内部类信息,每一个内部类信息都由一个inner_classes_info表进行描述。

inner_classes_info表结构

类型名称数量
u2inner_class_info_index1
u2outer_class_info_index1
u2inner_name_index1
u2inner_class_access_flags1
  • inner_class_info_index丶outer_class_info_index:都是指向常量池中CONSTANT_Class_info类型的索引,分别代表了内部类和宿主类的符号引用。
  • inner_name_index:指向常量池中CONSTANT_Utf8_info类型常量的索引,代表这个内部类的名称,如果是匿名内部类,那么这项值为0.
  • inner_class_access_flags:是内部类的访问标志。

inner_class_access_flags标志

标志名称标志值含义
ACC_PUBLIC0x0001内部类是否为public
ACC_PRIVATE0x0002内部类是否为private
ACC_PROTECTED0x0004内部类是否为protected
ACC_STATIC0x0008内部类是否为static
ACC_FINAL0x0010内部类是否为final
ACC_INTERFACE0x0020内部类是否为接口
ACC_ABSTRACT0x0400内部类是否为abstract
ACC_SYNTHETIC0x1000内部类是否为并非由用户代码产生的
ACC_ANNOTATION0x2000内部类是否为注解
ACC_ENUM0x4000内部类是否为枚举

Deprecated及Synthetic属性

Deprecated及Synthetic两个属性都属于标志类型的布尔属性,Deprecated属性用于表示某个类丶字段或者方法,已经被定为不再推荐使用。Synthetic属性代表此字段或者方法并不是有Java源码直接产生的,而是由编译器自行添加的。

注:在JDK 1.5之后,标识一个类丶字段或者方法是编译器自动产生的,也可以设置他们访问标志中的ACC_SYNTHETIC标志位,所有由非用户代码产生的类丶方法以字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器<init>方法和类构造器<clinit>方法。

Deprecated及Synthetic属性结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
  • attribute_length:这项的值必须为0x00000000,因为没有任何属性值需要设置。

StackMapTable属性

StackMapTable是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在与代替以前比较消耗性能的基于数据流分析的类型推导验证器。

注:StackMapTable属性中包含零至多个栈映射帧(Stack Map Frames),每个栈映射帧都显式或者隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。

StackMapTable属性结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2number_of_entries1
stack_map_framestack_map_frame_entriesnumber_of_entries

注:在虚拟机版本号大于或等于50.0的Class文件中,如果方法的Code属性中没有附带StackMapTable属性,那就意味着它带有一个隐式的StackMapTable属性。这个StackMapTable属性的作用等同于number_of_entries值为0的StackMapTable属性属性。一个方法的Code属性最多只能有一个StackMapTable属性,否则将抛出ClassFormatError异常。

Signature属性

StackMapTable属性是一个可选的定长属性,可以出现于类丶属性表和方法表结构的属性表中。任何类丶接口丶初始化方法或成员的泛型签名,如果包含了类型变量或者参数化类型,则Signature属性会为它记录泛型签名信息。之所以要专门使用一个属性去记录泛型类型,是因为Java语言的泛型采用的是擦除法实现的伪泛型。

注:在字节码中,泛型信息编译之后都通通被擦除掉。使用擦除法的好处是实现简单丶非常容易实现Backport,运行期也能够节省一些类型所占的内存空间。坏处是运行期无法像真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期反射时无法获取到泛型类型信息。Signature属性就是为了弥补这个缺陷而增设的,现在Java的反射API能够获取泛型类型,最终的数据来源也就是这个属性。

Signature属性结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2signature_index1
  • signature_index:值必须是一个对常量池的有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示类签名丶方法类型签名或者字段类型签名。如果当前的Signature属性是类文件的属性,则这个结构表示类签名,如果是方法表的属性,则表示方法类型签名,如果是字段表的属性,则表示字段类型签名。

BootstrapMethods属性

BootstrapMethods属性是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符。

注:如果某个类文件结构的常量池中曾经出现过CONSTANT_InvokeDynamic_info类型的常量,那么这个类文件的属性表中必须存在一个明确的BootstrapMethods属性,类文件的属性表中最多只能有一个BootstrapMethods属性。

BootstrapMethods属性

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2num_bootstrap_methods1
bootstrap_methodbootstrap_methodsnum_bootstrap_methods
  • num_bootstrap_methods:表示bootstrap_methods[]数组中的引导方法限定符的数量。
  • bootstrap_methods[]:数组中每个成员包含了一个指向常量池CONSTANT_MethodHandle结构的索引值,代表了一个引导方法,还包含了这个引导方法静态参数的序列。

bootstrap_method属性结构

类型名称数量
u2bootstrap_method_ref1
u2num_bootstrap_arguments1
u2bootstrap_argumentsnum_bootstrap_arguments

bootstrap_methodsp[]数组中的每个成员必须包含以下3项内容:

  • bootstrap_method_ref:值必须是一个对常量池的有效索引。常量池在该索引处的值必须是一个CONSTANT_MethodHandle_info结构。
  • num_bootstrap_arguments:标识bootstrap_arguments[]数组成员数量。
  • bootstrap_arguments[]:数组中每个成员必须是一个对常量池的有效索引。常量池在该索引处必须是下列结构之一:CONSTANT_String_info丶CONSTANT_String_info丶CONSTANT_Class_info丶CONSTANT_Integer_info丶CONSTANT_Long_info丶CONSTANT_Float_info丶CONSTANT_Double_info丶CONSTANT_MethodHandle_info丶CONSTANT_MethodType_info。

参考文献:《Java虚拟机规范》丶《深入理解Java虚拟机》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值