您的位置:首页 > 编程语言 > Java开发

Java虚拟机学习笔记—class文件的分析

2014-05-05 22:22 218 查看
Class 文件是一组以8为字节为基本单位的二进制流——流的意义在于说明class文件的所有数据条目都是紧凑地排列在一起,中间并不会存在任何空隙。java虚拟机定义了class的文件结构,这种统一的文件结构使得任何能将代码编译成class文件的语言能够在java虚拟机上运行,也就是说java虚拟机能支持的不仅仅是java,例如scala。

根据java虚拟机规范的规定,class文件只有两种结构(伪):无符号数和表,其中无符号数属于基本的数据类型,我们以u1、u2、u4、u8来下文出现的类型所占用对应的字节数(1,2,4,8个字节)。这些无符号数可以用来表示数值、索引引用、数量值、或者按照UTF-8编码构成的字符串值。

我们先来整体上观察一下整个class文件:



class的结构并不像其他XML等描述语言,每个数据项之间并没有任何分割符号,这就要求无论是顺序还是数量都是被限定的,上面class结构中数量部分不是为1的,会在入口的地方告诉你,具体的值如constant_pool_count等;这就能够使你精确定位到每个数据项。

为了让大家直观上看到class文件结构 ,我先给出个简单的小java程序以及对应的class文件:

package com.language.test;

public class Test {
private int field;

public int getField(){
return field;
}
}




根据我们给出的class的文件格式,我们知道class文件 中前四个字节(u4代表四个字节)表示魔数,从上面程序中的class文件我们看到,这个所谓的魔数的值为 CA FE BA BE 也就是咖啡了,从这里大家也许就猜到了这就是java使用一杯咖啡来作为图标的一个缘由。让我们继续看看接下来有u2个字节用来表示java的次版本号和u2个字节来表示java的主版本号,而我所用的环境我们可以看到是次版本为00 00 主版本为 00 32
,也就是主版本为50(32为16进制),java的版本号是从45开始的,50 代表这个文件可以被1.6以上的JRE环境运行,因为java是向下兼容。

继续我们的class文件分析之旅,我们可以看到接下来就到了常量池的地方,这是个非常重要的东西,对我们以后理解和分析java的执行都具有很重要的意义。刚刚我们说到,对于不固定长度的数据项,我们需要一个计数器来告诉我们具体这个数据项的条目,常量池就是我们遇到的第一个这样的数据项。



常量池的入口是一项u2类型(2字节,下文我就不解释u2 , u4.....是什么意思了)的数据代表常量池的数据条目,这里我们看到是00 16,也就是十进制的22,代表这个常量池具备21项的数据条目,索引值为1~21.将第0项常量空出来是有特殊考虑的, 这样做的目的在于满足后面某些数据项指向常量池的索引值的那些数据在特殊情况下需要表达“ 没有引用任何一个常量池条目”的意思。

常量池中主要有两大类常量:字面量和符号引用。字面量接近Java语言层面的常量概念,如一些字符串、一些final的常量值。而符号引用就属于编译原理的概念,主要有

1.类和接口的全限定名

2.字段的名称和描述符

3.方法的名称和描述符

java代码在javac编译的时候,并不像C或者C++那样有“链接”的步骤,而是在虚拟机加载类文件的时候动态链接。也就是说各个方法和字段的最终内存布局信息不实保存在类文件中,它们需要经过转换才可以被虚拟机使用。虚拟机运行时,需要从常量池中获取对应的符号引用,再在类创建或运行时解析并翻译到具体的内存地址中。

常量池的数据条目共有11种,每个数据条目都有一个u1开头的类型表示这个条目属于哪一种,具体的标志和类型可以查下表,这是一种规范(对应关系是固定的):



每一种类型条目有自己的结构,我们来看看 我们的class文件中的第一个条目:



刚刚我们看到了00 16 也就是常量池的入口,它告诉我们这个常量池有多少数据条目,接下来就是第一个常量池的数据条目了,我们可以看看u1开头表示的第一个条目是哪一种,在这里我们看到为0x07也就是标志为7,查看上面的表格我们知道,这是一个CONSTANT_Class_info条目,知道这个条目是CONSTANT_Class_info后,我们可以查看虚拟机规范去了解它的结构,



