如何使用java源代码 mainclass.java源代码_java主类与源代码名称

类文件结构



在说完了JVM内部结构后,接下来我们需要说一下另外一个非常重要的基础概念Class类结构。

我们既然知道了开发的Java源代码会首先被编译成字节码文件保存,JVM的类加载器会读取这些文件内容,然后将其转换为Class类对象保存到JVM管理的运行数据区里。那么这个编译后的Class类文件的结构如何呢?我们今天来简单说一下。

其实JVM的规范里有详细的定义和说明,不过它涉及了很多专业的名称和术语,还有就是说的比较简洁,理解起来还是有些难度。

类文件结构

简单说来从存储字节码的.class文件中读取的内容由10个基本组成部分构成:




如何使用java源代码 mainclass.java源代码_如何使用java源代码_02

简略结构



  • magic: 0xCAFEBABE
  • minor_version和major_version: 包括类文件的主次版本号
  • constant_pool: 当前类的常量池
  • access_flags: 当前类访问标志
  • this_class: 当前类名字
  • super_class: 其父类名字
  • interfaces: 当前类实现的任何接口
  • fields: 当前类定义的任何字段
  • methods: 当前类包含的方法
  • attributes: 类的任何属性(例如源文件的名称等)

一般情况下,该文件是由8位字节基本单位长度字节流构成,对于那些16位,32位,甚至64位的它们将分别读取2个,4个和8个基本单位来表示。

这类多字节数据项都将是以大端顺序存储的,也就是说高位字节在前。

我们在Java开发时,常用的读写这类数据的接口是java.io.DataInput和java.io.DataOutput,对应的类有java.io.DataInputStream和java.io.DataOutputStream。




如何使用java源代码 mainclass.java源代码_java主类与源代码名称_03

类文件结构



截图中的u1,u2,u4 它们分别表示无符号的1字节,2字节和4字节类型,它们分别对应着我们Java的基础数据类型中的byte,short,int等。我们完全可以使用java.io.DataInput接口定义的readUnsignByte,readUnsignShort, readUnsignInt等读取其值。

另外一类就是长度在加载之前是未知的,有可变长度的部分,如常量池,方法,属性等。这些部分的组织方式是以它们的大小或长度作为前缀的。

通过这种方式,JVM在实际加载可变长度区段之前就知道它们的大小。

在Java ClassFile中不同部分的顺序是严格定义的,这样JVM就知道每一步应该要加载什么,以及加载不同组件的顺序。

关于魔幻数字的故事

詹姆斯·高斯林(James Gosling)曾经解释过这个神奇数字的历史:

他说“我们过去常去一个叫 St Michael’s Alley的地方吃午饭。据当地传说,在黑暗的过去,Grateful Dead乐队在成名之前经常在那里演出。这是一个非常时髦的地方,绝对是一个‘Grateful Dead Kinda Place’。Jerry死后,他们甚至建了一座佛教风格的小神龛。当我们过去常去那里,我们把那个地方称为‘Cafe Dead’。

沿着这思路,有人注意到这是一个十六进制数。当时我重新整理了一些文件格式代码,需要几个神奇的数字,一个用于持久对象文件,一个用于类文件。所以我就选用CAFEDEAD作为对象文件格式,并在查询中添加了4个字符的十六进制单词,它们与“CAFE”匹配。我意外获得了BABE,所以就决定使用CAFEBABE。在那个时候,它似乎并不是那么重要,或者注定要去任何地方,除了历史的垃圾桶。

因此CAFEBABE成为类文件格式,而CAFEDEAD则是持久对象格式。但持久性对象设施消失了,随之消失的还有CAFEDEAD的使用——它最终被RMI取代。”

类文件版本

类文件的下4个字节包含主版本号和次版本号。我们的JVM会通过这组数字验证和标识类文件。

如果这个数量大于JVM可以装载的数量,那么类文件将被错误拒绝,并提示java.lang.UnsupportedClassVersionError。

我们通常可以使用javap命令行来查看任何Java类文件的类版本。

先定义一个MyClass.java文件,然后编译成MyClass.class

public class Main {public static void main(String [] args) {int my_integer = 0xFEEDED;}}

我们使用javap命令查看:

$ javap -verbose MyClass

