Java Class文件结构
平台无关性和语言无关性Java在刚刚诞生的时候提出过一个宣传口号:“一次编写,到处运行”,这句话充分表达了软件开发人员对冲破平台界限的渴求。“与平台无关”的理想最终实现在操作系统的应用层上:Sun公司及其他虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的“一次编写,到处运行”。各种不同平台的虚拟机与所有平台都统一使用的程序
平台无关性和语言无关性
Java在刚刚诞生的时候提出过一个宣传口号:“一次编写,到处运行”,这句话充分表达了软件开发人员对冲破平台界限的渴求。“与平台无关”的理想最终实现在操作系统的应用层上:Sun公司及其他虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的“一次编写,到处运行”。
各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石,这里说的是统一的存储格式——字节码,而不是Java语言,所以如果其它语言也能够被编译成这种字节码,是不是也能够运行在Java虚拟机上?在Java发展之初,设计者就曾经考虑过并实现了让其它语言运行在Java虚拟机之上的可能性,他们在发布规范文档的时候也可以把Java的规范拆分成了Java语言规范《The Java Language Specification》和Java虚拟机规范《The Java Virtual Machine Specification》。而到了如今,除了Java语言之外,已经发展出了一大批正在Java虚拟机纸上运行的语言,如Clojure、Groovy、JRuby、Jyhon、Scala等。
语言无关性的基础就是虚拟机和字节码的存储格式。Java虚拟机不和任何语言绑定,包括Java,它只与“Class文件”这种特定的二进制文件格式所关联,所以任何一门语言只要可以编译成规范的Class文件,就可以运行在Java虚拟机上,虚拟机不关心Class的来源是什么语言。
Class类文件结构
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上的空间数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。
无符号数: 无符号数属于基本的数据类型,以u1、u2、u4和u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串。
表: 表是由多个无符号数或者其它表作为数据项构成的复合数据类型,所有表都习惯性地以”_info”结尾。用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,它的组成如下图所示。
类型 | 名称 | 中文别名 | 数量 |
---|---|---|---|
u4 | magic | 魔数 | 1 |
u2 | minor_version | 次版本号 | 1 |
u2 | major_version | 主版本号 | 1 |
u2 | constant_pool_count | 常量池容量计数值 | 1 |
cp_info | constant_pool | 常量池 | constant_pool_count-1 |
u2 | access_flags | 访问标志 | 1 |
u2 | this_class | 类索引 | 1 |
u2 | super_class | 父类索引 | 1 |
u2 | interfaces_count | 接口计数器 | 1 |
u2 | interfaces | 接口索引集合 | interfaces_count |
u2 | fields_count | 字段表计数器 | 1 |
field_info | fields | 字段表 | fields_count |
u2 | methods_count | 方法表计数器 | 1 |
method_info | methods | 方法表 | methods_count |
u2 | attributes_count | 属性表计数器 | 1 |
attribute_info | attributes | 属性表 | attributes_count |
为了方便说明,所以准备了一段简单的代码作为示例:
package com.overridere.six;
public class Test {
public static void main(String[] args) {
TestClass tc = new TestClass();
System.out.println(tc.print());
}
}
用16进制编辑器(WinHex)打开这个类的Class文件如下图所示:
前面也说过了,Class文件里面的内容是严格按照顺序紧凑排列的,所以Class文件可以看做就是一行很长很长的数据(当做一个很长很长的数组),如何找到需要的数据呢?就是按偏移量来寻找(相当于数组中的下标),上图只不过将这个很长很长的数据以16进制的二维形式显示了,满16个进一位就加一行。
接下来将围绕这个文件对Class文件进行分析
魔数与Class文件版本
每个Class文件的头4个字节称为魔数(Magic Number) 16进制表中为0xCAFEBABE,它的唯一作用是确定这个文件是否是一个能被虚拟机接收的Class文件,是用来标识Class文件的。
第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version),Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。次版本号值为0x0000,主版本号值为0x0034,也就是十进制的52,代表JDK是1.8版本。
常量池
紧接着主版本号之后两个字节的是常量池计数值(constant_pool_count),表示常量池中的项目数量,这个容量计数是从1开始的,在本示例当中常量池容量(偏移量:0x0000008)值为十六进制的0x0016,即十进制的22,代表常量池中有21项常量,索引值范围为1~22。
常量池(constant_pool) 主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。字面量相当于Java语言中的常量的概念,符号引用包括下面三类常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
常量池中的项有各种各样的类型,在JDK1.7之前有11种类型,JDK1.7的时候为了更好的支持动态语言调用有额外增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info),总共14种类型,这14种类型都是表结构,并且表开始的第一位都是一个u1类型的标志位,代表当前这个常量属于哪种类型。这14种类型如下表所示:
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码的Unicode字符串 |
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_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
试着分析常量池中的常量,看图中常量池的第一项常量,无论什么类型的常量,第一个位置都是标志位且占一个字节,所以它的标志位(偏移地址:0x0000000A)是0x07,查看上面的类型表,得知标志位为7的类型是CONSTANT_Class_info类型,此类型代表一个类或者接口的符号引用,它的结构如下表所示:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
tag是标志位,name_index是一个索引值,指向常量池中一个CONSTANT_Utf8_info常量,磁场量代表了这个类或接口的全限定名,这里的name_index值(偏移地址:0x0000000B)位0x0002,也即指向常量池中的第二项常量。继续分享第二项常量,它的标志位(偏移地址:0x0000000D)是0x01,查表得知是一个CONSTANT_Utf8_info类型的常量。该类型结构如下表所示:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
length值为长度,后面长度为length的连续字节是一个使用UTF-8缩略编码表示的字符串。
UTF-8缩略编码与普通UTF-8编码的区别:从’u\0001’到’\u007f’之间的字符的缩略码使用一个字节表示,’\u0080’到’\u07ff’之间用两个字节表示,’\u0800’到’\uffff’用三个字节表示。
本示例中的length值(偏移地址:0x0000000E)为0x001C=28,往后28个字节为该字符串的内容,内容为“com/overridere/six/TestClass”。
通过javap -verbose命令可以输出TestClass.class文件字节码内容。
E:\JAVA\WorkPlace\JVMTest\bin\com\overridere\six>javap -verbose TestClass
Classfile /E:/JAVA/WorkPlace/JVMTest/bin/com/overridere/six/TestClass.class
Last modified 2017-4-6; size 393 bytes
MD5 checksum 3533fcf790f4d442f45fc0f44ccbfb99
Compiled from "TestClass.java"
public class com.overridere.six.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // com/overridere/six/TestClass
#2 = Utf8 com/overridere/six/TestClass
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Methodref #3.#11 // java/lang/Object."<init>":()V
#11 = NameAndType #7:#8 // "<init>":()V
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/overridere/six/TestClass;
#16 = Utf8 print
#17 = Utf8 ()I
#18 = Fieldref #1.#19 // com/overridere/six/TestClass.m:I
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 SourceFile
#21 = Utf8 TestClass.java
从以上代码可以看出,计算机已经帮我计算好了所有常量,并且第1、2项常量与我们手动计算的一样。其中一些从没在代码中出现过的常量如”I”、”V”、”《init》”、”LineNumberTable”、”LocalVariableTable”等,是用来描述一些不方便使用“固定字节”进行表达的内容。比如描述方法的返回值是什么,有几个参数,每个参数类型是什么。
最后放上所有常量类型的结构:
访问标志
在常量池后面的是访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。具体的标志位及标志含义如下表:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语意 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是啥一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
试着分析本示例中的access_flags,TestClass是一个普通类,不是接口、枚举或注解,不是final和abstract,被public修饰,所以ACC_PUBLIC、ACC_SUPER标志应当为真,其它应为假,所以值应该是0x0001|0x0020=0x0021。查看上面的十六进制文本,access_flags标志(偏移地址:0x000000EF)的确为0x0021。
类索引、父类索引和接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据集合。类索引用来确定本类的全限定名,父类索引用来确定本类的父类全限定名,除了java.lang.Object外所有类都有父类,所以父类索引都不为0。
接口索引集合就是描述本类实现了哪些接口,接口集合之前有一个接口计数器(interface_count),表示接口集合的容量,如果为0则表示没有实现接口,后面的接口集合不占用任何字节。
类索引、父类索引和接口集合里面的接口索引都是指向一个CONSTANT_Class_info类型的常量,通过CONSTANT_Class_info就可以找到CONSTANT_Utf8_info类型的常量中的全限定名字符串。
字段表集合
字段表(field_info)用于描述接口或类中声明的变量。字段包括类级变量以及实例级变量,不包括方法内部的局部变量。字段表结构如下图所示:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段访问标志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_VOLATILE | 0x0040 | 字段是否volatile |
ACC_TRANSIENT | 0x0080 | 字段是否transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动生成的 |
ACC_ENUM | 0x4000 | 字段是否enum |
name_index代表着字段的简单名称,descriptor_index表示字段和方法的描述符。
本类中的print()方法和字段m的简单名称分别为“print”和“m”。
描述符(descriptor_index)的作用是描述字段的数据类型、方法的参数列表和返回值。根据描述符规则,基本类型和和void返回类型都用一个大写字符来表示,对象类型则用L加对象的全限定名来表示。除了long和boolean类型分别用J和Z表示,其它基本类型都用首字母表示,比如说byte用B表示,char用C表示。
对于数组来说,每一维数组将用一个前置的“[”来描述,比如说”java.lang.String[][]”类型的数组将被记录为“[[Ljava/lang/String”,一个整型数组”int[]”将被记录为”[I”。
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组”()”内。如方法void print()的描述符为”()V”,方法java.lang.String toString()的描述符为”()Ljava/lang/String;”。
后面还有一个字段属性表(attributes)会在后面属性表集合中讲。
方法表集合
方法表集合与字段表集合差不多。包含的内容跟字段表一样,如下表:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
访问标志字段access_flags与字段有些不同,如下表:
标志名称 | 标志值 | 含义 |
---|---|---|
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 | 方法是否由编译器自动生成的 |
方法中的代码会保存在方法表中的属性表中的Code属性中
如果没有重写父类方法,就不会出现父类的方法,但是也会出现由编译器自动添加的方法,最典型的的如类构造器”《clinit》”方法和实例构造”《init》”方法。
属性表集合
属性表集合(attribute_info)。虚拟机规范预定义的属性表中的项有下表所示的属性:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
Signature | 类、方法表、字段表 | JDK1.5中新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要用这个属性记录泛型中的相关信息 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | JDK1.6中新增的属性,SourceDebugExtension用于存储额外的调试信息。 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成 |
LocalVariableTypeTable | 类 | JDK1.5中新增的属性,它使用特征前面代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类、方法表、字段表 | JDK1.5中新增的属性,为动态注解提供支持。用于知名哪些注解是运行时可见的 |
RuntimeInvisibleAnnotations | 类、方法表、字段表 | JDK1.5中新增的属性,与RuntimeVisibleAnnotations作用正好相反,用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotations | 方法表 | JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations类似,不过作用对象是方法参数 |
RuntimeInvisibleParameterAnnotations | 方法表 | JDK1.5中新增的属性,作用与RuntimeInvisibleAnnotations类似,不过作用对象是方法参数 |
AnnotationsDefault | 方法表 | JDK1.5中新增的属性,用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | JDK1.7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符 |
属性表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attributes_length | 1 |
u1 | info | attributes_length |
Code属性
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在方法表中的属性表中的Code属性中。Code属性结构如下表所示:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_local | 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 |
attribute_name_index是一项指向CONSTANT_Utf8_info类型常量的索引,常量固定为“Code”,代表该属性的名称。
attribute_length指示了属性值的长度。
max_locals代表了局部变量表(以后文章分析)所需的存储空间。
code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。
code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,则Code属性用于描述代码,所有其他数据都是用于描述元数据。
以本文中的示例为例试分析一下code。
根据字节码指令表翻译code属性(2A B7 00 0A B1)所对应的字节码指令。
1. 读入2A,查表得知对应的指令为aload_0
2. 读入B7,差表得知对应的指令为invokespecial
3. 读入00 0A,这是invokespecial的参数,查常量池得知0x000A对应的常量为实例构造器“《init》”方法的符号引用。
4. 读入B1,查表得知B1对应的指令为return,返回此方法并且返回值是void。
用javap -verbose指令显示Class文件字节码指令如下:
{
public com.overridere.six.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #10 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/overridere/six/TestClass;
public int print();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #18 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/overridere/six/TestClass;
}
我们可以看到其中Args_size和Locals都不为0,明明init()方法和print()方法都没有参数和局部变量,为什么这两个值不为0呢?以前学习的时候应该都注意到了,在任何方法里面都可以通过this关键字访问到此方法所属对象。这个访问机制的实现就是通过Javac编译器编译的时候将this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法的时候自动传入这个参数,所以在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量。
字节码指令之后就是异常处理表集合(略)
Exceptions属性
Exceptions属性的作用是列举出方法中可能抛出的受查异常(Chexked Exceptions),也就是方法描述时在throws关键字后面列举的异常。结构如下表:
类型 | 名称 | 数量 |
---|---|---|
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项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。
LineNumberTable属性
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码偏移量)之间的对应关系。它并不是运行时必需的属性,但是默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:line选中来取消或要求生成这项信息。如果选中不生成,对程序产生的主要影响就是当抛出异常时,对战中将不会显示出错行号,并且在调试程序的时候,也无法按照源码行来设置断点。
LocalVariableTable属性
LocalVariableTable属性用于描述栈帧(以后文章分析)中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不上运行时必需的属性,但默认会生成到Class文件中,可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。如果没有生成,最大的影响就是当其他人调用这个房东时,所有参数名称都将会丢失,IDE会使用诸如arg0、arg1之类的占位符代替原有的参数名。
其它属性(略)
以上所有内容都是在阅读深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)第六章内容之后的选择性总结归纳,如果想更详细地了解的话,请阅读正版书籍,如果发现以上内容有一些错误也欢迎指出,谢谢。
更多推荐
所有评论(0)