我们查看了虚拟机规范,知道了CONSTANT_Class_info的结构后,就可以看到tag是标志位,就是我们看到的u1开头,这个是每个常量池条目第一个数据结构的内容,我们主要看看name_index,CONSTANT_Class_info常量比较简单,就只有这个独特的内容项,它是u2的类型并且它的值代表在常量池中的索引,从class文件中我们看到,第一条条目(刚刚我们查出它是CONSTANT_Class_info)的这个name_index值为00 02,这代表它的值在常量池中第二个条目,这样第一个条目就结束了,我们可以看看第二条目是什么,第二个条目的标志为01,它表明这个条目是一个CONSTANT_Utf8_info的条目,它的结构为,



length代表这个utf-8编码的字符串长度是多少个字节的,后面跟着这字符串的内容(每个字节都是一个bytes,有length个),到这里我们可以看看这个条目的字符串是什么,我们看到length的值(class文件第一行的末尾)为00 16,这表明,字符串长度为22个字节,我们从高亮的22个字节对应的值中看到,这个条目的值为com/language/test/Test;这就可以知道我们第一个条目索引的常量池的第二条目的内容了,也就是说,刚刚我们的CONSTANT_Class_info条目中的值(索引项)——是我们的类的全限定名。



分析了前面2中常量池的条目相信大家也对常量池的分析有了一定的技巧,重复的操作我们就不继续了,下面我们借助一些工具来快速分析我们的这个class文件,Javap命令:



使用javap我们可以很直观的分析常量池,#1 = Class 表示第一个常量池的条目为CONSTANT_Class_info,引用值为第二个条目#2, #2 =Utf8 表示第二个条目的类型是CONSTANT_Utf8_info,值为com/language/test/Test,就跟我们刚刚所分析的一样。到这里我们对常量池的分析就基本结束了,具体的其他类型,大家有兴趣的可以百度查阅他们的结构。

我们继续我们的class之旅,大家估计对class文件的结构有些忘记了,我再一次给出它的结构:



上面我们已经分析到了常量池,接下来就是access_flags(访问标志)、this_class(类索引)、super_class(父类索引)、。。。。

大致如下


对于class文件中不确定数量的数据项,我们之前说过会在入口处加个数量的条目,正如上面图中所表示。有了这个数量我们就可以知道接下来有多少字段,那么对于每个字段我们来看一下它的结构:



也就是说接下有8个字节来描述这个字段外加一个属性信息的结构,对于本例中的字段,它的属性结构的计数器为0,也就是没有需要额外描述的信息,但若将字段field的声明改为“final static int field = 123;那就可能会存在一项名为ConstantValue的属性,其值指向常量123”

注意,字段表集合中不会列出从超类或父接口中继承而来的字段,但有可能列出原本类中不存在的字段,例如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。另外,在java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。

在class文件中对方法的描述与对字段的描述几乎使用了完全一致的方式,描述方法的结构同描述字段的结构是很类似的:



因为上面我们给出的例子中,字段没有属性,因此在描述字段的8个字节后加 一个字节的属性计数器(本例中为00 表明无额外属性)之后就是到了描述方法的结构了,如果有额外属性,那么就应该先接额外的属性再到方法的结构。下面高亮的部分正对应方法结构的各个字节:


读者也许会奇怪 这里的属性计数器为00 01 ,属性名字的索引为00 09 ;查常量池索引为9的 值 我知道这是一个code属性,在这里我们一直没有遇到关于方法的任何代码的信息,实际上代码的信息就是存储在code属性的属性结构中,完整的code属性结构是:



那么总共属性结构支持多少种属性呢,java虚拟机规范给出了下面这些预定义的属性:


属性的结构是完全自定义的,只要满足下面类似结构的就认为是符合规则的:



因为属性的分析要更加不同的属性的结构来一个个进行,不是本文的目的,本文只是想给大家一个对class文件有一个直观上的理解,就不再继续分析下去了,有兴趣的读者可以参考《深入理解java虚拟机》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: