jvm-class类文件结构(三)

jvm-class类文件结构(三)

Java虚拟机已经实现了语言无关性的特点。而实现语言无关性的基础是虚拟机和字节码的存储格式,Java虚拟机已经不和包括Java语言在内的任何语言绑定。它只与“class”文件这种特定的二进制文件相关联。在class文件中包含了Java虚拟机指令集和符号表以及若干辅助信息。

任何编程语言只要能编译成class文件,并且符合class规定的文件格式,就可以在java虚拟机中执行。

Class类文件结构

Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在class文件中,中间没有任何分隔符,这使得class文件中存储的内容几乎是全部程序运行的程序。Java虚拟机规范规定,Class文件格式采用类似C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表

无符号数:属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节。
:是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾。
那么表是干嘛的呢?表主要用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,比如方法、字段。
需要注意的是class文件是没有分隔符的,所以每个的二进制数据类型都是严格定义的。具体的顺序定义如下:

在class文件中,主要分为魔数、Class文件的版本号、常量池、访问标志、类索引(还包括父类索引和接口索引集合)、字段表集合、方法表集合、属性表集合。

魔数与Class文件版本号

每个Class文件的头4个字节是魔数,魔数的唯一作用在于确定这个Class文件是否是Java虚拟机接受的Class文件。如gif和jpeg等在文件头中都存在魔术,使用魔术而不是使用扩展名是基于安全性考虑的——扩展名可以随意被改变。Class文件的魔术值为“0xCAFEBABE”(咖啡宝贝?)。
紧接着魔数的4个字节是Class文件版本号:版本号又分为次版本号和主版本号。其中前两个字节用于表示次版本号,后两个字节用于表示主版本号。这个的版本号是随着jdk版本的不同而表示不同的版本范围的。如果Class文件的版本号超过虚拟机版本,将被拒绝执行。

jdk高版本能执行低版本,但是低版本不能运行高版本代码。52版本不能运行53版本的代码,即是53版本里面写的代码和52版本里面写的代码一样,一旦代码打包编译版本为53就一定不能运行在52版本的jdk服务器上面。因为虚拟机再检查时刚开始检查版本号时,发现版本号大于jdk版本号就直接抛出异常不往下执行了,所以还没有到代码那一步就已经抛出异常了。

常量池

常量池可以简单理解为class文件的资源从库,这种数据类型是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的项目之一。在常量池中主要存放字面量和符号引用
常量池的存储结构:

  • 字面量比较接近Java语言层面的常量概念,比如文本字符串、声明为final的常量值等(百度百科的解释是字面量是用双引用号引住的一系列字符)。
  • 符号引用则主要包括三类常量:
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符。

常量池能够表示那些信息?

在常量池中每一项常量都是一个表,在jdk1.7中共有14中常量类型,所以常量池的项目就对应14张表,这14张表的每种类型都不一样。但是有一个共同特点:表开始的第一位都是一个u1类型的标志位,代表这个常量属于哪种类型。

需要注意的是,在Class文件中,方法、字段都需要引用CONSTANT-Utf8_info类型的常量,所以这种类型的常量的长度有一定的限制,也就是Java中方法、字段的最大长度。在CONSTANT-Utf8_info中,其length的值u2,说明Java虚拟机只能编译最大大约64KB的变量或者方法名。超过的话将不会进行编译。

使用下面命令查看常量池
javap -v Simple
查看反编译字节码指令
javap -verbose ThreadDome1.class

符号引用与直接引用的关联

符号引用是一组符号,用来描述所引用的目标,符号是以任何形式存在的字面量。对于符号引用Java虚拟机并没有严格的限制。规定只需要使用的时候能够无歧义定位到目标就可以。符号引用属于常量池中的内容,那么是不是说符号引用的目标已经加载到内存中了呢?答案是否定的,因为符号引用与虚拟机的内存布局无关,符号引用的目标并不一定已经加载到内存中了。

直接引用可以是直接指向引用目标的指针、相对偏移量或者是一个能够间接定位到目标的句柄。直接引用是和虚拟机的内存布局有关的,同一个符号引用在不同的虚拟机上翻译的直接引用一般是不同的。如果有了直接引用,那么引用的目标必定是存在内存中的。

