本文实例讲述了深入理解JVM之Class类文件结构。分享给大家供大家参考,具体如下:
我们平时在DOS界面中往往需要运行先运行javac命令,这个命令的直接结果就是产生相应的class文件,然后基于这个class文件才可以真正运行程序得到结果。自然。这是Java虚拟机的功劳,那么是不是Java虚拟机只能编译.java的源文件呢?答案是否定的。时至今日,Java虚拟机已经实现了语言无关性的特点。而实现语言无关性的基础是虚拟机和字节码的存储格式,Java虚拟机已经不和包括Java语言在内的任何语言绑定。它只与“class”文件这种特定的二进制文件相关联。在class文件中包含了Java虚拟机指令集和符号表以及若干辅助信息。可以很容易想到Java(本质上不是Java语言本身的平台无关性,而是其底层的Java虚拟机的平台无关性使然。)的跨平台,因为任何一门功能性语言都可以表示为能被Java虚拟机接受的有效的class文件。比如,除了Java虚拟机可以将Java源文件直接编译为class文件外,使用JRuby等其他语言的编译器一样可以把程序代码编译成class文件,由此可见,Java虚拟机并不关心class文件是由何种语言编译来的。
Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在class文件中,中间没有任何分隔符,这使得class文件中存储的内容几乎是全部程序运行的程序。Java虚拟机规范规定,Class文件格式采用类似C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表。
无符号数属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾。那么表是干嘛的呢?表主要用于描述有层次关系的复合结构的数据,比如方法、字段。需要注意的是class文件是没有分隔符的,所以每个的二进制数据类型都是严格定义的。具体的顺序定义如下:
在class文件中,主要分为魔数、Class文件的版本号、常量池、访问标志、类索引(还包括父类索引和接口索引集合)、字段表集合、方法表集合、属性表集合。
头4个字节是魔数,魔数的唯一作用在于确定这个Class文件是否是Java虚拟机接受的Class文件。如gif和jpeg等在文件头中都存在魔术,使用魔术而不是使用扩展名是基于安全性考虑的——扩展名可以随意被改变。Class文件的魔术值为“0xCAFEBABE”(咖啡宝贝?)。
紧接着魔数的4个字节是Class文件版本号:版本号又分为次版本号和主版本号。其中前两个字节用于表示次版本号,后两个字节用于表示主版本号。这个的版本号是随着jdk版本的不同而表示不同的版本范围的。如果Class文件的版本号超过虚拟机版本,将被拒绝执行。
常量池可以简单理解为class文件的资源从库,这种数据类型是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的项目之一。在常量池中主要存放字面量和符号引用。字面量比较接近Java语言层面的常量概念,比如文本字符串、声明为final的常量值等(百度百科的解释是字面量是用双引用号引住的一系列字符)。符号引用则主要包括三类常量:
符号引用与直接引用的关联
符号引用是一组符号,用来描述所引用的目标,符号是以任何形式存在的字面量。对于符号引用Java虚拟机并没有严格的限制。规定只需要使用的时候能够无歧义定位到目标就可以。常量池存在于Class文件中,而Class文件是必须首先通过Java虚拟机的类加载机制加载到内存中(确切的说是方法区这个内存区域,回顾一下,方法区存放的主要是对象的实例,这个Class文件是虚拟机对外接受访问的接口)。符号引用属于常量池中的内容,那么是不是说符号引用的目标已经加载到内存中了呢?答案是否定的,因为符号引用与虚拟机的内存布局无关,符号引用的目标并不一定已经加载到内存中了。
直接引用可以是直接指向引用目标的指针、相对偏移量或者是一个能够间接定位到目标的句柄。直接引用是和虚拟机的内存布局有关的,同一个符号引用在不同的虚拟机上翻译的直接引用一般是不同的。如果有了直接引用,那么引用的目标必定是存在内存中的。
在常量池中每一项常量都是一个表,在jdk1.7中共有14中常量类型,所以常量池的项目就对应14张表,这14张表的每种类型都不一样。但是有一个共同特点:表开始的第一位都是一个u1类型的标志位,代表这个常量属于哪种类型。
需要注意的是,在Class文件中,方法、字段都需要引用CONSTANT-Utf8_info类型的常量,所以这种类型的常量的长度有一定的限制,也就是Java中方法、字段的最大长度。在CONSTANT-Utf8_info中,其length的值u2,说明Java虚拟机只能编译最大大约64KB的变量或者方法名。超过的话将不会进行编译。
常量池之后的数据结构是访问标志(access_flags),这个标志主要用于识别一些类或者接口层次的访问信息,主要包括:这个Class是类还是接口、是否定义public、是否定义abstract类型;如果是类的话是否被声明为final等。具体的标志访问如下:
这个数据项主要用于确定这个类的继承关系。
其中类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据。在Java中由于不允许多继承,所以父类索引是唯一的,但是一个类可以实现多个接口,所以得到的接口索引是一个集合,表示这个类实现了哪些接口。
字段表用于描述接口或者类中声明的变量。
字段包括类级变量和实例级变量,但是不包括方法内部声明的局部变量(这些变量是存储在Java虚拟机栈中的局部变量表中的)。自然,描述一个字段的信息包括:字段的作用域(public、protected、private)、实例变量与否(static)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、字段数据类型(基本数据类型、对象、数组)、字段名称。字段的信息也被存放在一张表中,其字段表包括三种类型:
上面出现了简单名称,上文中出现了全限定名,以及这里出现的描述符,三者有什么区别呢?其中全限定名称比较好理解,就是类的完整路径信息。而简单名称则是指没有类型和参数修饰的方法或者字段名称,比如一个方法如下:
public void inc(int a,int b){ System.out.println(a+b); }
那么这个方法的简单名称就是inc。
相对于以上两者,描述符相对复杂一些。描述符的主要的作用是描述字段的数据类型、方法的参数列表和返回值。其中我们熟悉的void,在Class文件中用V表示。下面是完整的描述符标志的含义:
对于数组类型,每一维度使用一个前置的“[”字符描述,如果是二维数组,那么就有两个“[”符号。比如“java.lang.String[][]”会被记录成“[[Ljava.lang.String;”
对于方法,则是按照县参数列表后返回值的顺序进行描述的。比如方法int inc(int a,int[] b,char[][] c,int d)的描述符是“(I[I[[CI)I”。
JVM中堆方法表的描述与字段表是一致的,包括了:访问标志、名称索引、描述符索引、属性表集合。方法表的结构与字段表是一致的,区别在于访问标志的不同。在方法中不能用volatile和transient关键字修饰,所以这两个标志不能用在方法表中。在方法中添加了字段不能使用的访问标志,比如方法可以使用synchronized、native、strictfp、abstract关键字修饰,所以在方法表中就增加了相应的访问标志。
要注意的是,如果父类方法没有在子类中重写,那么在方法中不会自动出现来自父类的方法信息。同样的,有可能添加编译器自动增加的方法,比如方法。
前面的Class文件、字段表和方法表都可以携带自己的属性信息,这个信息用属性表进行描述,用于描述某些场景专有的信息。在属性表中没有类似Class文件的数据项目类型和顺序的严格要求,只要新的属性不与现有的属性名重复,任何人都可以向属性表中写入自己定义的属性信息。
Code属性
Java程序方法体中的代码经过javac编译最终编译成的字节码指令就保存在Code属性中。但是并非所有的方法表都必须存在这个属性。Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code)和元数据(Metadata,包括类、字段、方法定义及其其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有其他的数据项目都用于描述元数据。
Exceptions属性
这个属性的作用是列举出方法中可能抛出的受查异常(Checked Exception),也就是描述throws 后的列举的异常
LineNumberTable属性
主要用于描述Java源代码行号与字节码行号之间的对应关系。这个属性也不是必须的。如果没有这个属性,对程序的直接影响就是当抛出异常的时候无法显示对应的行号;并且在调试的时候无法通过设置断点的方法是调试程序。
LocalVariableTable属性
用于描述栈帧中局部变量表中的变量与Java源码中定义的变量的之间的关系。也不属于必须的属性。如果没有这个属性,产生的直接影响就是当别人引用这个方法的时候,所有的参数名称都会丢失,IDE将会使用诸如args0、args1之类的参数进行显示。自然,当调试程序的时候,显示的参数名称是不可知的。
SourceFile属性
用于记录这个Class文件的源码文件名称。如果不使用这个属性,那么当抛出异常的时候,堆栈中将不会显示出错代码所属的文件名。
ConstantValue属性
作用是通知虚拟机自动为静态变量赋值。要注意的是,只有被static关键字修饰的额变量才可以使用这个属性(类变量)。对于非类变量,初始化是在方法中进行的;对于类变量可以选择两种方式进行变量的初始化:一是在类构造器方法中使用;二是是ConstantValue属性。目前Sun Hotspot的选择原则是:如果一个变量同时使用static和final关键字修饰,并且这个变量是基本数据类型或者java.lang.String类型的话,就使用ConstantValue属性进行初始化。如果没有被final修饰或者并非是基本数据类型,那么将会选择使用方法进行初始化。
InnerClass属性
这个属性主要用于记录内部类与宿主类之间的关联关系。
Deprecated以及Synthetic属性
这两个属性都属于标志类型的布尔属性,只存在有没有的区别。
Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,可以通过注解@deprecated实现
Synthetic属性代表此字段并不是由Java源码产生的,而是通过编译器自行添加的。
StackMapTable属性
该属性的目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
Signature属性
这个属性是专门用来记录泛型类型的,因为在Java语言采用的是擦除法实现的泛型,在字节码(Code属性)中,泛型信息编译之后会被擦除。擦除法的优点是能够节省泛型所占的内存空间,缺点是在运行期间无法通过反射得到泛型信息,而Signature属性则弥补了这一缺陷。现在的Java反射API已经能够得到泛型信息,功劳就在于这个属性。
BootstrapMethods属性
这个属性用于保存invokedynamic指令引用的引导方法限定符。该指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。
参考
1、周志明,深入理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社
更多java相关内容感兴趣的读者可查看本站专题:《Java面向对象程序设计入门与进阶教程》、《Java数据结构与算法教程》、《Java操作DOM节点技巧总结》、《Java文件与目录操作技巧汇总》和《Java缓存操作技巧汇总》
希望本文所述对大家java程序设计有所帮助。