jvm(6)-java类文件结构(字节码文件)

时间:2021-02-05 14:03:16
【0】README
0.1)本文部分文字描述转自 “深入理解jvm”,旨在学习类文件结构  的基础知识;
0.2)本文荔枝以及荔枝的分析均为原创;
0.3)下面的截图中有附注t*编号,不关乎博文内容;
0.4)鉴于大家对这篇文章这么热爱,如觉得有必要的话,请移步到 class字节码文件结构总结,该篇文章是对本文的总结;[update timestamp:1603290818]

【1】类文件概述
1)各种不同平台的虚拟机与所有平台都统一使用存储格式——字节码,他是构成平台无关性的基石;
2)时至今日,商业机构和开源机构已经在 java语言外发展出一大批在 jvm 上运行的语言,如 Groovy, JRuby, Jython,Scala等;
3)实现语言无关性的基础: 是虚拟机和字节码存储格式;
4)jvm 不和包括java在内的任何语言绑定,它只与“Class” 文件这种特定的二进制文件格式进行关联,Class 文件中包含了jvm 指令集和符号表以及若干其他辅助信息; (干货——意在说明jvm不是专门针对java 设计的,而是针对 Class 文件格式设计的)

【2】Class类文件结构
Attention)任何一个Class 文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口都不一定定义在文件里(如类或接口也可以通过类加载器直接生成);
1)Class文件介绍: Class文件是一组 以 8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class文件中,中间没有添加任何分隔符,这使得整个Class 文件中存储的内容几乎全部是程序运行的必要数据。 当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储;
2)根据jvm 规范的规定:Class 文件格式采用一种类似于C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型: 无符号和表,后面的解析都要以这两种数据类型为基础; (干货——Class 文件格式采用伪结构存储数据,这种伪结构中只有两种数据类型: 无符号和表)
  • 2.1)无符号类型: 属于基本的数据类型,以u1, u2, u4, u8 分别代表1、2、4个字节和8个字节的无符号数, 无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成字符串值;
  • 2.2)表: 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,如下所示(注意_info 后缀):(t1)
  • jvm(6)-java类文件结构(字节码文件)

【2.1】魔数与Clas文件的版本
1)魔数定义:每个Class文件的头四个字节称为魔数, 它的唯一作用是确定这个文件是否为一个能被虚拟机接收的Class 文件;(Class 文件的魔数值为: 0xCAFEBABY-咖啡宝贝);
2)魔数后面的是Class文件的版本号:第5、6个字节是次版本号,第7、8个字节是主版本号;(java的版本号是从45开始的, 高版本的jdk 能向下兼容以前版本的Class文件, 但不能运行以后版本的Class文件);
Attention) 现在的JDK为1.7,可生成的Class文件主版本号最大值为51.0(仅了解);
3)下表列出了从JDK1.1~1.7,主流JDK 版本编译器输出的默认和可支持的Class文件版本号(t2)
jvm(6)-java类文件结构(字节码文件)
4)看个荔枝:(t4)
jvm(6)-java类文件结构(字节码文件)
【2.2】常量池(Class文件的资源仓库)
1)主版本号之后是常量池入口:常量池可以理解为Class 文件中的资源仓库,它是Class 文件结构中与其他项目关联最多的数据类型, 也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目;
2)常量池中主要有两大类常量: 字面量和 符号引用,字面量比较接近于 java 语言层面的常量概念,如文本字符串,声明为final 的常量值等。
2.0)常量池容量从1开始计数而不是0:在Class文件格式规范制定时,设计者将第0项常量空出来是有考虑的,目的是 满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义;这种情况就可以用索引值=0来表示;
2.1)看个荔枝:(t4)
jvm(6)-java类文件结构(字节码文件)
jvm(6)-java类文件结构(字节码文件)
(t3)
3)符号引用则属于编译原理方面的概念,包括下面三类常量(constant):
  • c1)类和接口的全限定名;
  • c2)字段的名称和描述符;
  • c3)方法的名称和描述符;
