类文件结构
本节主要介绍:
- class文件结构
- jvm指令集
无关性的基石
如何理解JAVA的平台无关性?
JVM处理的实际上是class文件,只要实现在不同机器上的JVM,使得具有处理class文件的能力,就具有了平台无关性
java和class是分离的,java只是可以编译成class的一种语言,于此类似的还有Jython,JRuby,Groovy等语言。
Class文件的结构
class文件的结构?
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据。
根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表:
- 无符号数属于基本数据类型,以u1、u2、u4、u8来分别代表1、2、4、8个字节的无符号数。
- 表是由多个无符号数或其他表作为数据项构成的符合数据类型,所有的表都习惯性地以“_info”结尾。
整个 Class 文件就是一张表,它由下表中所示的数据项构成:
下面按照这个表的顺序一一说明
魔数和版本
什么是魔数?
Class文件的头4个字节,唯一作用是确定文件是否为一个可被虚拟机接受的Class文件,固定为“0xCAFEBABE”。
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6两个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。
class文件和类、接口的关系?
class文件一定对应着一个类或者接口
但是生成class文件的方式很多,甚至可以不作为一个文件存在
class文件版本兼容的问题?
高版本的JDK能够向下兼容低版本的Class文件,虚拟机会拒绝执行超过其版本号的Class文件。
常量池
什么是常量池?
主版本号之后是常量池入口,常量池可以理解为 Class 文件之中的资源仓库,它是 Class 文件结构中与其他项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项目之一,同是它还是 Class 文件中第一个出现的表类型数据项目。
因为常量池中常量的数量是不固定的,所以在常量池入口需要放置一个 u2 类型的数据来表示常量池的容量「constant_pool_count」
为什么要把第0项常量空出来?
之所以将第 0 项常量空出来是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达「不引用任何一个常量池项目」的含义,这种情况可以把索引值置为 0 来表示。
常量池中主要存放什么?
常量池中主要存放两大类常量:字面量和符号引用。
- 字面量比较接近 Java 语言层面的常量概念,如字符串、声明为 final 的常量值等。
- 符号引用属于编译原理方面的概念,包括了以下三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
全限定名?简单名称?描述符?
- 全限定名:用来描述类或接口名,和java中的全限定名有些区别,如java.lang.Object在class中的全限定名是java/lang/Object
- 简单名称:用来描述字段或者方法,如toString(),的简单名就是toString
- 描述符:用来描述字段的数据类型,方法的参数和返回。
描述符的具体描述规则?
字段
如:
基本类型:int ==> I
对象类型:String ==> Ljava/lang/String;
数组类型:long[] ==> [J方法
如:
Object m(int i, double d, Thread t) {..} ==> (IDLjava/lang/Thread)Ljava/lang/Object
什么是特征签名?
- Java语言层面的方法特征签名:特征签名 = 方法名 + 参数类型 + 参数顺序;
- JVM层面的方法特征签名:特征签名 = 方法名 + 参数类型 + 参数顺序 + 返回值类型
javac编译和C编译的区别?
javac没有“链接”这一阶段,走的是运行时动态链接
常量池表结构:
分析Class字节码的工具:Class文件都是二进制格式,可通过Jdk/bin/javap.exe工具,分析Class文件字节码。关于javap用法,可通过javap –help来查看。
详情可以参考:https://blog.csdn.net/luanlouis/article/details/40301985
访问标志
2个字节代表,标示用于识别一些类或者接口层次的访问信息.
类索引、父类索引、接口索引集合
- 类索引:u2,确定该类的全限定名
- 父类索引:u2,确定该类的父类的全限定名
- 接口索引:u2(接口数)+u2[](接口的全限定名),确定实现了哪些接口
索引都会指向常量池中CONSTANT_Class_info类型的常量
字段表集合
字段表用于描述类或接口中声明的变量,格式如下:1
2
3
4
5
6
7field_info {
u2 access_flags; //访问标识
u2 name_index; //名称索引
u2 descriptor_index; //描述符索引
u2 attributes_count; //属性个数
attribute_info attributes[attributes_count]; //属性表的具体内容
}
字段访问标识如下:(表中加粗项是字段独有的)
什么是类型签名?
方法表集合
方法表用于描述类或接口中声明的方法,格式如下:1
2
3
4
5
6
7method_info {
u2 access_flags; //访问标识
u2 name_index; //名称索引
u2 descriptor_index; //描述符索引
u2 attributes_count; //属性个数
attribute_info attributes[attributes_count]; //属性表的具体内容
}
方法访问标识如下:(表中加粗项是方法独有的)
方法中的代码在哪?
对于方法里的Java代码,进过编译器编译成字节码指令后,存放在方法属性表集合中“code”的属性内(属性表见后文介绍)。
父类中的方法需要展示吗?
如果没有覆盖,则不展示
如何区别重载的方法?
根据特征签名:指方法中各个参数在常量池的字段符号引用的集合,不包括返回值。
上述是java中的,Class文件格式中,特征签名范围更广,包括了返回值
属性表
属性表格式:1
2
3
4
5attribute_info {
u2 attribute_name_index; //属性名索引
u4 attribute_length; //属性长度
u1 info[attribute_length]; //属性的具体内容
}
属性表的限制相对宽松,不需要各个属性表有严格的顺序,只有不与已有的属性名重复,任何自定义的编译器都可以向属性表中写入自定义的属性信息,Java虚拟机运行时会忽略掉无法识别的属性。 关于虚拟机规范中预定义的属性,这里不展开讲了,列举几个常用的。
常用的几个:
Code属性表
java程序方法体中的代码,经编译后得到的字节码指令存储在Code属性内,Code属性位于方法表的属性集合中。但与native或者abstract的方法则不会存在Code属性中。
Code是class中非常重要的属性,并可以以此分为两个部分:代码(Code)和元数据(其他)
Code属性的格式如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Code_attribute {
u2 attribute_name_index; //常量池中的uft8类型的索引,值固定为”Code“
u4 attribute_length; //属性值长度,为整个属性表长度-6
u2 max_stack; //操作数栈的最大深度值,jvm运行时根据该值配置栈帧
u2 max_locals; //局部变量栈最大存储空间,单位是slot
u4 code_length; // 字节码指令的个数
u1 code[code_length]; // 具体的字节码指令
u2 exception_table_length; //异常的个数
{ u2 start_pc;
u2 end_pc;
u2 handler_pc; //当字节码在[start_pc, end_pc)区间出现catch_type或子类,则转到handler_pc行继续处理。
u2 catch_type; //当catch_type=0,则任意异常都需转到handler_pc处理
} exception_table[exception_table_length]; //具体的异常内容
u2 attributes_count; //属性的个数
attribute_info attributes[attributes_count]; //具体的属性内容
}
本地局部变量表中存储哪些信息?
入参,局部变量,异常处理器的参数(catch中定义的异常)
为什么没有任何局部变量Locals会等于1?
实例方法中有隐藏参数this, 显式异常处理器的参数,方法体定义的局部变量都使用局部变量表来存放。
另外:max_locals不等于所有局部变量所占Slot之和,因为Slot可以重用,javac编译器会根据变量的作用域来分配Slot给各个变量使用,从而计算出max_locals大小。
异常属性表
格式如下:1
2
3
4
5
6Exceptions_attribute {
u2 attribute_name_index;//"Exceptions"
u4 attribute_length;//属性长度,不包括开头6字节
u2 number_of_exceptions;//异常数
u2 exception_index_table[number_of_exceptions];//CONSTANT_Class_info索引
}
异常属性表和Code中的异常表的区别?
这里的异常属性表仅表示throw后的异常类
LineNumberTable属性
Code属性可选的可变长属性。它可以被调试器用来确定哪个Java虚拟机代码阵列的一部分对应于一给定行号的原始源文件中
格式如下:1
2
3
4
5
6
7
8LineNumberTable_attribute {
u2 attribute_name_index;//"LineNumberTable"
u4 attribute_length;//属性长度,不包括开头6字节
u2 line_number_table_length;//line_number_table表个数
{ u2 start_pc;//code属性中的字节码偏移量
u2 line_number; //源码中的行号
} line_number_table[line_number_table_length];
}
LocalVariableTable属性
Code属性可选的可变长属性。它可以被调试器使用的方法的执行过程中,以确定给定的本地变量的值以及作为域
格式如下:1
2
3
4
5
6
7
8
9
10
11LocalVariableTable_attribute {
u2 attribute_name_index;//"LocalVariableTable"
u4 attribute_length;//属性长度,不包括开头6字节
u2 local_variable_table_length;
{ u2 start_pc;//生命周期开始的字节码偏移量
u2 length;//作用范围长度
u2 name_index;//CONSTANT_UTF_info索引,名字
u2 descriptor_index;//CONSTANT_UTF_info索引,描述符
u2 index;//在局部变量栈中Slot的位置,如果是64位,则占两个Slot
} local_variable_table[local_variable_table_length];
}
SourceFile属性
class文件可选的固定长度的属性。描述文件名,最多只有一个。
格式如下:1
2
3
4
5SourceFile_attribute {
u2 attribute_name_index;//"SourceFile"
u4 attribute_length;//属性长度,不包括开头6字节
u2 sourcefile_index;//CONSTANT_UTF_info索引,名字
}
ConstantValue属性
字段表的定长属性,用来表示字段表中被static修饰的字段
- 类变量: 在类构造器方法或者使用ConstantValue属性来赋值
- 实例变量:在实例构造器方法进行赋值
格式如下:1
2
3
4
5ConstantValue_attribute {
u2 attribute_name_index;//"ConstantValue"
u4 attribute_length;
u2 constantvalue_index;//CONSTANT_Long、CONSTANT_Float等等
}
InnerClasses属性
class文件中的变长属性,用来表示内部类
格式如下:1
2
3
4
5
6
7
8
9
10InnerClasses_attribute {
u2 attribute_name_index;//"InnerClasses"
u4 attribute_length;
u2 number_of_classes;
{ u2 inner_class_info_index;//指向常量池中CONSTANT_Class_info索引,表示内部类符号引用
u2 outer_class_info_index;//指向常量池中CONSTANT_Class_info索引,表示宿主类符号引用
u2 inner_name_index;//指向常量池中CONSTANT_UTF8_info索引,表示内部类名字
u2 inner_class_access_flags;//访问标志
} classes[number_of_classes];
}
Deprecated及Synthetic属性
Deprecated表示过时,Synthetic表示非用户产生
class文件,字段,方法可选属性,只包含前6个简单字节
StackMapTable属性
Code属性中复杂的变长属性,在字节码加载时被新类检查验证器(Type Checker)使用,用来校验字节码的合法性
Signature属性
class文件、字段表、方法表的可选定长属性,用来表示泛型类型
格式如下:1
2
3
4
5Signature_attribute {
u2 attribute_name_index;//"Signature"
u4 attribute_length;
u2 signature_index;//CONSTANT_Utf8_info
}
BootstrapMethods属性
1 | CONSTANT_InvokeDynamic_info { |
1 | BootstrapMethods_attribute { |
1 | CONSTANT_MethodHandle_info { |
复杂属性,和InvokeDynamic指令密切相关
字节码指令介绍
Java虚拟机采用基于栈的架构,其指令由操作码和操作数组成。
操作码:一个字节长度(0~255),意味着指令集的操作码个数不能操作256条。
操作数:一条指令可以有零或者多个操作数,且操作数可以是1个或者多个字节。编译后的代码没有采用操作数长度对齐方式,比如16位无符号整数需使用两个字节储存(假设为byte1和byte2),那么真实值是 (byte1 << 8) | byte2。
放弃操作数对齐意味着什么?
- 优势:可以省略很多填充和间隔符号,从而减少数据量,具有更高的传输效率;Java起初就是为了面向网络、智能家具而设计的,故更加注重传输效率。
- 劣势:运行时从字节码里构建出具体数据结构,需要花费部分CPU时间,从而导致解释执行字节码会损失部分性能。
一个字节长度的指令集带来的限制?
Java虚拟机的指令集对于特定操作只提供有限的类型相关指令,并非为每一种数据类型都有相应的操作指令。必要时,有些指令可用于将不支持的类型转换为可被支持的类型。
如何保证同步开始的指令无论是否异常一定被释放?
编译器会自动生成一个异常处理器
指令参考:http://gityuan.com/2015/10/24/jvm-bytecode-grammar/