深入理解java虚拟机—类文件结构

引言

我们知道c语言的编译过程如下:预编译->编译生成汇编代码->汇编生成机器代码->链接生成可执行文件。c语言没有跨平台性的原因就是因为c语言是根据计算机cpu的指令生成的机器代码,所以它生成的可执行文件只有在相同指令集的环境下来能运行。
而java语言具有跨平台型,java编译之后是生成的字节码,通过java虚拟机来解释运行字节码(也会做后编译)。每个平台的java虚拟机不同,但是不同的虚拟机可以运行同样的class文件,这样就完成了跨平台性。
class文件由具有特定格式的java字节码组成。

Class类文件的结构

class文件一个字节为基础单位的二进制流,由无符号数和表两种数据类型组成组成,无符号数实质上就是不同大小的字节,u1、u2、u4、u8分别代表1、2、4、8个字节;表是由多个无符号数和多个表组成的一种数据结构。
Class文件由固定的文件格式,从头到尾的数据有固定的含义和类型。
(1)魔数:每个class的头4个字节称为魔数,它的作用是确认这是一个class文件,很多文件头部都有这个魔数,只是为了判断这个文件是否是这个类型,因为文件的后缀名是可以修改的,并不安全。4个字节的十六进制为0xCAFEBABE,cafe也就是咖啡,跟java的商标密切相关。
(2)版本号:接着魔术的四个字节是class文件的版本号,第5,6个字节是次版本号,第7,8个字节是主版本号。java的版本号从45开始,也就是说jdk1的主版本号是45,jdk8的主版本号是52,我们知道jdk是向下兼容的,只能执行这个版本及以下的class文件,所以需要通过这个版本号来确认是否可以执行这个class文件。
(3)常量池:在版本号后放的就是常量池,常量池的入口是个类型为u2(2个字节)的数据,表示常量的数量,常量的计数从1开始,把0空出来的目的是为了有些索引的数据表示为不指向任何常量。常量池主要存了字面量和符号引用,字面量就是java中的常量,而符号引用包含了下列三种常量:类和接口的全限定名;字段名称和描述符;方法名和描述符。也就是说字面量就是对我们编写的java代码的一种描述记录。
java的编译不同于c语言,在class文件中不会保存方法、字段的内存布局信息,这些方法字段的入口在运行期进行动态连接,再生成内存入口地址。在运行时,在常量池中获得对应的符号引用,在类创建或运行时解析翻译到具体的内存中。常量池的每个常量都是一个表,表中的第一个字段为u1类型(一个字节),不同的值代表不同类型的常量表类型;在jdk1.7中有如下14种类型。
这里写图片描述
每个表都有不同的结构,如下:
这里写图片描述
这里写图片描述
这里写图片描述
class文件中的字段方法都是引用CONSTANT_Utf8_info型常量来描述名称,索引我们可以看到CONSTANT_Utf8_info型常量能表示的最大长度为u2类型所能表示的长度,2个字节的最大值为2^16-1,也就是65535个字节,如果java程序中定义了超过64KB的英文字符的变量或方法名,将无法编译(个人认为,不可能,谁没事定义这么长的变量或方法名)。
常量类型表中的index表示指向第几个常量表,常量池中的常量表按顺序排,从1到常量池的长度,所以可以通过index来定位常量。
这里写图片描述
上面为一个普通的class的二进制文件,已通过十六进制编辑器打开,我们通过分析这个可以分析出常量池的内容,但是比较麻烦,需要逐个记录下常量表的内容,我们可以通过jdk自带的工具javap来分析class文件,找到常量池,如下图所示。
这里写图片描述
(4)访问标志:在常量池结束后紧接着是两个字节的访问标识。
以下是访问标志表:
这里写图片描述
直接通过class文件很难找到这个位置,因为常量池的内容太多太复杂。我们可以通过javap分析工具找到常量的最后一个值,例如:
这里写图片描述
然后再去class的二进制文件中寻找:
这里写图片描述
通过十六进制编辑器,在这个0x56就是最后的那个V,所以后面的0x0021就是访问标识。
这个类只被public进行修饰,所以是0x0001|0x0020=0x0021.
(5)类索引,父类索引、接口索引集合:
跟在访问标识后的分别是u2类型的类索引,u2类型的父类索引,u2类型的接口计数器和一组u2类型的接口索引集合。这些索引指向常量池中的类描述符常量。
这里写图片描述
我们看从0x0021后面的数,首先是0x0006,也就是指向常量池中的第六个常量,在这之前已经通过了javap分析出了常量池,这样能够更加直观的看到常量,
这里写图片描述
我们可以看到这是一个类描述符常量,值为该类的类名。
接着看0x0007,也就是第7个常量:
这里写图片描述
这是个Object类,因为这个类没有继承其他类,但是所有类都是Object的子类。
接着是0x0001,也就是说只有一个接口;接着是接口索 引集合,只有一个0x0008,那就是常量池中的第八个常量。
这里写图片描述
看出这个接口类是com.creat.abstractfactory.ProduceFactory.
我们来看一下源码:
这里写图片描述
跟我们从字节码中提取的信息是一样的。
(6)字段表集合:
字段表是用于描述接口或类中声明的变量,也就是类中的属性。其实看到字段表集合,我们就知道在之前肯定有个集合的数量,这个数量值是u2类型的。字段表包括的信息有:字段的作用域、是实例变量还是类变量(static是实例变量的修饰符)、可变性、并发可见性、是否可以被序列化、数据类型、字段名称。下面是字段表的结构。
这里写图片描述
第一个是字段访问标识access_flags,下面是访问标志的表:
这里写图片描述
第二个是name_index表示字段字面常量在常量池中的位置。
第三个是descriptor_index对该字段的描述(可以描述字段 数据类型,方法的参数列表(包括数量、类型以及顺序)和返回值)的常量引用,也就是常量池中的位置;以下是描述标识符的含义表:
这里写图片描述