4)java代码在进行javac的时候,并没有C或C++的连接步骤:而是在虚拟机加载Class 文件的时候进行动态链接。当虚拟机运行时,需要从常量池获取对应的符号引用,再在类创建时或运行时解析,翻译到具体的内存地址之中。
5)常量池中每一项都是一个表在JDK1.7之前有11种结构各部相同的表结构数据,之后又添加了3种(CONSTANT_MethodHandle_info, CONSTANT_MethodType_info 和 CONSTANT_InvokeDynamic_info);
  • 5.1)这14种表有一个共同特点:表开始 的 第一位是一个 u1 类型的标志位(tag),代表当前常量的常量类型。这14种常量类型所代表的具体含义如下:(t7)
  • jvm(6)-java类文件结构(字节码文件)

  • 5.2)之所以说常量池是最繁琐的数据,是因为这14种常量类型各自都有自己的结构。
  • 5.3)看个荔枝(以 CONSTANT_Class_info为例):CONSTANT_Class_info的标志位是0x07,此类型的常量代表一个类或接口的符号引用,如下表所示:(t8)
  • jvm(6)-java类文件结构(字节码文件)
对上图的分析(Analysis):
  • A1)tag:是标志位,它用于区分常量类型;
  • A2)name_index:是一索引值,它指向常量池中一个 CONSTANT_Utf_info 类型常量,此常量代表了这个类(或者接口)的全限定名,这里的name_index==0x0004,也即是指向了常量池中的第二项常量;
  • A3)还以及继续查找常量池常量,偏移量=0x0D,value=0x01=CONSTANT_Utf8_info 类型的常量。
5.4)CONSTANT_Utf8_info类型结构见下表:(t9)
jvm(6)-java类文件结构(字节码文件)
对上图的分析(Analysis):
    • A1)length:说明了这个UTF8 的字符串长度是多少字节, 他后面紧跟着的长度是length 字节的连续数据是一个使用UTF8 缩略编码表示的字符串;
    • A2)UTF8缩略编码和普通UTF8编码的区别是: 从'\u0001'~'\u007f'之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示,从'\u0080'~'u07ff'(128~2023)之间的所有字符的缩略编码用两个字节表示,从'\u0800'~'\uffff'(2048~65535)之间的所有字符的缩略编码就按照普通UTF8 编码规则使用三个字节表示;(干货——UTF8缩略编码和普通UTF8编码的区别)
    • A3)由于Class文件中方法,字段等需要引用 CONSTANT_Utf8_info 型常量来描述名称:所以 CONSTANT_Utf8_info型常量的最大长度是java中方法,字段名的最大长度;而这里的最大长度就是length的最大值,即u2类型能表达的最大值65535。所以java 程序中如果定义了超过64KB 英文字符的变量或方法名,将会无法编译;
5.5)javap:一个专门用于分析Class 文件字节码的工具,它可以把常量池中的常量都计算出来; (干货——javap 的作用)
E:\bench-cluster\cloud-data-preprocess\jvm\src>javap -verbose com.jvm.chapter6.TestClass
Classfile /E:/bench-cluster/cloud-data-preprocess/jvm/src/com/jvm/chapter6/TestClass.class
Last modified 2016-3-26; size 292 bytes
MD5 checksum 8c182f29b5f2790f2bf1155837ce89cd
Compiled from "TestClass.java"
public class com.jvm.chapter6.TestClass
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 // com/jvm/chapter6/TestClass.m:I
#3 = Class #17 // com/jvm/chapter6/TestClass
#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 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/jvm/chapter6/TestClass
#18 = Utf8 java/lang/Object
{
public com.jvm.chapter6.TestClass();
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 7: 0
}
SourceFile: "TestClass.java"
6)总结14种常量项的结构定义(如下表所示)(t10,11)
jvm(6)-java类文件结构(字节码文件)
jvm(6)-java类文件结构(字节码文件)
【2.3】 访问标志
1)在常量池结束后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口;是否定义为 public;是否定义为abstract类型;如果是类的话,是否被声明为final类型等;具体的标志位如下表所示:
2)访问标志一共有16个,这里只列出了8个,没有使用到的标志位为零;(t12)
jvm(6)-java类文件结构(字节码文件)
Attention)我使用的是jdk8的编译器进行编译,而书作者使用的是jdk7,所以对于访问标志的荔枝,我这里不举了(因为我发现算出来不对);

