Java虚拟机-类文件结构

代码编译的结果从本地机器码转为字节码;
 
无关性的基石:
各个不同平台的虚拟机和所有平台统一使用的程序存储格式-字节码,是构成平台无关性的基石;
实现语言无关性的基础仍然是虚拟机和字节码存储格式;
Java虚拟机不和包括Java在内的任何语言绑定,它只和class文件这种特定的二进制文件所关联;
Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。
基于安全方面的考虑,Java虚拟机规范要求在Class文件中使用许多强制性的语法和机构化约束,但任何一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。

Class类文件的结构
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
 
根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表
 
无符号数
无符号数属于基本数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节和8个字节的无符号数;
无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
 
表:
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯以“_info”为结尾。
表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表;
 
Class结构不像XML等描述语言,由于它没有任何分隔符号;

 
 
魔数和Class文件版本:
魔数(magic):每个Class文件的头4个字节成为魔数,它唯一的作用是确定整个文件是否为一个能被虚拟机接受的Class文件。
Class文件的魔数值是0xCAFEBABE
Class文件版本:第5和第6个字节是次版本号(Minor Version)。第7和第8个字节是主版本号(Major Version)。
Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。(即使文件格式并未发生任何改变,虚拟机也必须拒绝执行超过其版本号的Class文件)。
 
常量池:
紧挨着主次版本号的就是常量池入口;
常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
常量池容量计数器:因为常量池中常量的数量不固定,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数器,是从1开始计数
常量池容量计数器,0x0016,十进制为22,代表有21个常量。
Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都是与一般习惯相同,从0开始计数。
 
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)
字面量:比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等;
符号引用则属于编译原理方面的概念,包括下面三类常量:
1、类和接口的全限定名;
2、字段的名称和描述符
3、方法的名称和描述符
当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

常量池中每一项常量都是一个表,共有11种,表开始的第一位都是一个自己的标志位,表明这个常量属于哪种类型。

Java程序中不能定义超过64KB英文字符的变量和方法名,否则无法编译。
使用JAVAP工具可以分析Class文件字节码:javap -verbose TestClass
 
访问标志:
在常量池结构之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息
包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类,是否被声明为final类型等;
访问标志中一共有16个标志位可以使用;
 
类索引、父类索引与接口索引集合:用来确定集成关系
类索引:一个u2类型的数据,用于确定整个类的全限定名。
父类索引:一个u2类型的数据,用于确定这个类的父类的全限定名。由于Java不允许多重集成,所以父类索引只有一个。
接口索引集合:一组u2类型的数据集合,用于描述这个类实现了哪些接口。
对于接口索引集合,入口的第一项-u2类型的数据为接口计数器,表示索引表的容量。如果该类没有实现任何接口,则该接口计数器值为0,后面接口的索引表不再占用任何字节;
 
字段表集合
字段表(field_info),用于描述接口或者类中声明的变量。
字段(field)包括类级变量以及实例级变量,但是不包含方法内声明的局部变量。
描述该字段是否是public、private、protected、static、final、volatile等;

 
简单名称:指没有类型和参数修饰的方法或者字段名称;
 
描述符:用来描述字段的数据类型,方法的参数列表和返回值。
基本数据类型以及代表无返回值的void类型用一个大写字母表示;
对象类型用字符L加对象的全限定名来表示;


方法表集合:描述方法
Class文件存储格式中对方法的描述和对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样。
包括访问标志、名称索引、描述符索引、属性表集合;
 
方法中的代码存储在方法属性表集合中一个名为Code的属性中;


在Java虚拟机规范中,要重载一个方法,除了要与原方法具有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名,特征签名在java代码层面和字节码层面有不同的定义。
代码层面的签名只包括方法名称,参数顺序以及参数类型;
字节码正面还包括方法返回值和异常表。
因此在Class文件中,如果两个方法有相同的名称和特征签名,但是返回值不同,也是合法的。
 
