一.概述
类文件即是java文件编译后所形成的.class文件。他是Java无关性的基石,Java语言通过class文件实现了平台无关性和语言无关性。Java虚拟机只和class文件沟通而与操作系统无关实现了平台无关性。而Java虚拟机支持越来越多的编程语言都可以编译成class文件则体现了其语言无关性。当然,不同的语言会使用不同的编译器。
二.Class类文件的结构
一个class文件对应一个类/接口,他是以8字节为单位的二进制流,其中的数据有序排列且非常紧密,没有分隔符,8字节以上的数据则以高位在前的方式分割成若干的8字节单位。
构成class文件的基本数据类型有两种:1.无符号数 2.表
1.无符号数:是基本的数据类型,用来描述数字,索引引用,数量值,UTF-8编码的字符串。
2.表:表是由许多无符号数和其它表构成,可以说class文件也是一张表。
在class文件的编码习惯中,当遇到需要描述同一类型但是数量不知的多个数据是,会使用一个容量计数器+若干个数据项的形式来表示某一类型的集合。
以下图片是class文件的总体结构,u+数字表示占用的字节数,使用16进制表示。
建一个用于举例子的类,再编译。
package com.fightzh;
public class Test
{
private int m;
public int getM(){
return m + 1;
}
}
2.1魔数
魔数的功能是用来确定此文件是否能被虚拟机接受的参数,值为0xCA FE BA BE(咖啡宝贝?),占用4个字节。没有使用.class后缀来确定文件是因为后缀可以随意修改。可以使用winHex打开class文件。
2.2class文件的版本号
接下来4个字节是class文件的版本号,分两部分。前两个字节是次版本号,即小数点后的版本。后两个字节是主版本号,即小数点前的版本号。 虚拟机会执行小于自己版本号的class文件,拒绝执行大于自己版本号的class文件。下图举例,此class文件版本号是52.0,16进制换转化为10进制。
2.3常量池
我们可以把常量池看作是一个仓库,他储存了大量class文件的某些部分(字段表,方法表,属性表)需要用到的信息。常量池主要储存两大类常量:1:字面量,即字符串和final常量等 2符号引用:包括 (类和接口的全限定名,字段的名称和描述符,方法的名称和描述符)。
常量池的结构是2字节的常量计数器+各个常量(每个常量都是一张表)
常量计数器:0号引用表示不引用常量计数器的任何引用,所以常量计数器非常特殊,其引用从1开始。
各个常量:每个常量都是一张有自己结构的表,公共的结构是其开头都是1个字节的类型标志位,表示自己是什么类 型的常量。
举例:0x 00 13是十进制的19,表示有18个常量(0号常量不可用),看第一个常量 类型标志位0x 0A即10,查表,此常量共5个字节,出类型标志位1字节外,2字节指向class_info的索引,以图看来0x 00 04表示指向第四号常量。接下来还有2字节指向类型描述符的引用,值为0x 00 0F即15,指向第15号常量的引用。
我们可以使用专门的工具帮助我们分析class文件的字节码-----javap
可以看到举例的第一个常量,也可看到后续的17个常量。其中有一些不是源码中的常量是编译器自动添加的,以后的字段表,方法表,属性表引用。
常量池的项目类型:
类型 | 标志 | 描述 |
CONSTANT_Utf8_info | 1 | UTF-8编码字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 标识方法句柄 |
CONSTANT_MethodType_info | 16 | 标识方法类型 |
CONSTANT_InvokeDtnamic_info | 18 | 表示一个动态方法调用点 |
这14种常量类型各自有自己的结构,下面列出每个常量项的结构及含义
常量池中的14种常量项的结构总表:
2.4访问标志位
在常量池之后,是两个字节的访问标志位,用于识别一些类或者接口层次的信息,比如是否public 是否abstract 是否final等等。结果是以下访问标志的或运算0x0001 | 0x0020,没有使用的标志一律按0处理。
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令,JDK1.2以后编译出来的类这个标志为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口和抽象类,此标志为真,其它类为假 |
ACC_SYNTHETIC | 0x1000 | 标识别这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
举例:我们的访问标志位为0x 00 21,所以访问标志为public
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令,JDK1.2以后编译出来的类这个标志为真 |
2.5类索引,父类索引,接口索引集合
这三项数据用来确定类的继承关系。
类索引:占用2个字节,确定类的全限定名称
父类索引:占用两个字节(Java是单继承体系,所以大小固定,如果是Object类,则没有父类,指向0x 00 00号引用,即没有引用)
接口索引集合:由2字节的索引计数器和接口索引(每个2字节)们构成,顺序严格按照源码的实现顺序排列,如果没有接口,则如下所示:0x 00 00即表示没有接口
2.6字段表集合
字段表集合用于描述类中的字段,这其中不包含从父类继承而来的字段,,甚至可能会在编译器添加一些源码中没有的字段。按照惯例,字段表集合由2字节的字段计数器和字段表们构成。
0x 00 01表示只有一个字段
在Java中一般通过如下几项描述一个字段:字段作用域(public、protected、private修饰符)、是类级别变量还是实例级别变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、可序列化与否(transient修饰符)、字段数据类型(基本类型、对象、数组)以及字段名称。在字段表中,变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示,字段表格式如下表所示:
类型 | 名称 | 数量 |
u2 | access_flags(访问标志) | 1 |
u2 | name_index(简单名称) | 1 |
u2 | descriptor_index(描述符) | 1 |
u2 | attributes_count(属性表) | 1 |
attribute_info | attributes(属性) | attributes_count |
下面开始叙述字段表的每个构成:
1.访问标志:2个字段,描述字段的作用域
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 字段是否为public |
ACC_PRIVATE | 0x0002 | 字段是否为private |
ACC_PROTECTED | 0x0004 | 字段是否为protected |
ACC_STATIC | 0x0008 | 字段是否为static |
ACC_FINAL | 0x0010 | 字段是否为final |
ACC_VOLATILE | 0x0040 | 字段是否为volatile |
ACC_TRANSIENT | 0x0080 | 字段是否为transient |
ACC_SYNTHETIC | 0x1000 | 字段是否为编译器自动产生 |
ACC_ENUM | 0x4000 | 字段是否为enum |
比如:0x 00 02表示字段的作用域为private
2.简单名称引用:2个字节,会指向常量池中的一个应用,就是字段的名称,比如0x 00 05指向常量池第五号引用。
3.描述符引用:2个字节,指向常量池中的一个引用,表示字段的类型。类型共有以下几种,数组类型会前置 [ 描述,比如整形数组[I。
标识字符 | 含义 |
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型short |
Z | 基本类型boolean |
V | 特殊类型void |
L | 对象类型,如:Ljava/lang/Object; |
举例:0x 00 06指向常量池的第六号引用。所以,通过以上信息,可以知道此类由一个字段private int m;
4.在描述符之后,可能会有一个属性计数器(2字节)+属性表集合。如果字段是private static final int m;就会有属性表,下文中会讲到。举例,Test类的属性表计数器值为 0x 00 00 表示这个属性没有属性表
2.7方法表集合
放发表集合则描述了类中方法的信息,同字段表一样,父类中的方法如果没有被重写,便不会存在于方法表中。方法表集合的构成也和字段表一样。2字节的方法计数器+方法表集合。方法表结构如下:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
1.访问标志:描述方法的修饰符,2字节,由如下访问标志种类:
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 字段是否为public |
ACC_PRIVATE | 0x0002 | 字段是否为private |
ACC_PROTECTED | 0x0004 | 字段是否为protected |
ACC_STATIC | 0x0008 | 字段是否为static |
ACC_FINAL | 0x0010 | 字段是否为final |
ACC_SYNCHRONIZED | 0x0020 | 字段是否为synchronized |
ACC_BRIDGE | 0x0040 | 方法是否是由编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 字段是否为native |
ACC_ABSTRACT | 0x0400 | 字段是否为abstract |
ACC_STRICTFP | 0x0800 | 字段是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 字段是否为编译器自动产生 |
2.简单名称引用:方法的简单名称就是方法名getM
3.描述符引用:指向常量池中的引用,描述方法的参数及返回值(参数)返回值
4.属性表集合:方法内部的代码就放在属性表集合中的Code属性中,在属性表集合中会讲到。
举例:0x 00 02 表示Test类中由两个方法。然后,0x 00 01是第一个方法的访问标志,查表为public,0x 00 07指向常量池的第七号引用,表示简单名称<init>是编译器自动加入的常量,表示实例构造器。接下来,0x 00 08是描述符引用,描述方法的参数及返回值,指向常量池第八号引用()V,所以,此方法没参数,返回值为空。0x 00 01表示此方法有一个属性,0x 00 09指向常量池的引用,表示属性种类为Code,其中便是此方法的代码实现即指令集合了。
2.8属性表集合
属性表集合已经出现过了几次,表示一些其他的信息。由2字节的属性计数器和属性表集合构成。属性表有许多种类,每种都有不同的结构,
与Class文件中其它数据项对长度、顺序、格式的严格要求不同,属性表集合不要求其中包含的属性表具有严格的顺序,并且只要属性的名称不与已有的属性名称重复,任何人实现的编译器可以向属性表中写入自己定义的属性信息。虚拟机在运行时会忽略不能识别的属性,为了能正确解析Class文件,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性:
属性名称 | 使用位置 | 含义 |
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类文件、字段表、方法表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTale | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 源文件名称 |
Synthetic | 类文件、方法表、字段表 | 标识方法或字段是由编译器自动生成的 |
每种属性均有各自的表结构。这9种表结构有一个共同的特点,即均由一个u2类型的属性名称开始,可以通过这个属性名称来判段属性的类型
Code属性:Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性中。当然不是所有的方法都必须有这个属性(接口中的方法或抽象方法就不存在Code属性),Code属性表结构如下:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
max_stack:操作数栈深度最大值,在方法执行的任何时刻,操作数栈深度都不会超过这个值。虚拟机运行时根据这个值来分配栈帧的操作数栈深度
max_locals:局部变量表所需存储空间,单位为Slot。并不是所有局部变量占用的Slot之和,当一个局部变量的生命周期结束后,其所占用的Slot将分配给其它依然存活的局部变量使用,按此方式计算出方法运行时局部变量表所需的存储空间
code_length和code:用来存放Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。
每一个指令是一个u1类型的单字节,当虚拟机读到code中的一个字节码(一个字节能表示256种指令,Java虚拟机规范定义了其中约200个编码对应的指令),就可以判断出该字节码代表的指令,指令后面是否带有参数,参数该如何解释,虽然code_length占4个字节,但是Java虚拟机规范中限制一个方法不能超过65535条字节码指令,如果超过,Javac将拒绝编译
举例:0x 00 01表示只有一个属性。00 09是常量池引用,表明属性类型Code,00 00 00 1D表示长度是29字节,00 01表示最大入栈深度为1,00 01表示局部变量表所需空间为1字节,00 00 00 05表示此构造方法由5个指令,分别是2A B7 00 01 B1,接着00 00表示没有异常抛出,然后又是一个属性表集合,00 01表示由一个属性,00 0A表示此属性是第十号常量池引用(LineNumberTable)类型,接下来讲解其他的属性类型
ConstantValue属性:通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量(类变量)才可以使用这项属性。其结构如下:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | constantvalue_index | 1 |
可以看出ConstantValue属性是一个定长属性,其中attribute_length的值固定为0x00000002,constantvalue_index为一常量池字面量类型常量索引(Class文件格式的常量类型中只有与基本类型和字符串类型相对应的字面量常量,所以ConstantValue属性只支持基本类型和字符串类型)
对非static类型变量(实例变量,如:int a = 123;)的赋值是在实例构造器<init>方法中进行的
对类变量(如:static int a = 123;)的赋值有2种选择,在类构造器<clinit>方法中或使用ConstantValue属性。当前Javac编译器的选择是:如果变量同时被static和final修饰(虚拟机规范只要求有ConstantValue属性的字段必须设置ACC_STATIC标志,对final关键字的要求是Javac编译器自己加入的要求),并且该变量的数据类型为基本类型或字符串类型,就生成ConstantValue属性进行初始化;否则在类构造器<clinit>方法中进行初始化
Exceptions属性:列举出方法中可能抛出的受查异常(即方法描述时throws关键字后列出的异常),与Code属性平级,与Code属性包含的异常表不同,其结构为:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exceptions | 1 |
u2 | exception_index_table | number_of_exceptions |
number_of_exceptions表示可能抛出number_of_exceptions种受查异常
exception_index_table为异常索引集合,一组u2类型exception_index的集合,每一个exception_index为一个指向常量池中一CONSTANT_Class_info型常量的索引,代表该受查异常的类型
InnerClasses属性:该属性用于记录内部类和宿主类之间的关系。如果一个类中定义了内部类,编译器将会为这个类与这个类包含的内部类生成InnerClasses属性,结构为:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_classes | 1 |
inner_classes_info | inner_classes | number_of_classes |
inner_classes为内部类表集合,一组内部类表类型数据的集合,number_of_classes即为集合中内部类表类型数据的个数
每一个内部类的信息都由一个inner_classes_info表来描述,inner_classes_info表结构如下:
类型 | 名称 | 数量 |
u2 | inner_class_info_index | 1 |
u2 | outer_class_info_index | 1 |
u2 | inner_name_index | 1 |
u2 | inner_name_access_flags | 1 |
inner_class_info_index和outer_class_info_index指向常量池中CONSTANT_Class_info类型常量索引,该CONSTANT_Class_info类型常量指向常量池中CONSTANT_Utf8_info类型常量,分别为内部类的全限定名和宿主类的全限定名
inner_name_index指向常量池中CONSTANT_Utf8_info类型常量的索引,为内部类名称,如果为匿名内部类,则该值为0
inner_name_access_flags类似于access_flags,是内部类的访问标志
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 内部类是否为public |
ACC_PRIVATE | 0x0002 | 内部类是否为private |
ACC_PROTECTED | 0x0004 | 内部类是否为protected |
ACC_STATIC | 0x0008 | 内部类是否为static |
ACC_FINAL | 0x0010 | 内部类是否为final |
ACC_INTERFACE | 0x0020 | 内部类是否为一个接口 |
ACC_ABSTRACT | 0x0400 | 内部类是否为abstract |
ACC_SYNTHETIC | 0x1000 | 内部类是否为编译器自动产生 |
ACC_ANNOTATION | 0x4000 | 内部类是否是一个注解 |
ACC_ENUM | 0x4000 | 内部类是否是一个枚举 |
LineNumberTale属性:用于描述Java源码的行号与字节码行号之间的对应关系,非运行时必需属性,会默认生成至Class文件中,可以使用Javac的-g:none或-g:lines关闭或要求生成该项属性信息,其结构如下:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
line_number_table是一组line_number_info类型数据的集合,其所包含的line_number_info类型数据的数量为line_number_table_length,line_number_info结构如下:
类型 | 名称 | 数量 | 说明 |
u2 | start_pc | 1 | 字节码行号 |
u2 | line_number | 1 | Java源码行号 |
不生成该属性的最大影响是:1,抛出异常时,堆栈将不会显示出错的行号;2,调试程序时无法按照源码设置断点
LocalVariableTable属性:用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,非运行时必需属性,默认不会生成至Class文件中,可以使用Javac的-g:none或-g:vars关闭或要求生成该项属性信息,其结构如下:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | local_variable_table_length | 1 |
local_variable_info | local_variable_table | local_variable_table_length |
local_variable_table是一组local_variable_info类型数据的集合,其所包含的local_variable_info类型数据的数量为local_variable_table_length,local_variable_info结构如下:
类型 | 名称 | 数量 | 说明 |
u2 | start_pc | 1 | 局部变量的生命周期开始的字节码偏移量 |
u2 | length | 1 | 局部变量作用范围覆盖的长度 |
u2 | name_index | 1 | 指向常量池中CONSTANT_Utf8_info类型常量的索引,局部变量名称 |
u2 | descriptor_index | 1 | 指向常量池中CONSTANT_Utf8_info类型常量的索引,局部变量描述符 |
u2 | index | 1 | 局部变量在栈帧局部变量表中Slot的位置,如果这个变量的数据类型为64位类型(long或double), 它占用的Slot为index和index+1这2个位置 |
start_pc + length即为该局部变量在字节码中的作用域范围
不生成该属性的最大影响是:1,当其他人引用这个方法时,所有的参数名称都将丢失,IDE可能会使用诸如arg0、arg1之类的占位符代替原有的参数名称,对代码运行无影响,会给代码的编写带来不便;2,调试时调试器无法根据参数名称从运行上下文中获取参数值
SourceFile属性:用于记录生成这个Class文件的源码文件名称,为可选项,可以使用Javac的-g:none或-g:source关闭或要求生成该项属性信息,其结构如下:
型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | sourcefile_index | 1 |
可以看出SourceFile属性是一个定长属性,sourcefile_index是指向常量池中一CONSTANT_Utf8_info类型常量的索引,常量的值为源码文件的文件名
对大多数文件,类名和文件名是一致的,少数特殊类除外(如:内部类),此时如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错误代码所属的文件名
Deprecated属性和Synthetic属性:这两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念
Deprecated属性表示某个类、字段或方法已经被程序作者定为不再推荐使用,可在代码中使用@Deprecated注解进行设置
Synthetic属性表示该字段或方法不是由Java源码直接产生的,而是由编译器自行添加的(当然也可设置访问标志中的ACC_SYNTHETIC标志,所有由非用户代码产生的类、方法和字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器<init>和类构造器<clinit>方法)
这两项属性的结构为(当然attribute_length的值必须为0x00000000):
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
还有关于字节码指令的详细文章看以下两篇
字节码指令总体认识------点击打开链接
字节码指令的详细------点击打开链接