【2.4】类索引、父类索引与接口索引集合
1)Class 文件中由这三项数据来确定这个类的继承关系:类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组 u2类型的数据的集合;
  • 1.1)类索引:用于确定这个类的全限定名;
  • 1.2)父类索引:用于确定这个类的父类的全限定名;
  • 1.3)接口索引集合: 就是用来描述这个了实现了哪些接口,这些被实现的接口将按 implements 语句(若这个类本身就是接口,则应该是 extends语句) 后的接口顺序从左到右排列在 接口索引集合中;
2)类索引,父类索引和接口索引集合:都按照顺序排列在访问标志之后,类索引和父类索引用两个u2 类型的索引值标识,它们各自指向一个类型为 CONSTANT_Class_info的类描述符常量,通过 CONSTANT_Class_info 类型的常量中的索引值可以找到定义在 CONSTANT_Utf8_info类型的常量中的全限定名字符串;这个过程如下图所示:(t13)
jvm(6)-java类文件结构(字节码文件)
  • 2.1)对于接口索引: 入口的第一项——u2 类型的数据为接口计数器,表示索引表的容量;若该类没有实现任何接口,则其值=0,后面接口的索引表不再占用任何字节;(t14)
  • jvm(6)-java类文件结构(字节码文件)
【2.5】 字段表集合
1)字段表:用于描述接口或类中声明的变量;
2)字段:包括类级变量以及实例级变量,但不包括在方法内的局部变量;
3)在Java中 描述一个字段需要哪些信息?包括的信息有: 字段的作用域(public, private, protected),是实例变量还是类变量(static修饰符),可变性(final修饰符),并发可见性(volatile修饰符,是否强制从主内存读写),是否可被序列化(transient修饰符),字段数据类型(基本类型,对象,数组),字段名称;
  • 3.1)以上这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字, 字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述;下表给出了字段表的最终格式:(t15)
  • jvm(6)-java类文件结构(字节码文件)
  • 3.2)字符修饰符放在 access_flags 项目中,他与类中的access_flags 项目非常类似的,都是一个 u2数据类型,如下表所示:(t16)
  • jvm(6)-java类文件结构(字节码文件)
  • 3.3)跟随access_flag标志的是两项索引值: name_index 和 desc_index;他们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。
4)全限定名+简单名称+描述符
  • 4.1)全限定名: 如"com/jvm/chapter6/TestClass" 是 com.jvm.chapter6.TestClass.java 这个类的全限定名,仅仅是把类中的点替换为 “/” 而已,一般在其后面加上“;”以表全限定名结束;
  • 4.2)简单名称: 是指没有类型和参数类型的方法或者字段名称,这个类中的inc() 方法 和 m 字段的简单名称分别是 “inc” 和 “m”;
  • 4.3)描述符的作用:是用来描述字段的数据类型,方法的参数列表(包括数量,类型以及顺序)和返回值。工具描述符规则,基本数据类型(byte, char, double, flaot, int ,long, short, boolena)以及代表无返回值的void 类型都用一个大写字符来表示, 而对象类型用字符L 加对象的全限定名来表示,如下表所示(t17):
  • jvm(6)-java类文件结构(字节码文件)
  • 4.4)对于数组类型:每一维度将使用一个前置的“[”来描述,如java.lang.String[][] 类型的二维数组,将被记录为:"[[Ljava/lang/String",一个整型数组int[] 将被记录为 "[I";
  • 4.5)用描述符来描述方法时:按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”内;
  • 4.6)看个荔枝(examples):