访问标志

常量池之后的数据结构是访问标志(access_flags),占有两个字节,总共16位,这个标志主要用于识别一些类或者接口层次的访问信息,
主要包括:这个Class是类还是接口、是否定义public、是否定义abstract类型;如果是类的话是否被声明为final等。具体的标志访问如下:

类索引、父类索引和接口索引集合

这个数据项主要用于确定这个类的继承关系。

其中类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据。在Java中由于不允许多继承,所以父类索引是唯一的,但是一个类可以实现多个接口,所以得到的接口索引是一个集合,表示这个类实现了哪些接口。

类索引

我们知道一般情况下一个Java类源文件经过JVM编译会生成一个class文件,也有可能一个Java类源文件中定义了其他类或者内部类,这样编译出来的class文件就不止一个,但每一个class文件表示某一个类,至于这个class表示哪一个类,便可以通过 类索引 这个数据项来确定。JVM通过类的完全限定名确定是某一个类。 类索引的作用,就是为了指出class文件所描述的这个类叫什么名字。
类索引紧接着访问标志的后面,占有两个字节,在这两个字节中存储的值是一个指向常量池的一个索引,该索引指向的是CONSTANT_Class_info常量池项,
使用javap -v Simple,常量池中有以下信息:


可以看到常量池中的第一项是CONSTANT_Class_info项,它表示一个”com/louis/jvm/Simple”的类名。即类索引是告诉我们这个class文件所表示的是哪一个类。

父类索引

Java支持单继承模式,除了java.lang.Object 类除外,每一个类都会有且只有一个父类。class文件中紧接着类索引(this_class)之后的两个字节区域表示父类索引,跟类索引一样,父类索引这两个字节中的值指向了常量池中的某个常量池项CONSTANT_Class_info,表示该class表示的类是继承自哪一个类。

接口索引集合

由于类实现的接口数目不确定,所以接口索引集合的描述的前部分叫做接口计数器(interfaces_count),接口计数器占用两个字节,其中的值表示着这个类实现了多少个接口,紧跟着接口计数器的部分就是接口索引部分了,每一个接口索引占有两个字节,接口计数器的值代表着后面跟着的接口索引的个数。接口索引和类索引和父类索引一样,其内的值存储的是指向了常量池中的常量池项的索引,表示着这个接口的完全限定名。

字段表集合

字段表用于描述接口或者类中声明的变量。

字段包括类级变量和实例级变量,但是不包括方法内部声明的局部变量。自然,描述一个字段的信息包括:字段的作用域(public、protected、private)、实例变量与否(static)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、字段数据类型(基本数据类型、对象、数组)、字段名称。而字段叫什么名字,字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

可变性(final)

final访问标志(access_flags)占有两个字节,它能够表述的信息如下所示:

字段的数据类型表示和字段名称表示

