深入理解Java虚拟机(三)——Class文件结构

本文介绍了Java平台无关性得益于JVM和字节码。详细阐述了Class类文件结构,包括魔数、文件版本、常量池、访问标志、类索引等内容,还对Class文件进行了解析,如使用工具查看、分析各部分数据含义,最后用javap命令打印详细信息。

1 概述

  1. Java很重要的特点是平台无关性,即用Java语言编写的程序可以在不同平台之间无缝迁移,在Java诞生之初,有一个著名的宣传口号:“一次编写,到处运行(Write Once,Run AnyWhere)”。Java能够实现平台无关性的原因是它在平台之上提供了一个Java运行环境,也就是JVM,各种不同平台上的虚拟机都统一使用的程序存储结构——字节码,是构成平台无关系的基石。
    Java 虚拟机提供的语言无关性

2 Class 类文件的结构

  1. Class文件是一组以8位字节位基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没人任何分隔符,整个Class文件存储的内容几乎是全部程序运行的必要数据,没有空隙存在。
  2. 当遇到8个字节以上的空间数据项时,会按照高位在前的方式分割成若干个8位字节进行存储。
  3. Class文件只有两种数据类型,无符号数和表。
  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来表示1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
  • 是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,如下表所示的数据项构成。
类型名称数量描述
u4magic1魔数,表明当前文件是.class文件,固定0xCAFEBABE
u2minor_version1Class文件的次版本号
u2major_version1Class文件主版本号
u2constant_pool_count1常量池计数
cp_infoconstant_poolconstant_pool_count-1常量池内容
u2access_flags1类访问标识
u2this_class1当前类
u2super_class1父类
u2interfaces_count1实现的接口类数量
u2interfacesinterfaces_count实现接口类信息
u2fields_count1字段数量
field_infofieldsfields_count包含的字段信息
u2methods_count1方法数量
method_infomethodsmethods_count包含的方法信息
u2attributes_count1属性数量
attribute_infoattributesattributes_count各种属性

Class文件解析:
将以下代码使用jdk1.8进行编译,得到class文件,通过 Binary Vivewer 软件打开进行查看解析,或者用其它二进制软件打开文件。

// 测试代码1:
public class TestClass1{
	private int m;
	
	public int inc(){
		return m +1;
	}
	
}

打开结果:
在这里插入图片描述
2.1 魔数

 u4		magic
  • Class文件的头4个字节称为魔数,唯一作用就是让虚拟机接受这个 Class 文件,Class 的魔数为:0xCAFEBABY
    在这里插入图片描述

2.2 文件版本

 u2		minor_version	//次版本号
 u2		major_version	//主版本号

在这里插入图片描述
次版本号占2个字节:0x0000
主版本号占4个字节:0x0034(十六进制) = 52(十进制)

jdk 对应主版本号:
	JDK 1.8 = 52 
	JDK 1.7 = 51 
	JDK 1.6 = 50 
	JDK 1.5 = 49 
	JDK 1.4 = 48 
	JDK 1.3 = 47 
	JDK 1.2 = 46 
	JDK 1.1 = 45

2.3 常量池

  • 常量池可以理解为 Class 文件之中的资源仓库,是 Class 文件结构中与其它相关联最多的数据类型,也是占用 Class 文件空间最大的数据项之一。
//表类型数据项
 u2			constant_pool_count
 cp_info 	constant_pool[constant_pool_count-1]

constant_pool_count:常量池的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值 (计数从1开始)
在这里插入图片描述
常量池数量:0x0013(十六进制) = 19(十进制),说明有18个常量
常量池内容:存放两大常量:字面量和符号引用

  • 字面量:文本字符串、生命的final常量等
  • 符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
//常量池的表结构
cp_info{
	u1 tag;
	u1 info[];
}

tag 标志位取值:

类型标志描述
CONSTANT_utf8_info1UTF-8编码的字符串
CONSTANT_Integer_info3整形字面量
CONSTANT_Float_info4浮点型字面量
CONSTANT_Long_info长整型字面量
CONSTANT_Double_info双精度浮点型字面量
CONSTANT_Class_info类或接口的符号引用
CONSTANT_String_info字符串类型字面量
CONSTANT_Fieldref_info字段的符号引用
CONSTANT_Methodref_info10类中方法的符号引用
CONSTANT_InterfaceMethodref_info11接口中方法的符号引用
CONSTANT_NameAndType_info12字段或方法的符号引用
CONSTANT_MethodHandle_info15表示方法句柄
CONSTANT_MothodType_info16标志方法类型
CONSTANT_InvokeDynamic_info18表示一个动态方法调用点

常量池中的14种常量项的结构总表:
在这里插入图片描述
具体表结构描述如下:跳过描述

  1. CONSTANT_Utf8_info
CONSTANT_Utf8_info {
    u1 tag;	
    u2 length;
    u1 bytes[length]; 
}
  • tag:CONSTANT_Utf8(1)。
  • length:length项的值指明了bytes[]数组的长度(注意,不能等同于当前结构所表示的String对象的长度),CONSTANT_Utf8_info结构中的内容是以length属性确定长度而不是以null作为字符串的终结符。
  • bytes[]:bytes[]是表示字符串值的byte数组,bytes[]数组中每个成员的byte值都不会是0,也不在0xf0至0xff范围内。
  1. CONSTANT_Integer_infoCONSTANT_Float_info
// 表示4字节(int和float)的数值常量
CONSTANT_Integer_info {
    u1 tag; 
    u4 bytes; 
} 
CONSTANT_Float_info { 
    u1 tag; 
    u4 bytes;
}
  • tag:
     CONSTANT_Integer_info结构的tag项的值是CONSTANT_Integer(3)。
     CONSTANT_Float_info结构的tag项的值是CONSTANT_Float(4)。
  • bytes:
     CONSTANT_Integer_info结构的bytes项表示int常量的值,按照Big-Endian的顺序存储。
     CONSTANT_Float_info结构的bytes项按照IEEE 754单精度浮点格式表示float常量的值,按照Big-Endian的顺序存储。
  1. CONSTANT_Long_infoCONSTANT_Double_info
// 表示8字节(long和double)的数值常量
CONSTANT_Long_info {
    u1 tag; 
    u4 high_bytes; 
    u4 low_bytes; 
} 

CONSTANT_Double_info { 
    u1 tag; 
    u4 high_bytes; 
    u4 low_bytes; 
}
  • tag:
     CONSTANT_Long_info结构的tag项的值是CONSTANT_Long(5)。
     CONSTANT_Double_info结构的tag项的值是CONSTANT_Double(6)。
  • high_bytes和low_bytes
     CONSTANT_Long_info结构中的无符号的high_bytes和low_bytes项用于共同表示long型常量,构造形式为((long) high_bytes << 32) + low_bytes,high_bytes和low_bytes都按照Big-Endian顺序存储。
     CONSTANT_Double_info结构中的high_bytes和low_bytes共同按照IEEE 754双精度浮点格式表示double常量的值。high_bytes和low_bytes都按照Big-Endian顺序存储。
  1. CONSTANT_Class_info
// 用于表示类或接口
CONSTANT_Class_info {
    u1 tag; 
    u2 name_index;
}
  • tag:CONSTANT_Class_info结构的tag项的值为CONSTANT_Class(7)
  • name_index:name_index项的值,必须是对常量池的一个有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,代表一个有效的类或接口二进制名称的内部形式。
  1. CONSTANT_String_info
// 用于表示java.lang.String类型的常量对象
CONSTANT_Class_info {
    u1 tag; 
    u2 name_index;
}
  • tag:CONSTANT_String_info结构的tag项的值为CONSTANT_String(8)。
  • string_index:string_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info 结构,表示一组Unicode码点序列,这组Unicode码点序列最终会被初始化为一个String对象。
  1. CONSTANT_Fieldref_infoCONSTANT_Methodref_infoCONSTANT_InterfaceMethodref_info