e1)如方法 void int() 的描述符为 “()V”;
e2)方法java.lang.String toString() 的描述符为 "()Ljava/lang/String";
e3)方法int indexOf(char[]source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述符为 "(CII[CIII)I";

5)字段表集合的荔枝:(t18)
jvm(6)-java类文件结构(字节码文件)
6)字段表都包含的固定数据项目到 desc_index 为止就ending了。不过在desc_index 之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零个至多个的额外信息。对于本例中的字段m,他的属性表计数器为零;

【2.6】方法表集合
1)方法表的结构如同字符表一样:依次包括了 访问标志(access_flags),名称索引(name_index),描述符索引(desc_index),属性表集合(attributes)几项,如下表所示(t19):
jvm(6)-java类文件结构(字节码文件)
2)方法访问标志(t20)
jvm(6)-java类文件结构(字节码文件)
3)方法里面的代码到哪里去了? 方法里面的代码经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目;(t21)
jvm(6)-java类文件结构(字节码文件)
对上图的分析(Analysis):
  • A1)代表方法集合有两个方法:分别是 编译器添加的实例构造器<init>方法和源码中的方法inc()方法;
  • A2)与字段表集合相对应的,如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器<clinit>方法和 实例构造器<init> 方法;
  • A3)在java中,要重载(Override)一个方法,除了要与原方法具有相同的简单名称外,还必须要求拥有与原方法不同的特征签名。特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,所以java 中是无法仅仅依靠返回值的不同来对一个已有方法进行重载的;(干货——为什么java 中是无法仅仅依靠返回值的不同来对一个已有方法进行重载)
【2.7】 属性表集合
1)虚拟机规范预定义的属性(t22,23,24)
jvm(6)-java类文件结构(字节码文件)jvm(6)-java类文件结构(字节码文件)jvm(6)-java类文件结构(字节码文件)
2)对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info 类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可;属性表结构如下图所示(t25):
jvm(6)-java类文件结构(字节码文件)
3)虚拟机规范定义的属性
  • 3.1)Code属性:java程序方法体中的代码经过javac 编译后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合中,但并非所有的方法表都必须存在这个属性,如接口或抽象类中的方法就不存在。Code属性表的结构如下:(t26)
  • jvm(6)-java类文件结构(字节码文件)
  • 对上图的分析(Analysis):
    • A1)attribute_name_index:是一项指向CONSTANT_Utf8_info 型常量的索引,常量固定值为 “Code”,它代表了该属性的属性名称;
    • A2)attribute_length:代表属性长度;
    • A3)max_stack:代表操作数栈深度的最大值;
    • A4)max_locals:代表了局部变量表所需要的存储空间;Slot是虚拟机为局部变量分配内容所使用的最小单位,对于byte, char, flaot, int, short, boolean and returnAddress 等长度不超过32位的数据类型,每个局部变量占用1个Slot;而double 和 long 这两种64位的数据类型则需要 两个Slot来存放;方法参数,显式异常处理器的参数,方法体中定义的局部变量都需要使用局部变量表来存放;还需要注意的是, 并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot是可以重用的,当代码执行超过了一个局部变量的作用域时,这个局部变量所占的Slot 可以被其他局部变量使用,javac编译器会根据变量的作用域来分配Slot给各个变量使用, 然后计算出 max_locals的大小;
    • A5)code_length 和 code:用来存储java 源代码编译后生成的字节码指令。code_length代表字节码长度,code用于存储 字节码指令的一系列字节流;关于code_length,需要注意的是,虽然它是一个u4类型的长度值,但虚拟机中明确限制了一个方法不允许超过65535条字节码指令,即它实际上只使用了u2的长度,如果超过这个限制, javac编译器会拒绝编译;
    • A6)Code属性:是Class文件中最重要的一个属性,如果把一个java程序中的信息分为代码(Code,方法体里面的java代码)和元数据(metadata,包括类,字段,方法定义以及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据;
  • 3.1.1)看个例子(总结以上属性)(t27)
  • jvm(6)-java类文件结构(字节码文件)
  • 3.1.2)再看个荔枝:(t28)
  • jvm(6)-java类文件结构(字节码文件)
对上图的分析(Analysis):
    • A1)大家注意到args_size=1应该会有疑问: 这个类有两个方法——实例构造器<init>和inc()方法,这两个方法都没有参数,那么这个args_size=1是如何而来的 ? 而且无论是在参数列表里还是方法体内,都没有定义任何局部变量,那locals=1 又是如何来的?
    • A2)answer:在任何实例方法里面,都可以通过this 关键字访问到此方法所属的对象。这个访问机制非常重要,而实现却很简单,仅仅是通过javac 编译器编译的时候吧对this 关键字的访问转变为 对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已;因此,在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot 位来存放对象实例的引用,方法参数值从1开始计算。这个处理只对实例方法有效,如果代码清单中的inc()方法为static,那args_size 就不会等于1而是0了(因为static关键字表明它是类变量,this指针无法引用到);
3.1.3)异常表格式如下表所示:(t29)
jvm(6)-java类文件结构(字节码文件)
对上图的分析(Analysis):
    • A1)他包含4个字段,这些字段的定义为: 如果当字节码在第start_pc 行到 第 end_pc 行之间,出现了类型为catch_type或者其子类的异常(catch_type是一个指向一个 CONSTANT_Class_info类型常量的索引),则转到 第 handler_pc 行继续处理。当catch_type=0时,代表任意异常情况都需要转向到 handler_pc 处进行处理;
看个荔枝)(t30)
jvm(6)-java类文件结构(字节码文件)
对上图的分析(Analysis),这里有3条异常表记录:
    • A1)如果try 语句块中出现属于Exception或其子类异常,则转到catch语句进行处理;
    • A2)如果try 语句块中出现不属于Exception或其子类异常,则转到finally语句进行处理;
    • A3)如果catch语句块中出现了任何异常,则转到finally语句块处理;
    • A4)problem:这段代码的返回值是多少?
对以上字节码的分析(Analysis):
    • A1)字节码中0~9行:所做的操作是将整数1赋值给变量m,并将此时x的值复制一份副本到最后一个本地变量表的Slot中(这个Slot里面的值在ireturn指令执行前将会被重新读入到操作栈顶,作为方法返回值使用),为了方便,我们将本地变量表的最后一个Slot命名为returnValue;
    • A2)字节码中10~16行:如果没有出现异常的话,继续10~16行,将变量m 赋值为3,然后将之前保存在returnValue中的整数1 读入到操作栈顶,最后ireturn 指令会以int 形式返回操作栈顶的值,方法结束。
    • A3)字节码中17~行:如果出现了异常,PC寄存器指针转到第17行,第17~34行所做的事情就是将2 赋值给变量m,然后将变量m 此时的值赋给 returnValue,最后再将变量m 的值改为3.方法返回前同样将returnValue中保留的整数2读入到操作栈顶。从第35行开始的代码,作用是变量m的值赋值为3,并将栈顶的异常抛出,方法结束;
    • (注:以上是3.1Code属性的全部内容,有点多,且杂,不要搞混淆了,我们继续 3.2Exception属性的内容)