class文件对数据类型的,字段的信息也被存放在一张表中,其字段表包括三种类型:

  • u2类型访问标志(access_flags),其访问标志在access_flags中
  • u2类型的name_index(字段的简单名称)
  • u2类型的描述符(descriptor_index)
    上面出现了简单名称,上文中出现了全限定名,以及这里出现的描述符,三者有什么区别呢?其中全限定名称比较好理解,就是类的完整路径信息。而简单名称则是指没有类型和参数修饰的方法或者字段名称。描述符的主要的作用是描述字段的数据类型、方法的参数列表和返回值。其中我们熟悉的void,在Class文件中用V表示。下面是完整的描述符标志的含义:

    对于数组类型,每一维度使用一个前置的“[”字符描述,如果是二维数组,那么就有两个“[”符号。比如“java.lang.String[][]”会被记录成“[[Ljava.lang.String;”

对于方法,则是按照县参数列表后返回值的顺序进行描述的。比如方法int inc(int a,int[] b,char[][] c,int d)的描述符是“(I[I[[CI)I”。

属性表集合—–静态field字段的初始化

在定义field字段的过程中,我们有时候会很自然地对field字段直接赋值,如下所示:

public static final int MAX=100;  
public  int count=0;  

对于虚拟机而言,上述的两个field字段赋值的时机是不同的:

  • 对于非静态(即无static修饰)的field字段的赋值将会出现在实例构造方法()中
  • 对于静态的field字段,有两个选择:1、在静态构造方法()中进行;2 、使用ConstantValue属性进行赋值

Sun javac编译器对于静态field字段的初始化赋值策略
目前的Sun javac编译器的选择是:如果使用final和static同时修饰一个field字段,并且这个字段是基本类型或者String类型的,那么编译器在编译这个字段的时候,会在
对应的field_info结构体中增加一个ConstantValue类型的结构体,在赋值的时候使用这个ConstantValue进行赋值;如果该field字段并没有被final修饰,或者不是基本
类型或者String类型,那么将在类构造方法<cinit>()中赋值。
对于上述的public static final init MAX=100; javac编译器在编译此field字段构建field_info结构体时,除了访问标志、名称索引、描述符索引外,会增加一个ConstantValue类型的属性表。

方法表集合

方法表集合是指由若干个方法表(method_info)组成的集合。对于在类中定义的若干个,经过JVM编译成class文件后,会将相应的method方法信息组织到一个叫做方法表集合的结构中,字段表集合是一个类数组结构,如下图所示:

一个类中方法包含的信息

属性表集合

记录方法的机器指令和抛出异常等信息属性表集合记录了某个方法的一些属性信息,这些信息包括:

  • 这个方法的代码实现,即方法的可执行的机器指令
  • 这个方法声明的要抛出的异常信息
  • 这个方法是否被@deprecated注解表示
  • 这个方法是否是编译器自动生成的

Code类型的属性表

Code类型的属性表(attribute_info)可以说是class文件中最为重要的部分,因为它包含的是JVM可以运行的机器码指令,JVM能够运行这个类,就是从这个属性中取出机器码的。除了要执行的机器码,它还包含了一些其他信息,如下所示:

Code属性表的组成部分:

  • 机器指令—-code:
    目前的JVM使用一个字节表示机器操作码,即对JVM底层而言,它能表示的机器操作码不多于2的 8 次方,即 256个。class文件中的机器指令部分是class文件中最重要的部分,并且非常复杂,本文的重点不止介绍它,我将专门在一片博文中讨论它,敬请期待。
  • 异常处理跳转信息—exception_table:
    如果代码中出现了try{}catch{}块,那么try{}块内的机器指令的地址范围记录下来,并且记录对应的catch{}块中的起始机器指令地址,当运行时在try块中有异常抛出的话,JVM会将catch{}块对应懂得其实机器指令地址传递给PC寄存器,从而实现指令跳转;
  • Java源码行号和机器指令的对应关系—LineNumberTable属性表:
    编译器在将java源码编译成class文件时,会将源码中的语句行号跟编译好的机器指令关联起来,这样的class文件加载到内存中并运行时,如果抛出异常,JVM可以根据这个对应关系,抛出异常信息,告诉我们我们的源码的多少行有问题,方便我们定位问题。这个信息不是运行时必不可少的信息,但是默认情况下,编译器会生成这一项信息,如果你项取消这一信息,你可以使用-g:none 或-g:lines来取消或者要求设置这一项信息。如果使用了-g:none来生成class文件,class文件中将不会有LineNumberTable属性表,造成的影响就是 将来如果代码报错,将无法定位错误信息报错的行,并且如果项调试代码,将不能在此类中打断点(因为没有指定行号。)
  • 局部变量表描述信息—-LocalVariableTable属性表:
    局部变量表信息会记录栈帧局部变量表中的变量和java源码中定义的变量之间的关系,这个信息不是运行时必须的属性,默认情况下不会生成到class文件中。你可以根据javac指令的-g:none或者-g:vars选项来取消或者设置这一项信息。

http://blog.csdn.net/luanlouis/article/details/41113695
http://blog.csdn.net/u011080472/article/details/51326334
《深入理解java虚拟机》

猜你喜欢

转载自blog.csdn.net/piaoslowly/article/details/81459950