// 字段,方法,接口方法具有相同的类型
CONSTANT_Fieldref_info {
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}
CONSTANT_Methodref_info { 
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}
CONSTANT_InterfaceMethodref_info {
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}
  • tag:
     CONSTANT_Fieldref_info结构的tag项的值为CONSTANT_Fieldref(9)。
     CONSTANT_Methodref_info结构的tag项的值为CONSTANT_Methodref(10)。
     CONSTANT_InterfaceMethodref_info结构的tag项的值为CONSTANT_InterfaceMethodref(11)。
  • class_index:
     class_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Class_info结构,表示一个类或接口,当前字段或方法是这个类或接口的成员。
     CONSTANT_Methodref_info结构的class_index项的类型必须是类(不能是接口)
     CONSTANT_InterfaceMethodref_info结构的class_index项的类型必须是接口(不能是类)。
     CONSTANT_Fieldref_info结构的class_index项的类型既可以是类也可以是接口。
  • name_and_type_index:
     name_and_type_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,它表示当前字段或方法的名字和描述符。
     CONSTANT_Fieldref_info结构中,描述符必须是字段描述符。CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info中描述符必须是方法描述符。
  1. CONSTANT_NameAndType_info
// 用于表示字段或方法
CONSTANT_NameAndType_info { 
    u1 tag; 
    u2 name_index; 
    u2 descriptor_index;
}
  • tag:CONSTANT_NameAndType(12)。
  • name_index:必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,这个结构要么表示特殊的方法名,要么表示一个有效的字段或方法的非限定名(Unqualified Name)。
  • descriptor_index:必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,这个结构表示一个有效的字段描述符或方法描述符。
  1. CONSTANT_MethodHandle_info
// 用于表示方法句柄
CONSTANT_MethodHandle_info {
    u1 tag;
    u1 reference_kind;
    u2 reference_index;
}
  • tag:CONSTANT_MethodHandle(15)。
  • reference_kind:必须在1至9之间(包括1和9),它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为(Bytecode Behavior)。
  • reference_index:
    reference_index项的值必须是对常量池的有效索引:
     (1)如果reference_kind项的值为1(REF_getField)、2(REF_getStatic)、3(REF_putField)或4(REF_putStatic),那么常量池在reference_index索引处的项必须是CONSTANT_Fieldref_info结构,表示由一个字段创建的方法句柄。
     (2)如果reference_kind项的值是5(REF_invokeVirtual)、6(REF_invokeStatic)、7(REF_invokeSpecial)或8(REF_newInvokeSpecial),那么常量池在reference_index索引处的项必须是CONSTANT_Methodref_info结构,表示由类的方法或构造函数创建的方法句柄。
     (3)如果reference_kind项的值是9(REF_invokeInterface),那么常量池在reference_index索引处的项必须是CONSTANT_InterfaceMethodref_info结构,表示由接口方法创建的方法句柄。
     (4)如果reference_kind项的值是5(REF_invokeVirtual)、6(REF_invokeStatic)、7(REF_invokeSpecial)或9(REF_invokeInterface),那么方法句柄对应的方法不能为实例初始化()方法或类初始化方法()。
     (5)如果reference_kind项的值是8(REF_newInvokeSpecial),那么方法句柄对应的方法必须为实例初始化()方法。
  1. CONSTANT_MethodType_info
// 用于表示方法类型
CONSTANT_MethodType_info { 
    u1 tag; 
    u2 descriptor_index; 
}
  • tag:CONSTANT_MethodType(16)。
  • descriptor_index:必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符。
  1. CONSTANT_InvokeDynamic_info
// 表示invokedynamic指令所使用到的引导方法(Bootstrap Method)、引导方法使用到动态调用名称(Dynamic Invocation Name)、参数和请求返回类型、以及可以选择性的附加被称为静态参数(Static Arguments)的常量序列。
CONSTANT_InvokeDynamic_info { 
    u1 tag; 
    u2 bootstrap_method_attr_index; 
    u2 name_and_type_index; 
}
  • tag:ONSTANT_InvokeDynamic(18)。
  • bootstrap_method_attr_index:对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引。
  • name_and_type_index:对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info 结构,表示方法名和方法描述符。

