各种不同的平台虚拟机与所有平台都统一使用的程序存储格式–字节码是构成平台无关性的基石。
除了在Java外,还有很多其它在JVM上运行的语言比如:JRuby、Scala、Groovy等。
Class文件结构:
实现语言平台无关性的基础仍然是是虚拟机和字节码存储格式。
Java虚拟机不与包括Java在内的任何语言绑定,它只与“Class文件”这特定二进制文件格式有关,Class文件中包含了Java虚拟机指令集和符号以及其它若干辅助信息。基于安全考虑,对Class文件做了强制性语法和结构化约束,任何一门语言都可以表示为一个被JVM所接受的有效Class文件。需要字节码命令组合实现各种语义。
任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定定义在文件里(类或接口也可以通过类加载器直接生成)。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件中,中间没有添加任何分隔符,内容几乎全部是运行时必要的数据,没有间隙。当遇到占用超过8位字节以上空间的数据时,会按照高位在前(大端存储)的方式分割成若干个8位自己进行存储,其文件格式采用类似C语言结构体的伪结构来存储数据且只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础。
无符号数:以u1、u2、u4、u8分别代表一个字节、两个字节、四个字节、八个字节的无符号数,无符号数可以描述数字、索引引用、数量值或按照utf-8编码构成的字符串值。
表:是由多个无符号数或者其他表作为数据项的符合数据类型,所有的表都习惯性以“_info”结尾。
Class文件结构排布
1、魔数:每个Class文件开头四个字节为魔数,十六进制值为“oxCAFEBABE”,用于标识是一个jvm认可的Class文件,选择不使用”.class”验证是因为文件后缀是可以改的。
2、版本号:总共四个字节,第五个和第六个字节是次版本号,第七个和第八个是主版本号。Java1.7可生成主版本号最大为51.0
3、常量池:紧接着就是常量池的入口,由于常量池数据每个Class文件可能都不一样,因此长度不是固定的,需要开头一个u2类型的数据来表示常量池中常量的项数(并非字节长度,每一项常量长度可能不一样),而实际项数是数值-1,下标索引从1开始,0作为表示后面指向常量池的索引值数据在特定情况下需要表达“不引用任何一个常量池的项目”。
常量池中每一项常量都是一个表,比如开头第一个u1(07)表示类型为:“CONSTANT_Class_info”,则表示这是一个类或接口的符号引用,则它的结构式tag(u1(07))、name_index(u2(是一个索引值指向常量池中一个CONSTANT_Utf8_info类型常量)).标志位u1已经解释过,则u1与紧跟后面u2组成“CONSTANT_Class_info”,因此第一项常量还包含后面两个字节长度的内容0x0002,表示指向第二个常量
由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info类型常量来描述名称,从上图中可以看出,tag标志位为1的未该类型常量,用u2类型表示后面字节长度,由于2个字节最多表示2的16次方(0-65535)因此方法或变量名称用英文字符最多不超过64KB(64K字节),紧跟着长度后面就是length个字节长度的内容。
Oracel公司为我们准备了一个专门分析Class文件字节码的工具 javap 在命令行中输入javap -verbose TestClass就可以把字节码内容。
CONSTANT_Class_info结构分两部分,首先是所有项都必须有的u1类型tag标志位,后面一个u2类型的index指向常量池中一个全限定名常量项(utf8)的索引。
4、访问标志
常量池后面是一个u2类型的访问标志,这个标志用于识别一些类或接口层次的访问信息:类还是接口,是否为public类型,是否为abstract类型,如果是类的话是否被声明为final等。
ACC_PUBLIC:0x0001
ACC_FINAL:0x0010
ACC_SUPER:0x0020
…..
所有符合包含的项相加值为该访问标志的值。
5、类索引、父类索引和接口索引集合
类索引和父类索引都是一个u2类型的数据,它的值是指向常量池中一个CONSTANT_Class_infol类型的常量项索引;但是由于接口可以继承多个所以是不固定的,因此分两部分:第一部分u2类型的(interfaces_count)接口计数器,用来记录接口的数量;第二部分就是interfaces_count个接口索引,指向常量池中全限定名常量项。
6、字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量(static修饰)和实例变量(非static修饰),但不包括方法中的变量(方法中变量存储在栈帧中的局部变量表中,一个栈帧对应一个方法执行)。
字段表集合中不会列出从超类或者父类接口中继承而来的字段,但可能会列出java原本不存在的字段:比如this或者内部类指向外部类的字段。
包含两部分:计数容量和项内容
包含的信息有;字段的作用域(public protect…)是实例变量还是类变量、可变性(final)、并发可变性(volatile)…..
上述各个字段都是boolean值,最终合并起来用一个值access_flags(field_info结构如下),具体的数据还是引用常量来表示,最终的数据项格式:
u2 access_flags 1 与类中的访问标志符非常相似
u2 name_index 1 字段的简单名称 inc()方法,为inc 去掉类型和参数的方法或字段名称
u2 descriptor_index 1 字段和方法的描述符 描述字段的数据类型、方法的参数列表(数量、类型及顺序)和返回值。描述符比如“java.lang.String[][]’”二维数据:”[[Ljava/lang/String;”(注意java对象用L表示,long用J表示,区别与其他的类型标识),int[]:[I; 描述方法先参数列表顺序,后返回值的顺序。参数列表要在小括号内:void inc():()V、java.lang.String.toString():()Ljava/lang/String; 、int index(char[] source,int sourceoffset,char[] target):([CI[C)I
……………………………………………………………………..
字段表包含的固定数据项到此结束,后面属于非必须项来描述额外信息。
u2 attributes_count 1
attribute_info attributes attribute_count
7、方法表集合
与字段表集合表示方法类似,内容有区别。也有一个最终的项结构类似。方法除了用户编写的外,编译器也会自动添加比如类构造器“”方法和实例构造器“”方法。
重载:需要一个与原方法相同名称,但不同特征签名,(java代码层面)而返回值不包含在签名中,因此不能仅仅靠返回值来实现重载,因此要参数类型及顺序不同。
在方法表的属性表有一个code属性用来存储编译后方法体字节码指令(与之对应的字段表属性中有ConstantValue表示final关键词定义的常量值)。
8、属性表集合
Class文件、字段表、方法表都可以携带自己的属性表集合,这部分应该是从他们之中单拉出讲解,并不属于单独的部分,并且java虚拟机对这个部分要求就稍微宽松一些。
其中属性表中最重要的一个属性是Code,方法表中的属性表中的code属性存储方法体。
》Code属性
它的结构:
如果把一个java程序中的信息分为代码(Code,方法体里面的java代码)和元数据(metadata,包括类、字段、方法定义及其他信息)两部分,那么整个Class文件中,Code属性用于描述代码,所有的其他数据项都用于描述元数据。
其实java字节码执行引擎是面向操作数栈,但是某些指令(invokespecial)还会带有参数(紧跟指令后面,而其他很多操作数都是操作数栈顶元素或栈顶前几个元素),区别于面向物理机的寄存器的系统。
这个类有两个方法–实例构造器()和inc(),这两个方法很明显都没有参数, 为什么args_size=1,这是因为每个实例方法里都有一个对象指向所属对象,而实现就是编译器编译的时候把this关键字的访问,转变为一个普通方法参数的访问,就是自动把参数传进去,方法体里面就可以使用。如果方法是static则是不会存在this,也就args_size=0。
字节码指令后面是显示异常表处理表的集合,这对于Code属性并不是必须存在的。
异常表的格式:
含义:当字节码在第start_pc行到第end_pc行出现了类型为catch_type(指向一个CONSTANT_Class_info型常量索引)或者其子类的异常,则转到第handler_pc行去继续处理。当catch_type为0时,表示不指向常量池,则这个时候所有的异常都到handler_pc行处理。由此可以看出编译器是使用异常表而不是简单跳转指令来实现java异常及finally处理机制(以前是)。
》Exception属性
区别于前面的code属性中的异常表,与Code同级,且属于方法表属性表,Exceptions属性列举出方法中可能抛出的受检查(用户在throws 后面列出的异常)的异常。
属性表结构:
》LineNumberTable属性
描述java源码行号与字节码行号对应的关系,这样抛出异常可以定位行号,如果选则去掉则不会显示行号
》LocalVariableTable属性
描述栈帧中局部变量表中的变量与java源码定义的变量的关系,如果去掉对运行虽然没有影响,但别人引用这个方法时,参数名称会丢失,会使用arg0,arg1来表示,而且调试的时候无法根据参数名称来获取参数值。
》Constantvalue属性
通知jvm自动为静态变量赋值,对于实例变量(非static)的赋值在方法中进行,而对于类变量采用两种方式:有static没有final则在初始化阶段赋值,有final修饰,则根据Constantvalue在准备阶段就开始赋值。但这种方式只限于基本数据类型和String。
》InnerClass属性
记录一个类内部类的信息(数量,每个类的一些情况)
》StatckMapTable属性
在Code属性中,在虚拟机类加载的字节码验证阶段被类型检查器使用,代替以前比较消耗性能的基于数据流分析的类型推倒验证器。这项属性表述了方法体中所有的基本块(basic block 按照控制流分拆的块)开始时本地变量表和操作数栈应有的状态,在字节码验证期间,不需要推到合发性状态只需验证即可,这样可以节省一些时间,但也会存在被篡改的问题。
》Signature属性
用来记录泛型类型,因为java语言的泛型采用类型擦除法实现伪泛型,在字节码中,类型被擦除,虽然实现简单,但运行期间反射无法获得泛型信息,所以增设这个属性来通过反射也能获得泛型类型。可以表示类签名、方法类型签名、字段表类型签名。
字节码指令简介
Java虚拟机的指令是由一个字节长度、代表某种特定操作含义的数字(操作码)以及跟随后面的零个(操作数栈数据)或多个此操作所需的操作数构成。jvm面向操作数栈而不是寄存器,所以它的大多数指令不跟操作数(默认在操作数栈)
1、字节码与数据类型绑定
**加载和存储指令用在数据在栈帧中局部变量表和操作数栈:**iload用于从栈帧中局部变量表中加载int类型到操作数栈中。fstore把操作数栈中float类型存储在局部变量表中。由于虚拟机操作码只有一个字节(最多共有256种指令),包含操作类型的指令操作码也有很大压力,如果支持所有的数据类型,则指令数可能已经超出256种了,所以对于有些不支持的类型(byte、char、short、boolean)先通过类型扩展或零位扩展为int类型处理。
2、运算指令
对于操作数栈两个操作数运算,并把结果压入栈顶。java虚拟机规范没有明确规定整型溢出具体运算结果。,仅规定整型(int long)进行除(idiv ldiv)和取余数(irem lrem)时除数为零时的异常。其余任何任何时候都不应该抛出异常(超出范围就按能表示最大值或最小值表示,如果原值是无穷大或者无穷小那么都表示NaN)。
3、类型转换指令
类型转换指令可以将两种不同数值类型进行相互转换:实现用户代码中的显示类型转换操作;或指令集中不支持的数据类型转为指令支持的类型。
jvm直接支持(无需显示转换)宽化类型转换(小范围向大范围转换):byte/char/short->int->long->float->double(虽然float占四个字节但表示方法不同,所以比long表示的范围大)。
浮点值转为整型数值:
将一个浮点值窄化为整型(int 或者long)时,如果浮点值是NaN,那转换结果int或long就是0;
如果浮点数不是无穷大,向零舍入后在整型范围内,那么就是转换结果。
如果不在范围则根据符号转换整型所能表示的最大或者最小正数。
double到float:
如果转换的值结果绝对值太小而无法使用float表示的话,将返回float类型的正负零。如果转换结果的绝对值太大而无法使用float来表示的话,将返回float类型的正负无穷大。对于NaN值将按规定转换为float类型的NaN。
4、同步指令
jvm支持方法级和方法内部一段指令序列的同步,这两种同步结构都是使用管程来支持的。
方法级的同步是隐式的,无需通过字节码指令控制,它实现在方法调用和返回操作之中。虚拟机可以通过方法结构中的ACC_SYNCHRONIZED访问得知是否为一个同步方法。如果是同步方法,则执行线程会被要求先成功持有管程,然后才执行方法,方法完成(正常或非正常)释放管程。执行线程有个管程,其他线程无法获得同一个管程,如果方法执行期间抛出了异常,且无法处理,这个方法所持有的管程在异常抛出方法外释放。
指令集序列的同步通过synchronized来实现:
void onlyMe(Foo f){
synchronized(f){
doSomething();
}
}
指令集monitorenter和monitorexit两条指令来支持同步,年末一起必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter都必须有对应的monitorexit指令执行。