属性表集合
属性表集合的限制较少,不要求各个属性表有严格的顺序,并且只要不和已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息;
Java虚拟机在运行时会忽略掉它不认识的属性。


Code属性
Java程序方法体中的代码经过javac编译器处理后,最终变为字节码指令存储在Code属性内。

max_stack:虚拟机运行的时候会根据它的值来分配栈帧中的操作栈深度;
max_locals:单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位;
code_length+code:用来存储Java源程序编译后生成的字节码指令;
code_length:表示字节码长度
code:用于存储字节码的一系列字节流。
虚拟机规范中明确限制一个方法不允许超过65535条字节码指令。如果超过了,Java编译器也会拒绝编译。
在任何实例方法中,都可以通过this关键字访问到此方法所属对象,它是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问。
 
Exceptions属性
作用:列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常;
 
LineNumberTable属性
作用:用于描述java源码行号与字节码行号之间的对于关系。不是运行时必需的属性,但是会默认生成到Class文件中;
javac中可以使用-g:none取消,或使用-g:lines来要求生成这项信息。
 
LocalVariableTable属性
作用:描述栈帧中的局部变量表中的变量与Java源码中定义的变量之间的关系。不是运行时必需的属性。
javac中可以使用-g:none取消,或使用-g:vars来要求生成这项信息;
 
SourceFile属性
作用:用于记录生成这个Class文件的源码文件名称。可选。
javac中可以使用-g:none取消,或使用-g:source来要求生成这项信息;
 
ConstactValue属性
作用:通知虚拟机自动为静态变量赋值;只有被static关键字修饰的变量(类变量)才可以使用这个属性;
对于非静态变量,则是在实例构造器<init>方法中进行;
目前Sun Javac编译器中,对于final修饰的静态变量,并且这个变量的数据类型是基本数据类型或String,就使用ConstactValue属性来进行初始化;
如果仅仅是静态变量,未使用final修饰,那么就是在实例构造器<clinit>方法中进行初始化;
 
虚拟机规范中并未要求字段必须设置ACC_FINAL标识。对final关键字的要求是javac编译器自己加入的限制。
 
InnerClasses属性
作用:用于记录内部类与宿主类之间的关联;
 
Deprecated及Synthetic属性
Deprecated作用:表示某个类、字段或方法,已经被程序作者定位不再推荐使用。
Synthetic作用:表示此字段或方法并不是由Java编码直接产生的,而是由编译器自行添加的。JDK1.5以后;
如this字段和实例构造器,类构造器等;
 
StackMapTable属性
JDK1.6发布后增加到Class文件规范中的,是一个复杂的变长属性,位于Code属性的属性表中。
 
Signature属性
JDK1.5发布后增加到Class文件规范中的,是一个可选的定长属性,可以出现于类、属性表和方法表结构的属性表中;
 
BootstrapMethods属性
JDK1.7发布后增加到Class文件规范中的,是一个复杂的变长属性,位于类文件的属性表中
 
 
字节码指令简介
Java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的数字(操作码)以及跟随其后的零至多个代表此操作所需参数(操作数)而构成。
由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。
 
字节码指令集的优缺点:
缺点
1、由于限制Java虚拟机操作码的长度是一个字节(0~255),意味着指令集的操作码总数不超过256条;
2、由于Class文件格式放弃了编译后代码的操作数长度对齐,意味着虚拟机处理那些超过一个字节数据的时候,不得不在运行时从字节中重建出具体数据的结构;
优点:
1、放弃操作数长度对齐,意味着可以省略很多填充和间隔符号;
2、用一个字节来代表操作码,也是为了尽可能获取短小精干的编译代码;
 
这种追求尽可能小数据量,高传输效率的设计是由Java语言涉及之初面向网络、智能家电的技术背景决定的,并一直沿用到至今。
 
字节码与数据类型
 