了解了以上tag取值及表结构描述,我们继续解析。
在这里插入图片描述
常量池数量后跟的为具体的常量池信息,
第一位字节为tag,0x0A(十进制为10),根据 tag 取值表查到了
在这里插入图片描述
第一个常量表 CONSTANT_Methodref_info,对应两个索引,各占两个字节0x0004 #4,0x000F #15,表示对应了具体位置在第4和第15个常量表位置。
在这里插入图片描述
再接着是一个字节的tag,0x0009对应9,找到
第二个常量表 CONSTANT_Fieldref_info,对应两个索引,各占两个字节 0x0003 #3,0x0010 #16,表示对应了具体位置在第3和第16个常量表位置。
再接着是一个字节的tag,0x0007对应7,找到
第三个常量表 CONSTANT_Class_info,对应了一个索引,占了两个字节 0x0011 #17,表示对应了具体位置在第17个常量表位置。
再接着是一个字节的tag,0x0007对应7,找到
第四个常量表 CONSTANT_Class_info,对应了一个索引,占了两个字节 0x0012 #18,表示对应了具体位置在第18个常量表位置。
再接着是一个字节的tag,0x0001对应1,找到
第五个常量表 CONSTANT_Utf8_info,表示一个UTF-8的字符串,两个字节表示字符串的
长度, 0x0001 表示字符串长度为1,后面跟着的 1 个字节为字符串内容,0x006D 109表示 ASCII 的m。

不断的向下找,一共有 18 个这样的结构。这里借助 Java 提供的命令帮助我们读取:
javap -verbose class文件
在这里插入图片描述
可以看到,我们刚刚读取到的五个都在命令行中显示了,这里根据命令行也得到了总共有18个常量,分别对应的常量表的位置,最后一个常量就是 #18 在对应的 java/lang/Object 结束,对应了字节码中 t 结尾,到这里我们的字节码中常量池部分就解析完成啦。

2.4 访问标志

 u2		access_flags	
  • 访问标志为两个字节,识别类或者接口层次的访问信息,包括:Class文件时类还是接口;是否定义为 public 类型;如果是类,是否声明为 final 等。具体如下表:
标志名称标志值含义针对的对像
ACC_PUBLIC0x0001是否为public类型所有类型
ACC_FINAL0x0010是否为声明为final
ACC_SUPER0x0020使用新的invokespecial语义类和接口
ACC_INTERFACE0x0200标识是一个接口类型接口
ACC_ABSTRACT0x0400是否为抽象类型接口或抽象类,此标识符为真,其他类型为假
ACC_SYNTHETIC0x1000该类不由用户代码生成所有类型
ACC_ANNOTATION0x2000注解类型注解
ACC_ENUM0x4000枚举类型枚举

access_flags 的值为上表中或关系的集合。
在这里插入图片描述
根据访问标志的定义,可以找到下两个字节为 0x0021,根据表中比对 0x0021 是 0x0001 | 0x0020,因此可以读出是 public class类。这里我更改一次测试代码,在练习一次:

// 测试代码2:
public interface TestClass2{
	public int inc();	
}

在这里插入图片描述
这里 0x0601,就是 0x0001 | 0x0200 | 0x0400 ,及 publlic interface abstract 的标识。

2.5 类索引、父类索引与接口索引集合

 u2		this_class;
 u2		super_class; 
 u2		interfaces_count;
 u2		interfaces[interfaces_count];
  • 类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。
  1. 类索引:确定这个类的全限定名,
  2. 父类索引:确定这个类的父类的全限定名,由于Java语言不允许多重继承,所以父类索引,除了 java.lang.Object 之外,所有的Java类至少有一个父类。
  3. 接口索引集合:确定类实现了哪些接口。

类索引和父类索引均由 u2 类型的索引值表示,各自指向一个类型为 CONSTANT_Class_info 的类描述符常量,通过 CONSTANT_Class_info 类型的常量中的索引值可以确定定义在 CONSTANT_Utf8_info 类型的常量中的全限定名字符串。
接口索引集合,因为引用接口数量不一定,第一项为 u2 类型的接口计数器,表示索引表的容量,如果没有实现接口,则计数器为0,后面不在占用任何字节。