在上面的主类上执行javap,我们得到如下的符号表。




如何使用java源代码 mainclass.java源代码_如何使用java源代码_04

javap 显示类文件内容



关于常量池

我们知道所有的类或者接口的定义在应用程序运行过程中都不会发生变化的,因此所有与类或接口相关的常量都将存储在常量池中。

它们包括类名、变量名、接口名、方法名和签名、最终变量值、字符串字面量等。

这些常量在常量池中被存储为可变长度数组元素。

这些常量可变数组的前面是它的数组大小,因此JVM知道在加载类文件时需要多少常量。

而在每个数组元素中,第一个字节表示一个标记位,该标记指定数组中该位置的常量类型,JVM通过读取这个字节的标记来标识常量的类型。

因此,如果这个字节标记表示一个字符串文字,那么JVM知道接下来的两个字节表示字符串文字的长度,而条目的其余部分是字符串文字本身。

我们同样可以使用javap命令分析任何类文件的常量池。




如何使用java源代码 mainclass.java源代码_Java_05

常量池内容



上图中常量池总共有15个条目。

#1是方法public static void main;

#2用于整数值 0xFEEDED(16707053)

#3和#4分别是我们类的this和super类。

剩余其它都是存储字符串文本的符号表。

关于访问标识

它是一个2个字节的条目,用类指示文件是定义的类还是接口,如果是一个类,它是公共的、抽象的还是final的。

这些访问标志包括ACC_PUBLIC,ACC_FINAL,ACC_SUPER,ACC_INTERFACE,ACC_ABSTRACT,ACC_SYNTHETIC,ACC_ANNOTATION,ACC_ENUM等等。

关于this Class

this Class类是一个2个字节的条目,其内容是指向常量池中的索引。




如何使用java源代码 mainclass.java源代码_java主类与源代码名称_06

图片来自网络



比如在上面的图中,this类的值0x0007是常量池中的索引。

this 所指向的对应项Constant_pool[this_class]在常量池中有两部分,第一部分是一个字节的标记类表示在常量池中的条目类型。

这种情况下是类或者接口类型。在上面的图表中,这是用橙色表示的。第二个条目部分是两个字节,同样在常量池中有索引。

在上面的图中,两个字节包含值0x0004。因此它指向Constant_pool[0x0004],它的值是具有接口或类名称的字符串文字。

关于super Class和其它集合组件

排在this Class后面那个字节就是Super Class,跟this Class类似,它是2个字节的值,也是一个指向常量池的指针,该常量池具有类的超类的条目。

接口集合

包含由该文件中定义的类或接口实现的所有接口。

接口部分开始的两个字节是提供有关正在实现的接口总数的信息的计数,紧接着是一个数组,该数组包含了被该类实现的接口在常量池里的索引。

字段集合

字段是类或接口的实例或类级变量或属性存储的地方。

字段部分只包含由文件的类或接口定义的字段,而不包含从超类或超接口继承的字段。

它的前两个字节表示计数,是该部分中包含的所有字段数,跟在其后的是每个字段的可变长度数组。数组中每个元素都表示一个字段。

有些信息存储在这个结构中,有些像字段名等信息存储在常量池中。

方法集合

方法组件托管由该类显式定义的方法,而不包括从超类继承的任何其他方法。

同样,其开始的前两个字节是该类或者接口中包含的方法计数,其余的是包含每个方法结构的可变长度数组。

每个方法结构包含了有关方法的参数列表,返回值类型,方法的局部变量所需的堆栈单词数,方法操作数堆栈所需的堆栈单词,异常表,字节码序列等信息。

属性集合

属性部分包含有关类文件的几个属性,比如其中一个属性是源代码属性,它显示编译该类文件的源文件的名称。

同样属性部分的前两个字节是属性数量的计数,然后是属性本身。jvm将忽略它们不理解的任何属性。




如何使用java源代码 mainclass.java源代码_Java_07


总结一下

这里我简单的参照JVM规范说明了一下我们编写Java源代码后编译成的.class文件内容,它被JVM的类加载模块加载后,会形成上面的数据结构,了解这个结构,主要是为以后我们在Java编程中如何去理解其处理过程,代码优化,性能调优,包括反射机制如何读取等都有帮助。