记录学习《深入理解Java虚拟机》的第六章,对一个简单程序的class文件进行整个分析
Class类文件的结构
Class文件格式采用一种类似于C语言结构体的伪结构体,这种伪结构体只有两种类型:“无符号数”和“表”
“无符号数”:当作结构体中的类型
以u1,u2,u4,u8以代表1,2,4,8字节这些是伪结构体中的基础类型
“表”:
表的结构体中会有cp_info,field_info等的类型,这些也是一个伪结构体他们分别对应的是常量表(cp_info),字段表(field_info),方法表(method_info),属性表(attribute_info)。
我们执行一个程序去分析一个简单程序的class文件
class文件我是用BZ.exe来分析的
0xCAFEBABE 0x000000~3 <magic> jvm识别class文件的标准它是固定值
0x0000 0x000004~5 <minor_version> jdk次版本的显示 从jdk1.2后就没有了一直会被设置为0
0x0040 0x000006~7 <major_version> jdk主版本号 十进制为64 为jdk20版本
0x0013 0x000008~9 <constant_pool_count> 常量池的常量数,0x0013转为十进制为19,这表示池中有18个常量,索引值为1~18,0不为索引值,0是作为特殊考虑它表达“不引用如何一个常量池项目”的含义。
0x00000A~A8 <constant_pool> 常量池,在前面已经知道了有18个常量,在0x00000A位置中的值是[0x0A]对应十进制为10,从常量数据表中(cp_info)找10的tag为CONSTANT_Methodref_info,也就是说在0x00000A~E都是CONSTANT_Methodref_info类型的范围,第一个index值为2指向了当前class文件常量池中的第二个常量,第二个index值为3指向了当前常量池的第三个常量,在地址0x00000F就为当前class文件常量池的第二个常量为CONSTANT_Class_info类型,它的index是指向当前常量池的第四个常量......依次往下推。当到第四个常量时是为CONSTANT_Utf8_info类型在地址0x000017,它的bytes就是一个类名,在class文件中长度为16个字节。注:在class文件中的UTF-8的编码不同,它使用的是UTF-8缩略编码表示的字符
我们可以使用jdk工具里的javap来查看class文件的常量:javap -verbose "class文件"
可以看到和前面分析的一毛一样。
0x0000A9~AA <access_flags> 方法标志,用于识别一些类或者接口的层次的访问信息,比如这个Class是类还是接口;是否为public类或者abstract类型;有木有被final修饰等。有个表可以有对照,<access_flags>值为 0x0021 ACC_PUBLIC,ACC_SUPER
0x0000AB~AC <this_class> 类索引用于确定这个类的全限名且是在常量池中寻找 值是8对应常量池中的第8个常量:org/example/TestClass
0x0000AD~AE <super_class> 父类索引引用于确定这个类的父类的全限名,在java中每个类只能有一个父类这个位置不会为空,只有在Object类才会为空 值是2对应第2常量为:java/lang/Object【这里有个疑问当像JRuy或者Jython拥有“多继承”语言时它这里会有什么效果,据我网络查阅说是当JRuy或Jython中的代码涉及到多重继承时,它们会使用Java字节码的接口(interface)机制来模拟多重继承。每个接口都会被编译为一个独立的字节码文件,并且类可以实现多个接口。”具体没有真实的分析过 有兴趣的话可以试一试】
0x0000AF~B0 <interfaces_count> 值为0 接口的数量。由于接口数量为0 <interfaces>不会占用字节。
0x0000B1~B2 <fields_count> “字段”的数量,“字段”的定义是包括类级变量(全局)以及实例级变量(全局性类的实例),但不包括方法内部的局部变量(包括类的实例) 值为1【呃...可以看作存在栈中的不会被看作为“字段”,只有在堆中才会。哈哈哈 栈中变量气抖冷】,“字段”数值为1
0x0000B3~BA <fields> 字段 对应着字段信息表和字段访问符0x0000B3~B4为access_flags=0x0002,字段为private【带有<>里面是class伪结构体子元素,不带有的是表里的子元素 如access_flags是表里的子元素】。0x0000B5~B6和0x0000B7~B8为name_index和descriptor_index他们两的值是常量池的索引name_index=当前常量池的第11个(m 是字段的名字),descriptor_index=当前第12个(I 是字段的类型)
字段类型描述符的参照:B(byte),J(long),C(char),S(short),D(double),Z(boolean),F(float),V(void),I(int),L(对象类型,如Ljava/lang/Object;Ljava/lang/String;Lorg/example/TestClass;),[B(byte[]),[[B(byte[][])以此类推......
0x0000B9~BA attributes_count 【这些属性容易搞混要 注意点,现在这个是字段表里面的attributes_count】 值为0 因为attributes_count为0 attributes也为0
那什么是attributes呢?attributes为属性,在类,字段,方法伪结构体中都会有,在这类,字段,方法中都会有携带自己的属性以描述自己,比如方法里的(code)属性有java编译的代码【在java中代码只会在方法属性中java代码会以字节码的形式在存在方法的code属性,但这可能就会有疑问了如果是代码块呢,代码块不是一个方法而且代码块可出现在方法里外那代码快到代码会放入哪里?在Java的class文件中,代码块不会被作为独立的结构来描述,而是与包含他它们的方法相关联,当如果代码块是在方法里面申请的那好说它字节码会与于当前方法的字节码合并在一起;当代码块是在方法外面那先看当前代码块是否被static修饰,如果是那这个代码块会被称为静态构造代码块它会与静态构造方法的字符串进行合并,如果不是就是构造代码块它会于构造方法合并在一起】
在比如类中可能会有内部类,这个内部类就可以当做一个属性为(innerClasses)内部类列表
每个属性都是按照属性表结构,也就是说每个属性表都会包含上面属性表结构
在《Java虚拟机规范》中可以寻找当前定义的预定义属性,在规范中的4.7解释在jdk20中预定义属性已经有30项了。
0x0000BB~BC <methods_count> 值为2,说明在中这个class文件中有两个方法【一个我们创建的inc和<init>方法】
0x0000BD~114 <methods>这是第一个方法(上面是有两个方法嘛) 方法和字段差不多,表结构是一样的 对着上面两个表 。
0x0000BD~BE access_flags 值为1代表次方法为当前方法被public。0x0000BF~C0 name_index值为5,指向常量池的第5个为“<init>”。
0x0000C1~C2 descriptor_index值为6,第六个常量池为()V,方法的返回类型和字段差不多的()V翻译过来就是无参无返回,加上一些例子就可以很好理解了:void xxx() 可以描述为name_index=xxx这是方法的名字,descriptor_index=()V为方法的返回类型。对于其他的【结合字段里的对基础类型来看】,简单一点的 int xxx(char c1,int[] is1),名字和上面的一样,descriptor_index=(C[I)I ;复杂一点的 String[] xxx(int i1,int[] is1,String[][] s2),名字和上面一样,descriptor_index=(I[I[[Ljava/lang/String)[Ljava/lang/String
和上面显示的合起来:public void <init>()
0x0000C3~C4 attributes_count 值为1说明在当前方法里一个属性,后进入了属性表,0x0000C5~C6 attribute_name_inden这是属性表名称索引 为12,在常量池第十二个常量为“code”,也就是说这是一个code属性表
在code属性表中attribute_name_index已经看过了,接下来0x0000C7~CA attribute_length 这个是整个code属性表的长度,由于属性名称于属性表长度,这个长度它是不包含attribute_name_index和attribute_length的少了六个字节。0x0000CB~CC max_stack代表了当前方法最大栈深度,也就是说在执行方法的时候操作数栈不会超过这个值为1。0x0000CD~CE max_locals 这代表了局部变量表所需的空间(就是存局部变量的空间)在这个空间中它是以糟(slot)为最小单位,在从多基础类型中超32位的类型为2糟比如long和double,其他都是1糟在地址中值为1说明当前方法的局部变量表为1糟 [注意1糟不能说明在此方法中只有一个局部变量,在Javac编译器会根据变量的作用域来分配变量糟给各个变量使用,根据同时生存的最大局部变量数量和类型计算出max_locals(这个始终都会有1糟因为即使你在方法中没有定义局部变量也会有一个“this”,"this"访问当前对象,它是一个局部变量)【1糟也说明了当前方法里没有对象和long,double类型的局部变量】] 。0x0000CF~D2 code_length 表示了当前方法里代码的字符码的数量,方法里代码转换的字节码在jvm看来是指令是以一个字节为单位的指令【我感觉像操作系统中的中断表】值为5 说明有5个指令,这些指令在《java虚拟机规范》现在有约200个指令(以一个字节为单位的,8bit,最多只有256个指令)。0x0000D3~D7 code 这个就是存储指令咯,本章最重要的 值为“2A B7 00 01 B1”得去对照这200条指令看虚拟机到底执行了什么。0x0000D8~D9 exception_table_length 这是当前方法里的异常数,当前异常数为0,exception_table就没有了【在简单程序中没有设置异常】。0x0000DA~DB attributes_count 属性数,值为1在当前code表中有一条属性。0x0000DC~DE 值为14,查看常量池,噢?LineNumberTable属性,这个属性是查看方法代码的行号与字节码的对应关系的且这个属性只有code表会有(这个LineNumberTable属性不是运行时必须的可以在运行java时设置-g:none or -g:lines来取消这个)
从0x0000DC~0x0000E7是LineNumberTable属性的范围。
0x0000E8~0x000114为描述第二个方法区域,和第一个没什么差别主要就是max_stack比第一个方法多了一个应该是应用了一个常量“1”,然后它的代码方法转换的字节码为7个比第一个多了两个(关于字节码该看还是得看的去对照那200个指令)。
最后到类伪结构体的属性了
0x000115~0x000116 <attributes_count> 值为1,说明类有一个属性。
0x000117~0x000118 值为17 从常量池中寻找第17个看到是SourceFile属性,它是记录源文件名称的从它为结构体看
0x000117~0x00011E 是它的范围,直接看最后面两个字节值是18,从常量池中寻找。噢~ TestClass.java 你好类名。