在这里插入图片描述
可以读到 TestClass1 中类索引为 0x0003 #3, 0x0004 #4,0x0000 即没有接口,数量为0。
在这里插入图片描述
利用 Javap 也可以查看到,#3 代表常量池中 TestClass1 类的全限名,#4 为父类 java.lang.Object。
此时,我们对 TestClass1.class 的类索引、父类索引与接口索引集合已经解析完成了。

2.6 字段表集合

  • 字段表用于描述接口或类中生命的变量。字段包括类级变量和实例级变量,不包括方法内部声明的局部变量。
 u2		fields_count; //字段的数量
 field_info { //字段表
    u2             access_flags;  
    u2             name_index; 
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

field_info 具体结构如下:

  • access_flags:访问标识符
标志描述
ACC_PUBLIC0x0001public 访问修饰符
ACC_PRIVATE0x0002private 访问修饰符
ACC_PROTECTED0x0004protected 访问修饰符
ACC_STATIC0x0008static
ACC_FINAL0x0010final
ACC_VOLATILE0x0040valatile
ACC_TRANSIENT0x0080transient
ACC_SYNTHETIC0x1000编译器自动产生
ACC_ENUM0x4000enum

注:实际情况中,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED 三个标识符最多只有一项,ACC_FINAL、ACC_VOLATILE 不能同时选择,接口中字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL 标志。

  • name_index:引用的常量池,描述字段的简单名称。简单名称是指没有类型或参数修饰的方法或者字段名称。
  • descriptor_index:引用的常量池,描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写V字符来表示,而对象类型则用字符L加对象的全限定名来表示。如下表:
标识字符含义
B基本类型byte
C基本类型char
D基本类型double
F基本类型float
I基本类型int
J基本类型long
S基本类型short
Z基本类型boolean
V特殊类型void
L对象类型,如Ljava/lang/Object;
[数组类型,多个维度则有多个[

  用描述符来描述方法时,按照先参数列表后返回值的顺序描述,参数列表按照参数的严格顺序放在一组 “()” 之内。如方法 void inc() 的描述符为 “()V”,方法 java.lang.String.toString() 的描述符为 “()Ljava/lang/String;”。

接下来看我们继续解析 TestClass1.class 的字段 m 。如下图所示:
在这里插入图片描述
注:字段表集合中不会列出从超类或者父类接口继承而来的字段;Java 中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,字段名称都必须不一样,但对于字节码来说,如果两个字段的描述符不一致,那字段重名就是合法的。

2.6 方法表集合

 u2		methods_count; //方法的数量
method_info { //方法表
    u2             access_flags;  
    u2             name_index; 
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

method_info 具体结构如下:

  • access_flags:访问标识符
标志名称标志值含义
ACC_PUBLIC0x0001是否为Public类型
ACC_PRIVATE0x0002是否为private类型
ACC_PROTECTED0x0004是否为PROTECTED类型
ACC_STATIC0x0008static
ACC_FINAL0x00 10是否被声明为final,只有类可以设置
ACC_SYNCHRONIZED0x0020synchronized
ACC_INTERFACE0x0200标志这是一个接口
ACC_BRIDGE0x0040方法是否由编译器产生的桥接方法
ACC_VARARGS0x0080方法是否接受不定参数
ACC_NITIVE0x0100方法是否为native
ACC_ABSTRACT0x0400方法是否为abstract
ACC_STRICTFP0x0800方法是否为strictfp
ACC_SYNTHETIC0x1000方法是否由编译器自动产生

接下来继续分析 TestClass1.class 文件,如下图:
在这里插入图片描述
这里可以看到,我们分析得到一个 public void init() 的方法。接下来在学习了属性表集合后,在解析后面的内容。

2.7 属性表集合

  • 在 Class 文件、字段表、方法表都可以携带自己的属性表集合,用于描述专有信息。
 u2		attributes_count; //属性的数量
attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

虚拟机规范预定义的属性:

属性名称使用位置含义
Code方法表Java代码编译成的字节码指令
ConstantValue字段表final关键字定义的常量池
Deprecated类,方法,字段表被声明为deprecated的方法和字段
Exceptions方法表方法抛出的异常
EnclosingMethod类文件仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClass类文件内部类列表
LineNumberTableCode属性Java源码的行号与字节码指令的对应关系
LocalVariableTableCode属性方法的局部便狼描述
StackMapTableCode属性供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配
Signature类,方法表,字段表用于支持泛型情况下的方法签名
SourceFile类文件记录源文件名称
SourceDebugExtension类文件用于存储额外的调试信息
Synthetic类,方法表,字段表标志方法或字段为编译器自动生成的
LocalVariableTypeTable使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations类,方法表,字段表为动态注解提供支持
RuntimeInvisibleAnnotations表,方法表,字段表用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotation方法表作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法
RuntimeInvisibleParameterAnnotation方法表作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数
AnnotationDefault方法表用于记录注解类元素的默认值
BootstrapMethods类文件用于保存invokeddynamic指令引用的引导方式限定符

对于每个属性,它的名称需要从常量池中引用一个CONSTANT_utf8_info类型的常量类表示,而属性值的结构则是完全自定义的,只需要通过一个 u4 的长度属性区说明属性值所占用的位数即可。
属性表定义的结构:

类型名称数量
u2attribute_name_index1
u2attribute_length1
u1infoattribute_length
  1. Code 属性
  • Java程序方法体中的代码经过 Javac 编译器处理后,最终变为字节码指令存储在 Code 属性内。Code 属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在 Code 属性。

Code属性表结构:

类型名称数量描述
u2attribute_name_index1attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为Code,它代表了该属性的属性名称
u4attribute_length1attribute_length指示了属性值的长度
u2max_stack1max_stack代表了操作数栈(Operand Stacks)深度的最大值。虚拟机运行的时候需要根据这个值来分配栈帧(StackFrame)中的操作栈深度。
u2max_locals1max_locals代表了局部变量表所需的存储空间
u4code_length1code_length代表字节码长度;理论上一个方法的字节码不超过u4,但实际是u2,如果超过这个限制,javac会拒绝
u1codecode_lengthcode是用于存储字节码指令的一系列字节流
u2exception_table_length1异常表长度
exception_infoexception_tableexception_table_length
u2attributes_count1属性数量
attribute_infoattributesattributes_count

了解了Code属性,我们继续完善上一节未读的 init 方法属性,
在这里插入图片描述
注意: code 的内容可以看到 2A B7 00 01 B1对应相应的指令。

  • 读入2a,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
  • 读入b7,查表得0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用。
  • 读入00 01,这是invokespecial的参数,查常量池得0x0001对应的常量为实例构造器<init>方法的符号引用。
  • 读入b1,查表得0xB1对应的指令为return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束。

到紧接着后面为 0x000A #10 指向的为 LineNumberTable 表,这里介绍两个概念。

  • LineNumberTable:用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。
LineNumberTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 line_number_table_length;
    {   u2 start_pc;
        u2 line_number; 
    } line_number_table[line_number_table_length];
}
  • LocalVariableTable:属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。
LocalVariableTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 local_variable_table_length;
    {   u2 start_pc;
        u2 length;
        u2 name_index;
        u2 descriptor_index;
        u2 index;
    } local_variable_table[local_variable_table_length];
}

至此,剩余的字节信息可以逐步分析,直至解析完成。最后,我们用 javap -verbose 直接打印出详细信息。
在这里插入图片描述
可以看到,总共有两个方法,init() 和 inc(),其中 init() 方法,stack=1,locals=1,args_size=1,代表栈中元素有1个,局部变量表中元素有1个,参数有1个。

init() 是无参构造方法,inc() 也是无参方法为什么args_size=1?

在任何实例方法里面,都可以通过this关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现却非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。这个处理只对实例方法有效,如果 inc() 声明为static,那args_size就不会等于1而是等于0了。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值