3.2)Exception属性(见上表6-13)
1)作用:Exception属性的作用是列举出方法中可能抛出的受查异常,也就是方法描述时在 throws 关键字后面列举的异常,如下表所示:(t31)
jvm(6)-java类文件结构(字节码文件)
对上图的分析(Analysis):
A1)number_of_exceptions:表示方法可能抛出 number_of_exceptions 种异常,每一种异常使用一个 exceptoin_index_table 项表示, exceptoin_index_table是一个指向常量池中 CONSTANT_Class_info 型常量的索引,代表了该异常类型;
3.3)LineNumberTable 属性
3.3.1)作用:用于描述java 源码行号与字节码行号的对应关系;他并不是必须的,但默认生成,在javac时,通过 -g:none 或 -g:lines 来取消或要求生成这个信息。如果选择不生成,那么当抛出异常时,堆栈中将不会显示错误的行号,且在调试程序的时候,也无法按照源码行来设置断点;
3.3.2)LineNumberTable属性的结构如下表所示:(t32)
jvm(6)-java类文件结构(字节码文件)
3.3.3)对上图的分析:line_number_table 是一个数量为 line_number_table_length,类型为 line_number_info 的集合,line_number_info 表包括了 start_pc 和 line_number 两个u2 类型的数据项,前者是字节码行号,后者是 java 源码行号;(t33)
jvm(6)-java类文件结构(字节码文件)
3.4)LocalVariableTable 属性(局部参数表属性)
3.4.1)作用:用于描述栈帧中局部变量表中的变量与 java 源码中定义的变量之间的关系,也不是必须的,但默认会生成,可以在javac的时候使用 -g:none 或 -g:vars 来取消或要求生成这项信息;如果没有生成这项消息,最大的影响是当其他人引用这个方法时,所有的参数名称都将会丢失。
3.4.2)其属性结构如下:(t34)
jvm(6)-java类文件结构(字节码文件)
3.4.3)local_variable_info 项目:代表了 一个栈帧与源码中的局部变量之间的关联, 如下表所示:(t35)
jvm(6)-java类文件结构(字节码文件)
3.4.4)对上图的分析(Analysis):
  • A1)start_pc 和 length 属性:分别代表了 这个局部变量的生命周期开始的字节码偏移量及其作用范围长度,两者结合起来就是这个局部变量在字节码中的作用域范围;
  • A2)name_index and desc_index: 都是指向常量池中 CONSTANT_Utf8_info 型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符;
  • A3)index:是这个局部变量在栈帧局部变量表中Slot的位置。当这个变量是 64 位类型时,它占用的Slot 为 index and index + 1;
  • A4)LocalVariableTable属性:它增加了一个姐妹属性——LocalVariableTypeTable,这个新增的属性结构与LocalVariableTable 非常相似,仅仅是把记录的字段描述符的desc_index 替换为字段的特征签名,对于非泛型类型来说, 描述符和特征签名能描述的信息是基本一致的,但是泛型引入后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能正确地描述泛型类型了,所以就引入了LocalVariableTypeTable了;
3.5)SourceFile属性(记录源文件名称)(t36)
jvm(6)-java类文件结构(字节码文件)
1)这个属性是可选的,默认生成,使用 javac的-g:none 或 -g:source 来关闭或生成它;如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名;
2)其结构如下:(t37)
jvm(6)-java类文件结构(字节码文件)
对上图的分析(Analysis):
    • A1)sourcefile_index:是指向常量池中CONSTANT_Utf8_info 型常量的索引,常量值是 源码文件的文件名;(t38)
    • jvm(6)-java类文件结构(字节码文件)
