当JVM运行Java程序的时候,它会加载对应的class文件,并提取class文件中的信息存放在JVM开辟出来的方法区 内存中。那么这个class文件里面到底有些什么内容呢?
一、class文件内容概述
class文件是由8bits的字节流组成,全部字节构成了15个有意义的项目。这些项目之间没有任何无意义的字节,因此class文件非常紧凑。占据多字节空间的项目按照高位在前的顺序存放。下面我们详细讨论这些项目:
★ magic(魔数) 每个class文件的前4个字节称为魔数,值为0xCAFEBABE。作用在于轻松的辨别class文件与非class文件。
★ minor_version、major_version(次、主版本号) 各占2个字节。随着Java技术的发展,class文件的格式会发生变化。版本号的作用在于使得虚拟机能够认识当前加载class的文件格式。从而准确的提取class文件信息。
★ constant_pool_count 、constance_pool(常量池) 从这里开始的字节组成了常量池 。 存储了诸如符号常量、final常量值、基本数据类型的字面值等内容。JVM会将每一个常量构成一个常量表,每个常量表都有自己的入口地址。而实际上在 JVM会将这些常量表存储在方法区中一块连续的内存空间中,因此class文件会根据常量表在常量池中的位置对其进行索引。比如常量池中的第一个常量表的 索引值就是1,第二个就是2。有的时候常量表A需要常量表B的内容,则在常量表A中会存储常量表B的索引值x。而constant_pool_count 就记录了有多少个常量表,或则所有多少个索引值。实际上,常量池中没有索引值为0的常量表,但这缺失的索引值也被记录在 constant_pool_count中,因此 constant_pool_count等于常量表的数量加1。关于常量池的具体内容,我们会在下面详细讲述,并用一个例子来显示整个class文件的内容。
★ access_flags(访问标志) 占用2个字节。用来表明该class文件中定义的是类还是接口,访问修饰符是public还是缺省。类或接口是否是抽象的。类是否是final的。
★ this_class 占用2个字节。 它是一个对常量池的索引。指向的是常量池中存储类名符号引用的CONSTANT_Class_info常量表(见下面常量池具体结构)。比如 this_class=0x0001。则表示指向常量池中的第一个常量表。通常这个表是指向当前class文件所定义的类名。
★ super_class 占用2个字节 与this_class类似,指向存放当前class文件所定义类的超类名字的索引的 CONSTANT_Class_info常量表。
★ inteface_count、interfaces
interface_count是class文件所定义的类直接实现的接口或父类实现的接口的数量。占2个字节。intefaces包含了对每个接口的
CONSTANT_Class_info常量表的索引。
★fields_count、fields fields_count表明了类中字段的数量 。fields是不同长度的field_info表的序列。这些 field_info表中并不包含超类或父接口继承而来的字段。field_info表展示了一个字段的信息,包括字段的名字,描述符和修饰符。如果该字 段是final的,那么还会展示其常量值。注意,这些信息有些存放在field_info里面,有些则存放在field_info所指向的常量池中。下面 我们阐述一下这个field_info表的格式: access_flags(2byte 访问修饰符) name_index(2byte 存储字段名的常量表在常量池中的索引) description_index(2byte 存储字段的所属类型的常量表在常量池中的索引) attribute_count(2byte 属性表的数量) attribute (属性) 其中attribute是由多个attribute_info组成。而JVM规范定义了字段的三种属性:ConstanceValue、Deprecated和Synthetic。
★method_count、methods 与字段类似,method_count表明类中方法的数量和每个方法的常量表的索引。methods表明了不同长度的method_info表的序列。该表格式如下: access_flags(2byte 访问修饰符) name_index(2byte 存储方法名的常量表在常量池中的索引) description_index(2byte 存储方法的返回类型和参数类型的常量表在常量池中的索引) attribute_count(2byte 属性表的数量) attribute (属性) 其中方法的属性JVM规定了四种:Code,Deprecated,Exceptions,Synthetic。
二、常量池的具体结构 在Java程序中,有很多的东西是永恒的,不会在运行过程中变化。比如一个类的名字,一个类字段的名字/所属类型,一个类方法的名字/返回类型/参数名与所属类型,一个常量,还有在程序中出现的大量的字面值。比如下面小段源码红色显示的东西。 public class ClassTest { private String itemS ="我们 "; private final int itemI =100 ; public void setItemS (String para ){...} } 而这些在JVM解释执行程序的时候是非常重要的。那么编译器将源程序编译成class文件后,会用一部分字节分类存储这些永恒不变的红色东西。而这些字节我们就成为常量池。事实上,只有JVM加载class后,在方法区中为它们开辟了空间才更像一个“池”。
正如上面所示,一个程序中有很多永恒的红色东西。每一个都是常量池中的一个常量表(常量项)。而这些常量表之间又有不同,class文件共有11种常量表,如下所示:
(1) CONSTANT_Utf8 用UTF-8编码方式来表示程序中所有的重要常量字符串。这些字符串包括: ①类或接口的全限定名, ②超类的全限定名,③父接口的全限定名, ④类字段名和所属类型名,⑤类方法名和返回类型名、以及参数名和所属类型名。⑥字符串字面值 表格式: tag(标志1:占1byte) length(字符串所占字节的长度,占2byte) bytes(字符串字节序列)
(2)
CONSTANT_Integer、
CONSTANT_Float、
CONSTANT_Long、
CONSTANT_Double 所有基本数据类型的字面值。比如在程序中出现的1用CONSTANT_Integer表示。3.1415926F用
CONSTANT_Float表示。
表格式: tag bytes(基本数据类型所需使用的字节序列)
(3) CONSTANT_Class 使用符号引用来表示类或接口。我们知道所有类名都以 CONSTANT_Utf8表的形式存储。但是我们并不知道 CONSTANT_Utf8表中哪些字符串是类名,那些是方法名。因此我们必须用一个指向类名字符串的符号引用常量来表明。 表格式: tag name_index(给出表示类或接口名的CONSTANT_Utf8表的索引)
(4) CONSTANT_String 同 CONSTANT_Class,指向包含字符串字面值的 CONSTANT_Utf8表。 表格式: tag string_index(给出表示字符串字面值的CONSTANT_Utf8表的索引)
(5) CONSTANT_Fieldref 、 CONSTANT_Methodref、 CONSTANT_InterfaceMethodref 指向包含该字段或方法所属类名的 CONSTANT_Utf8表,以及指向包含该字段或方法的名字和描述符的 CONSTANT_NameAndType 表 表格式: tag class _index(给出包含所属类名的CONSTANT_Utf8表的索引) name_and_type_index(包含字段名或方法名以及描述符的 CONSTANT_NameAndType表 的索引)
(6) CONSTANT_NameAndType 指向包含字段名或方法名以及描述符的 CONSTANT_Utf8表。 表格式: tag name_index(给出表示字段名或方法名的CONSTANT_Utf8表的索引) type_index(给出表示描述符的CONSTANT_Utf8表的索引)
下面是我将一个源程序编译成class文件后,对文件中的每一个字节的分析,可以更好的理解class文件的内容以及常量池的组成。
三、TestClass.class 文件实例分析
TestClass.class 字节码分析(字节顺序从上到下,从左到右。每个字节用一个0-255的十进制整数表示)
202 254 186 190
-- 魔数
(1) 7 0 2 -- 对类ClassTest的符号引用(7为标志 02指向了常量池的索引2的位置) (2) 1
0 17 104 114 47 116 101 115 116 47 67 108 97 115 115 84 101 115 116
-- 类全限定名hr\test\ClassTest
(4)
1 0 16 106 97 118 97 47 108 97 110 103 47 79 98 106 101 99 116
-- 超类全限定名 java/lang/Object
(12)
4 64 73 15 218
-- 第3个类字段float字面值,占4bytes(3.1415926)
(15)
1 0 4 67 111 100 101
-- Code
0 33
---- access_flag 访问标志 public //
字段
itemI
0 1 ---
属性数量
我们分析上面的字节码例子,不难看出:
蓝色背景的常量池字节码区域: (1) 所有的字面值都是存放在常量池中的。 特别注意的是“我们”这个字符串常量也是在常量池中的。如果一个程序出现多个“我们”,那么常量池中也只会有一个。另外,也正是因为“我们”存放在常量池中,使得一些字符串的==比较变的需要琢磨了。 (2)ClassTest并没有任何显示的父类。但在常量池中,我们发现有Object的符号常量存在。 这也证实了在Java中,任何类都直接或间接继承了Object的,而Object并不需要在代码中显示继承,JVM会帮我们做到这一点。 (3)常量池中有一个隐含参数this的符号常量。即使程序中不存在this,JVM也会悄悄的设置一个这样的对象。
绿色背景的类字段字节码区域: (1)字段PI是浮点型常量,在编译期的字节码中就已经指定好了PI的字面值存储在常量池中的某个索引内 。这一点也证实了Java中的常量在编译期就已经得到了值,在运行过程中是无法改变的。
橙色背景的类方法字节码区域: (1)主方法main是作为ClassTest的类方法存在的,在字节码中main和其他的类方法并没有什么区别。 实际上,我们也确实可以通过ClassTest.main(..)来调用ClassTest中的main方法。
(2)在class文件常量池字节码中有两个比较特别的方法名符号:<clinit>和<init>。其中<clinit>方法是编译器自己生成的,编译器会把类静态变量的直接初始化语句和静态初始化语句块的代码都放到了class文件的<clinit>方法中。而对所有非静态非常量数据域的初始化工作要靠<init>方法来完成。针对每一个类的构造方法,编译器都会产生一个<init>方法。即使是缺省构造器也不例外。 |
|