也许看到这,有人会说:描述符和全限定名有啥区别?我们可以看到一个类的全限定名可以是如下:com/creat/avl/Test
,而描述符可以描述字段的数据类型、方法的参数列表和返回值,基础的类型描述符在上面那个表中已经有了,例如byte的描述符为
B,void的描述符为V,对象类型邮电类型全限定名,不过会在前面加个L,例如Ljava/lang/Object。
如果是数组类型会在前面加个[,那么二维数组会加两个[也就是[[,例如String[][]表示为[[Ljava/lang/String;如果描述符用来描述方法,那么就是按照先参数列表,后返回值的顺序描述,例如方法int add(String[] a,int b)的描述符为([Ljava.langString;I)I 如果方法是int add(int a,intb)那么描述符就为(II)I
接下来就是属性的量,用u2表示,可想而知 接下来就是属性集合。属性表暂且不看,方法表集合中也有属性表,等看完方法表集合,一起看属性表。
(7)方法表集合:
在字段表集合之后就是方法表集合,下面是方法表的结构。
这里写图片描述
看到方法表集合,我们就只有入口有个方法表的计数器,用u2类型表示。
第一个为方法访问标识access_flags,用u2类型表示,下面是方法访问标识的类型。
这里写图片描述
第二个是方法名的字面量常量索引,
第三个是方法描述符,之前提到过,在这也不说了;
第四个为属性表的数量值,
第五个为属性表的集合。
java方法里面的代码被作为属性进行装载到属性表中。
这里有一点要清楚的是:如果父类的字段或者方法没有被子类重写,那么在子类的class文件中就不会出现。
同时编译器会自动添加两个方法:

类构造器<clinit>和实例构造器<init>方法

在java语言中,如果方法名一样,参数一样,返回值不同,那么是无法完成方法重载的,但是可以合法共存于class文件中。同时java语言中字段名都要不一样,但是如果字段名一样,描述符不同,也可以在class文件中共存。
(8)属性表集合:
在之前的字段表和方法表中都有涉及到了属性表集合。
class文件中以下这些预定义的属性:
这里写图片描述
这里写图片描述
这里写图片描述
首先一个符合规则的属性表是以下结构:
这里写图片描述
第一个是2个字节表示属性类型名,指向常量池中的位置。
第二个是四个字节表示的属性长度;
第三个则是属性的值,但是这个值是不定的。
也就是说一个规则的属性表具有attribute_name_index和attribute_length两个结构,第三个为各种属性表的自定义结构。
Code:例如以下是Code属性整体的表结构:
这里写图片描述
这是存放方法中代码指令的表。
max_stack是操作数栈的深度,max_locals代表局部变量表所需空间,单位为Slot,那些byte、char、float、returnAddress长度不超过32位的数据类型,每个局部变量占一个Slot,而double、long这种64位的数据类型需要两个Slot来存放。code_length代表字节码的长度,code用来存放字节码指令,每个指令就是一个字节,不同的字节代表不同的指令。但实际上字节码指令的最大长度不能超过u2,也就是65535条字节码指令。所以如果方法过长可能导致编译失败。
在code之后是方法中的异常表,首先是异常表集合的长度,接着是异常表集合。下面是异常表的结构:
这里写图片描述
如果字节码在start_pc行到end_pc行(不包括end_pc)之间出现了catch_type(指向一个CONSTANT_class_info常量)或者子类的异常,则跳带handler_pc行进行处理。
我们可以用javap去解读字节码中的指令,这样就能非常清楚的看到异常的处理过程。
Exceptions:
这里的Exception跟code中的异常表不一样,这里的Exceprion是方法表中的一个属性表,描述的是方法后面抛出的异常。下面是Exception表的结构:
这里写图片描述
attribute_name_index和length就不说了,接下来是number_of_exceptions表示异常数量,接下来就是异常集合,指向常量池中的CONSTANT_Class_info类型常量。
LineNumberTable:
这个属性用来描述java源码和字节码行号的关系。这个不是必要属性,可以用在javac参数后加-g:none或-g:lines取消生成这项信息,影响就是如果抛出异常就不知道异常是在哪行源码出的错误。下面是该属性的结构:
这里写图片描述
line_number的表结构是有两个u2的数据项,前一个是字节码行号,后面是java源码的行号,但是用javap分析出来的行号映射java源码在前面,字节码行号在后面。
LocalVariableTable:
该属性表用来描述栈帧中局部变量表与java源码中定义变量的关系,这个也不是必要属性。下面是该表的结构:
这里写图片描述
前三项就不多说了,主要看第四项local_variable_info表,这个表是表示栈帧中局部变量表和源码中局部变量的关联。下面是该表结构:
这里写图片描述
start_pc表示该变量生命周期开始的字节码偏移量,length表示生命周期在字节码中的覆盖范围长度。name_index指向常量池中变量名,descriptor_index指向该变量的描述符。index代码这个变量在局部变量表中slot的位置。
在jdk1.5及之后,又引入了LocalVariableTypeTable属性表,与LocalVariableTable非常相似,只是把descriptor_index改成了Signature特征签名,主要是针对泛型。特征签名主要是为了记录泛型类型。
SourceFile:用来记录java源码文件名,下面是表结构。
这里写图片描述
ConstantValue属性:
该属性是通知虚拟机为静态变量赋值,也就是说如果在类中直接给static成员赋值,例如”static int i = 100”那么,则会生成ConstantValue来通知虚拟机给静态成员赋值。如果不是静态成员,那么对成员的赋值是在实例化构造器中,静态成员的赋值有两种,一种是在类构造器中,还有一种是生成ConstantValue对其赋值,如果是static final同时是基本类型或者String,那么采用ConstantValue,如果不是那么采用在类构造器中赋值。
InnerClasses属性:
这个表是类的内部类属性集合,该属性用于激励内部类与外部类之间的关联。也就是说,如果一个类有内部类,那么编译器会为它生成该属性,下面是表结构:
这里写图片描述
inner_class_info_index是内部类的类信息常量引用,outer_class_info_index是外部类的类信息引用,inner_name_index是
内部类的名字常量索引,如果是匿名内部类,那么这个值为0;inner_class_access_flags是内部类的访问标识,下面是标识的类型:
这里写图片描述
Deprecated和Synthetic属性:
这两个属性只是做一个标识的作用, Deprecated属性表示该方法或该类不被作者所推荐使用。Synthetic代表该方法或该字段由编译器自动添加,也可以设置ACC_SYSNTHETIC标识。
StackMapTable属性:
这个是jdk1.6之后的属性,这个属性会被虚拟机验证字节码阶段使用,由于之前在运行阶段对字节码进行验证效率不高,所以在jdk1.6之后,编译阶段就将验证类型存在class文件中,这个验证类型可以代替验证字节码的推导过程,虚拟机在运行阶段验证验证类型来替代直接验证字节码。下面是属性结构。
这里写图片描述
Signature属性:
这个属性是用来记录泛型的类型,因为在编译之后泛型是会被擦除的,这个属性就可以记录下泛型的信息。
BootstrapMethods属性:
这个属性用来保存invokedynamic指令引用方法的限定符。

总结

class类文件由以下部分组成:

  1. 魔数
  2. 次版本
  3. 主版本
  4. 常量池
  5. 类访问标识
  6. 此类信息常量池索引
  7. 父类信息常量池索引
  8. 接口集合常量池索引
  9. 字段表集合
  10. 方法表集合
  11. 类属性集合
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值