3.6)ConstantValue属性
1)作用:通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量才可以使用这项属性;
2)对于非static类型的变量(实例变量):对于它的赋值是在实例构造器<init>方法中进行的; (干货——实例构造器为<init>方法)
3)对于static类型的变量(类变量):有两种赋值方法:
method1)在类构造器<clinit>方法中; (干货——类构造器为<clinit>方法)
method2)使用 ConstantValue属性;
4)目前 Sun javac 编译器的选择是:如果同时使用final 和 static 关键字来修饰一个变量,且其数据类型是基本类型或 String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非是基本数据类型或String类型,则将会选择在 <clinit> 方法中进行初始化;
5)对于ConstantValue属性的定义:jvm 规范中并没有强制要求字段必须为final,只是要求有ConstantVluae属性的字段必须设置为static而已,对final关键字的要求是 javac编译器自己加入的限制;而且对ConstantValue属性值只能限于基本类型和String,因为Class文件格式的常量类型中只有与基本属性和字符串相对应的字面量,所以ConstantValue属性不可能支持别的类型;(干货——对ConstantValue属性值只能限于基本类型和String);
6)其属性结构如下:(t39)
jvm(6)-java类文件结构(字节码文件)
对上图的分析(Analysis):
A1)ConstantValue属性:是一个定长属性,他的attribute_length数据项 必须为2;
A2)constantvalue_index数据项:代表了常量池中一个字面量常量的引用,根据字段类型的不同 ,字面量可以是 CONSTANT_Long_info, CONSTANT_Float_info,CONSTANT_Double_info,CONSTANT_Integer_info, CONSTANT_String_info,常量中的一种;
3.7)InnerClasses属性
1)作用:用于记录内部类和宿主类间的关联;
2)其属性结构如下所示:(t40)
jvm(6)-java类文件结构(字节码文件)
对上图的分析(Analysis): 数据项 number_of_classes 代表需要记录多少个内部类信息,每个内部类的信息都由一个 inner_classes_info 表进行描述。 其结构如下:(t41)
jvm(6)-java类文件结构(字节码文件)
对上图的分析(Analysis):
    • A1)inner_class_info_index and outer_class_info_index :都是指向常量池中 CONSTANT_Class_info 型常量的索引,分别代表了内部类和宿主类的符号引用;
    • A2)inner_name_index:是指向常量池中 CONSTNAT_Utf8_info 型常量的索引,代表这个内部类的名称,如果是匿名内部类,那么这项值为0;
    • A3)inner_class_access_flags:是内部类的访问标志,他的取值范围如下所示:(t42,43)
    • jvm(6)-java类文件结构(字节码文件)
    • jvm(6)-java类文件结构(字节码文件)


(t43)


3.8)Deprecated和Synthetic属性(属于标志类型的布尔属性)
  • 1)Deprecated属性:用于表示某个类,字段或方法,已经被程序作用定为不在使用;
  • 2)Synthetic属性:代表这个字段或方法并不是由java 源码直接产生的,而是由编译器自行添加的;
  • 3)在jdk1.5后, 标识一个类,字段或方法是编译器自动产生的,也可以设置它们访问标志中的ACC_SYNTHETIC 标志位;
  • 4)所有由非用户代码产生的类,方法以及字段都应该至少设置Synthetic属性和 ACC_SYNTHETIC标志位中的一项,唯一的例外就是 实例构造器<init> 方法和类构造器<clinit> 方法;
  • 5)两个属性的结构如下:(t44)
  • jvm(6)-java类文件结构(字节码文件)
3.9)StackMapTable 属性
  • 1)作用: 它位于Code属性的属性表中, 该属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用, 目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器;
  • 2)StackMapTable属性:包含零个至多个栈映射帧,每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束;(仅供了解啦,这里我也不是很懂,不po 荔枝了);
3.10)Signature属性
  • 1)作用:可以出现在类,属性表和方法表结构的属性表中;在jdk1.5 大幅增强了 java 的语法后, 任何类,接口,初始化方法或成员的泛型 签名如果包含了类型变量或参数化类型,则 Signature 属性会为它记录泛型签名信息。
  • 2)引入它的原因:因为java 的泛型采用的是擦除方法实现的伪泛型,在字节码(Code属性)中, 泛型信息编译之后都通通被擦除掉。擦除后的坏处就是: 运行期无法将泛型类型与用于定义的普通类型同等对待,例如运行期无法做反射时无法获得泛型信息。Signature属性就是为了弥补这个缺陷而增设的,现在 java的反射API能够获得泛型类型,最终的数据来源就是这个属性;
  • 3)Signature属性的结构如下:(t45,46)
  • jvm(6)-java类文件结构(字节码文件)jvm(6)-java类文件结构(字节码文件)
3.11)BootstrapMethods 属性
  • 1)作用:用于保存 invokedynamic 指令引用的引导方法限定符;
  • 2)这个真不懂,省略掉 desc;