标签:syn eof variables java虚拟机 dynamic local open 含义 index
作为一名Java后台开发的程序员, 深入理解JVM, 重要性不言而喻, 这篇文章主要是记录JVM类文件结构相关知识.
这部分比较抽象, 所以以实例的形式来学习. 这部分作为资料, 以便后面的章节用来翻阅.
1 | public class { |
1 | cafe babe 0000 0034 0022 0a00 0600 1409 |
javap -verbose Main.class
1 | cafe babe |
Java 虚拟机规范中定义了许多规范, 其中有一部分定义了字节码的结构和规范. Java 虚拟机规范定义了两种数据类型来表示 Class 文件格式, 分别是: 无符号数和表.
无符号数属于最基本的数据类型, 以 u1, u2, u4, u8分别代表 1 个字节, 2 个字节, 4 个字节, 8 个字节的无符号数, 无符号数可以用来描述数字, 索引引用, 数量值或者按照 UTF-8 编码构成的字符串值.
表是由多个无符号数或者其他表作为数据项构成的复合数据类型, 所有表都习惯性地以”_info”结尾. 表用于描述由层次关系的复合结构的数据, 整个Class文件本质上就是一张表.
整个 Class 文件本质上就是一张表, 它由表下表所示的数据项构成.
Class 文件的第 1 - 4 个字节代表了该文件的魔数()Magic Number). 它唯一的作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件, 其值固定是: 0xCAFEBABE(咖啡宝贝). 如果一个 Class 文件的魔数不是 0xCAFEBABE, 那么虚拟机将拒绝运行这个文件.
我们看看实例部分, 其前 4 个字节分别是:cafe babe
Class 文件的第 5 - 6 个字节代表了 Class 文件的次版本号(Minor Version), 即编译该 Class 文件的 JDK 次版本号。
Class 文件的第 7 - 8 个字节代表了 Class 文件的主版本号(Major Version), 即编译该 Class 文件的 JDK 主版本号。
高版本的 JDK 能向下兼容以前的 Class 文件, 但不能运行新版本的 Class 文件.
例如一个 Class 文件是使用 JDK 1.5 编译的, 那么我们可以用 JDK 1.7 虚拟机运行它, 但不能用 JDK 1.4 虚拟机运行它.
下表列出了各个版本 JDK 的十六进制版本号信息:
我们看看实例部分, 其 5 - 8 个字节分别是:0000 0034, 那么我们可以知道, 这个 Class 文件是由 JDK1.8 编译的.
紧接着主次版本号之后是常量池入口, 由于常量池中常量的数量是不固定的, 所以在常量池的入口需要放置一个常量池容量计数值(constant_pool_count), 这个容量计数是从1而不是0开始的, 设计者这样设计的目的是为了满足后面某些指向常量池的索引值的数据在特殊情况下需要表达”不引用任何一个常量池项目”的含义.
Class文件结构中只有常量池的容量计数是从1开始的, 索引集合. 字段集合. 方法集合. 属性集合的容量计数都是从0开始的.
注意, Long和Double型占用两个计数.
常量池中主要存放两大类常量: 字面量(Literal)和符号引用.
字面量接近Java语言层面的常量概念, 如文本字符串. 声明为final的常量值等.
符号引用属于编译原理的概念, 包括三类常量:
常量池中每一项常量都是一个表, 在JDK1.7之后共有14种表结构, 它们有一个共同的特点, 就是表开始的第一位是一个u1类型的标志位(tag, 取值见下表), 代表当前这个常量属于哪种常量类型.
每个常量池的常量都用一个类型为 cp_info 的表表示, 该表有 14 个值, 分别是:
常量池中的14种常量项的结构总表:
我们Class 文件第 9 - 10 个字节为 0022, 表示有 33 个常量.
第 1 个常量. 紧接着 0022 的后一个字节为 0a, 表示该常量为CONSTANT_MethodHandle_info. 从上面的总表查阅知道, 该常量项第 2 - 3 个字节表示方法的类描述符, 这里是 0006 表示指向常量池第 6 个常量所表示的信息. 该常量项的第 4 - 5 个字节表示名称及类描述符, 这里值为 0014 表示指向常量池第 20 个常量所表示的信息.
第 2 个常量. 紧接着 0014 的后一个字节为 09, 表示该常量为CONSTANT_Fieldref_info. 从上面的总表查阅知道, 该常量项第 2 - 3 个字节表示字段的类或者接口描述符, 这里是 0015 表示指向常量池第 21 个常量所表示的信息. 该常量项的第 4 - 5 个字节表示字段描述符, 这里值为 0016 表示指向常量池第 22 个常量所表示的信息.
第 3 个常量. 紧接着 0016 的后一个字节为 08, 表示该常量为CONSTANT_String_info. 从上面的总表查阅知道, 该常量项第 2 - 3 个字节表示指向字符串字面量的索引, 这里是 0017 表示指向常量池的第 23 个常量.
第 4 个常量. 紧接着 0017 的后一个字节为 0a, 表示该常量为CONSTANT_MethodHandle_info的常量. 从上面的总表查阅知道, 该常量项第 2 - 3 个字节表示方法的类描述符, 这里是 0018 表示指向常量池第 24 个常量所表示的信息. 该常量项的第 4 - 5 个字节表示名称及类描述符, 这里值为 0019 表示指向常量池第 25 个常量所表示的信息.
第 5 个常量. 紧接着 0019 的后一个字节为 07, 表示该常量为CONSTANT_Class_info的常量. 从上面的总表查阅知道, 该常量项第 2 - 3 个字节表示全限定名常量项, 这里是 001a 表示指向常量池第 26 个常量所表示的信息.
……
更多可以参考 2.实例 部分中的分析.
在常量池结束之后, 紧接着的两个字节代表访问标记(access_flags), 这个标志用于识别一些类或者接口层次的访问信息, 包括: 这个Class是类还是接口, 是否定义为public类型, 是否定义为abstract类型等. 具体的标志位以及标志的含义见下表.
在实例里面, 这两个字节是 00 21, 通过查看我们并没有发现有标志值是 00 21 的标志名称. 这是因为这里的访问标志可能是由多个标志名称组成的, 所以字节码文件中的标志值其实是多个值进行或运算的结果.
通过查阅上述表格, 我们可以知道, 00 21 由 00 01 和 00 20 进行或运算得来, 也就是说该类的访问标志是 public 并且允许使用 invokespecial 字节码指令的新语义.
类索引和父类索引都是一个u2类型的数据, 而接口索引集合是一组u2类型的数据的集合, Class文件中由这三项数据来确定这个类的继承关系.
类索引. 类索引用于确定这个类的全限定名, 它用一个 u2 类型的数据表示. 这里的类索引是 00 05 表示其指向了常量池中第 5 个常量, 通过我们之前的分析, 我们知道第 5 个常量其最终的信息是 Main 类.
父类索引. 父类索引用于确定这个类的父类的全限定名, 父类索引用一个u2类型的数据表示. 这里的父类索引是 00 06 表示其指向了常量池中第 6 个常量, 通过我们之前的分析, 我们知道第 6 个常量其最终的信息是 Object 类. 因为其并没有继承任何类, 所以 Demo 类的父类就是默认的 Object 类.
接口索引. 接口索引集合就用来描述哪个类实现了哪些接口, 这些被实现的接口将按 implements 语句(如果这个类本身就是一个接口, 则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中. 对于接口索引集合, 入口第一项是 u2 类型的数据为接口计数器(interfaces_count), 表示索引表的容量, 而在接口计数器后则紧跟着所有的接口信息. 如果该类没有实现任何接口, 则该计数器值为0, 后面接口的索引表不再占用任何字节.
这里 Main 类的字节码文件中, 因为并没有实现任何接口, 所以紧跟着父类索引后的两个字节是0x0000, 这表示该类没有实现任何接口. 因此后面的接口索引表为空.
字段表集合用于描述接口或者类中声明的变量. 这里说的字段包括类级变量和实例级变量, 但不包括在方法内部声明的局部变量.
在类接口集合后的2个字节是一个字段计数器, 表示总有有几个属性字段. 在字段计数器后, 才是具体的属性数据. 字段表的每个字段用一个名为 field_info 的表来表示, field_info 表的数据结构如下所示:
字段访问标志:
跟随 字段访问标志 的是两项索引值: name_index和 descriptor_index. 它们都是对常量池的引用, 分别代表字段的简单名称 以及 字段和方法的描述符.
描述符的作用是描述字段的数据类型, 方法的参数列表(包括数量, 类型及顺序)和返回值.
根据描述符的规则, 基本数据类型以及代表无返回值的void类型都用一个大写字符来表示, 而对象类型则用字符L加对象的全限定名表示, 见下表
对于数组类型, 每一维度将使用一个前置的”[“字符来描述. 如”String[][]”, 会被记录为”[[Ljava/lang/String”,”int[]”被记录为”[I”.
描述符描述方法时, 按照先参数列表, 后返回值的顺序描述. 参数列表按照参数的严格顺序放置一组小括号“()”内, 如void inc()的描述符为“()V”,“viod main(String[] args)”的描述符为“([Ljava/lang/String;)V”,“int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)”的描述符为“([CII[CIII)I”.
字段表都包含的固定数据项到descriptor_index为止就结束了, 不过在descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息, 字段都可以在属性表中描述零至多项的额外信息.
字段表集合中不会列出从超类或者父类接口中继承而来的字段, 但有可能列出原本Java代码之中不存在的字段.
因为我们并没有声明任何的类成员变量或类变量, 所以在 Main 的字节码文件中, 字段计数器为 00 00, 表示没有属性字段.
在字段表后的 2 个字节是一个方法计数器, 表示类中总有有几个方法. 在字段计数器后, 才是具体的方法数据, 方法表中的每个方法都用一个 method_info 表示, 其数据结构如下:
方法表所包含的数据项目的含义也和字段表集合的非常的类似, 仅在访问标志和属性表集合的可选项中有所区别. 由于volatile, transient关键字不能修饰方法, 同时synchronized, native, strictfp和abstract关键字可以修饰方法. 对于方法表, 所有标志位及其取值如下
通过访问标志, 名称索引, 描述符索引可清楚的表达方法的定义. 那方法里面的代码去哪里了呢? 方法里的Java代码经过编译器编译成字节码指令后, 存放在方法属性表集合中属性表中; 这个属性表的名称为”Code”. 属性表是Class文件格式中最具扩展性的一种数据项目.
与字段表集合相对应的, 如果父类方法在子类中没有被重写(Override), 方法表集合中就不会出现来自父类的方法信息, 但可能出现编译器自动添加的方法, 最典型的便是类构造器”<clinit>”方法和实例构造器”<init>”方法.
在Java语言中, 重载(Overload)一个方法, 1.要与原方法具有相同的简单名称. 2.要与原方法有不同的特征签名.
Java代码的方法特征签名只包括方法名称, 参数顺序及参数类型; 而字节码的特征签名还包括方法返回值以及受查异常表.
Main 类的字节码文件中, 方法计数器的值为 00 02, 表示一共有 2 个方法.
第 1 个方法. 方法计数器后 2 个字节表示方法访问标识, 这里是 00 01, 表示其实 ACC_PUBLIC 标识, 即该方法访问表示为 public.紧 接着 2 个字节表示方法名称的索引, 这里是 00 07 表示指向了常量池第 7 个常量, 查阅可知其指向了<init>. 紧接着的 2 个字节表示方法描述符索引项, 这里是 00 08 表示指向了常量池第 8 个常量, 查阅可知其指向了()V. 紧接着 2 个字节表示属性表计数器, 这里是 00 01 表示该方法一共有 1 个属性. 紧接着的一连串就是属性表的内容.
在Class文件, 字段表, 方法表, 属性表都可以携带自己的属性表集合, 用于描述某些场景专有的信息.
与Class文件中其他的数据项目要求严格的顺序, 长度和内容不同, 属性表集合的限制稍微宽松了一些, 不再要求各个属性表具有严格顺序, 并且只要不与已有属性名重复, 任何人实现的编译器都可以想属性表中写入自己定义的属性信息, Java虚拟机运行时会忽略掉它不认识的属性. 下边将介绍一些关键常用的属性.
虚拟机规范预定义的属性:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
Signature | 类、方法表、字段表 | JDK1.5中新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | JDK1.6中新增的属性,SourceDebugExtension属性用于存储额外的调试信息。譬如在进行JSP文件调试时,无法通过Java堆栈来定位JSP文件的行号,JSR-45规范为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension属性就可以用于存储这个标准所新加入的调试信息 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | JDK1.5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类、方法表、字段表 | JDK1.5新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性用于注明哪些注解是运行时(实际上运行时就是进行反射调用)可见的 |
RuntimeInvisibleAnnotations | 类、方法表、字段表 | JDK1.5新增的属性,与RuntimeVisibleAnnotations属性作用刚好相反,用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotations | 方法表 | JDK1.5新增的属性,作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法参数 |
RuntimeInvisibleParameterAnnotations | 方法表 | JDK1.5新增的属性,作用与RuntimeInvisibleAnnotations属性类似,只不过作用对象为方法参数 |
AnnotationDefault | 方法表 | JDK1.5新增的属性,用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | JDK1.7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符 |
对于每个属性, 它的名称需要从常量池引用一个CONSTANT_Utf8_info类型的常量来表示, 而属性值的结构则完全自定义的, 只需要通过一个u4的长度属性去说明属性值所占用的位数即可. 一个符合规则的属性表应该满足以下定义结构.
属性表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
attribute_name_index是指向CONSTANT_Utf8_info类型常量的索引, CONSTANT_Utf8_info类型常量记录着属性的名称; attribute_length标识属性值所占用的位数.
这里不做过多扩展了, 每种属性具体的定义参考书就可以了.
<<深入理解Java虚拟机—-JVM高级特性与最佳实践>>(第二版, 周志明)
从 HelloWorld 看 Java 字节码文件结构
Java类文件结构详解
标签:syn eof variables java虚拟机 dynamic local open 含义 index
原文地址:https://www.cnblogs.com/lijianming180/p/12037972.html