加载和存储指令
作用:用于将数据在栈帧中的局部变量表和操作数栈之间来回传输;
这类指令包含如下如下内容:
1、将一个局部变量加载到操作栈;***load
2、将一个数值从操作数栈存储到局部变量表;***store
3、将一个常量加载到操作数栈;***const
4、扩充局部变量表的访问索引的指令:wide;
存储数据的操作数栈和局部变量表主要就是由加载和存储指令进行操作;
 
运算指令
运算或算数指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作数栈顶。
算数大致分为:对整型数据进行运算的指令和对浮点型数据进行运算的指令;
Java虚拟机要求在进行浮点数的运算时,所有的运算结果都必须舍入到适当的精度,舍入模式是IEEE754规范中的默认舍入模式,称为向最接近数舍入模式;处理的时候不会抛出任何运行时异常;
在将浮点数转为整数时,Java虚拟机使用IEEE754标准的向零舍入模式,结果会导致数字备截断,所有小数部分的有效字节都会备丢弃;
 
类型转换指令
类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作。
宽化类型转换、窄化类型转换。
 
对象创建与访问指令
对象创建后,就可以使用对象访问指令获取对象实例或者数组实例中的字段或者数组元素。
指令如下:
    1)创建实例的指令:new
 
    2)创建数组的指令:newarray,anewarray,multianewarray
 
    3)访问字段指令:getfield,putfield,getstatic,putstatic
 
    4)把数组元素加载到操作数栈指令:baload,caload,saload,iaload,laload,faload,daload,aaload
 
    5)将操作数栈的数值存储到数组元素中执行:bastore,castore,castore,sastore,iastore,fastore,dastore,aastore
 
    6)取数组长度指令:arraylength
 
    7)检查实例类型指令:instanceof,checkcast
 
 
操作数栈管理指令
 
用于直接操作操作数栈的指令,包括:
 
    1)将操作数栈的栈顶一个或两个元素出栈:pop、pop2
 
    2)复制栈顶的一个或两个数值并将复制值活着双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1
、dup_x2
、dup2_x2
 
    3)将栈最顶端的两个数值互换:swap
 
 
控制转移指令
让JVM有条件或无条件从指定指令而不是控制转移指令的下一条指令继续执行程序。控制转移指令包括:
    1)条件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnotnull,if_cmpeq,if_icmpne,if_icmlt,if_icmpgt等
    2)复合条件分支:tableswitch,lookupswitch
    3)无条件分支:goto,goto_w,jsr,jsr_w,ret
各种类型的比较最终都会转化为int类型的比较操作;
 
方法调用和返回指令
方法调用指令与数据类型无关,但是方法返回指令是根据返回值的类型区分的;
 
异常处理指令
在java程序中显式抛出异常的操作是由athrow指令来实现;
 
同步指令
方法级的同步是隐式的,无需通过字节码指令来控制,它实现在方法调用和返回操作中。
虚拟机从方法常量池中的方法标结构中的 ACC_SYNCHRONIZED标志区分是否是同步方法。方法调用时,调用指令会检查该标志是否被设置,若设置,执行线程持有moniter,然后执行方法,最后完成方法时释放moniter。
同步一段指令集序列,通常由synchronized块标示,JVM指令集中有monitorenter和monitorexit来支持synchronized语义。
结构化锁定是指方法调用期间每一个monitor退出都与前面monitor进入相匹配的情形。JVM通过以下两条规则来保证结结构化锁成立(T代表一线程,M代表一个monitor):
     1)T在方法执行时持有M的次数必须与T在方法完成时释放的M次数相等
     2)任何时刻都不会出现T释放M的次数比T持有M的次数多的情况

虚拟机实现的方式主要有以下两种:
1、将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集;
2、将输入的Java虚拟机代码在加载或执行时翻译成宿主机CPU的本地指令集(JIT代码生成技术)
posted @ 2015-05-19 12:03  time-info  阅读(190)  评论(0编辑  收藏  举报