JVM——类文件结构

时间:2023-02-01 10:18:56

http://blog.csdn.net/zhoufenqin/article/details/51045890

http://blog.csdn.net/ochangwen/article/details/51457398

http://blog.csdn.net/u010349169/article/category/2620885


无关性的基石

各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码( ByteCode ) 是构成平台无关性的基石。

实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。
JVM——类文件结构

Class类文件结构

注意:任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。 本章中,笔者只是通俗地将任意一个有效的类或接口所应当满足的格式称为“Class文件格式”,实际上它并不一定以磁盘文件的形式存在。

Class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数、无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。 
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。 
JVM——类文件结构

Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以在表6-1中的数据项,无论是顺序还是数量,甚至于数据存储的字节序( Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。接下来我们将一起看看这个表中各个数据项的具体含义。

1、魔数与Class文件的版本 (共8字节)

每个Class文件的头4个字节称为魔数,值为0xCAFEBABE。紧接着魔数的4个字节存储的是Class文件的版本号:5 6 字节是次版本号,7 8 字节是主版本号。(0xCAFEBABE00000034  我的版本号是52,是jdk1.8的版本

2、常量池(这里是指编译后生成的放在.class文件中的常量池,在类加载后会放入JVM的运行时常量池中)

紧接着魔数与版本号之后的是常量池入口.
常量池简单理解为class文件的资源从库
    1)是Class文件结构中与其它项目关联最多的数据类型
    2)是占用Class文件空间最大的数据项目之一
    3)是在文件中第一个出现的表类型数据项目。


常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于java语言层面的常量概念,如文本字符串、final常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量。

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。
从1开始计数。Class文件结构中只有常量池的容量计数是从1开始的

第0项腾出来满足后面某些指向常量池的索引值的数据在特定情况下需要表达"不引用任何一个常量池项目"的意思,这种情况就可以把索引值置为0来表示。当constant_pool中有14项,constant_poo_count的值为15。

Java代码在进行Java编译的时候,在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法被虚拟机使用的。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址之中。

JVM——类文件结构

JVM——类文件结构
JVM——类文件结构

3、访问标志(2字节)

这个标志主要用于识别一些类或接口层次的访问信息,主要包括:
    1).这个Class是类还是接口
    2).是否定义public
    3).是否定义abstract类型
    4).如果是类的话是否被声明为final等

具体的标志访问如下:

标志名称

标志值

含义

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

标识这是一个枚举

  本例测试类的访问标志如何计算的?
   access_flags一共有16个标志位可以使用,当前只定义了其中8个(JDK1.5增加后面3种),没有使用到标志位一律为0。
   由于TestClass只是一个被public修饰的普通Java类,并使用JDK1.2之后的编译器进行编译,因此测试类的访问标志为ACC_PUBLIC | ACC_SUPER = 0x0001 | 0x0020 = 0x0021 (位运算的或)

4、类索引,父类索引和接口索引(保存的是索引值)

   这三项数据主要用于确定这个类的继承关系
    其中类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引(interface)集合是一组u2类型的数据。

    类索引(this_class),用于确定这个类的全限定名,占2字节
    父类索引(super_class),用于确定这个类父类的全限定名(Java语言不允许多重继承,故父类索引只有一个。除了java.lang.Object类之外所有类都有父类,故除了java.lang.Object类之外,所有类该字段值都不为0),占2字节

    接口索引计数器(interfaces_count),占2字节。如果该类没有实现任何接口,则该计数器值为0,并且后面的接口的索引集合将不占用任何字节,
    接口索引集合(interfaces),一组u2类型数据的集合。用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果该类本身为接口,则为extends语句)后的接口顺序从左至右排列在接口的索引集合中

    this_class、super_class与interfaces按顺序排列在访问标志之后,它们中保存的索引值均指向常量池中一个CONSTANT_Class_info类型的常量,通过这个常量中保存的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

5、字段表集合

字段表( field_info ) 用于描述接口或者类中声明的变量。字段( field )包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。我们可以想一想在Java中描述一个字段可以包含什么信息?可以包括的信息有:字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性( final ) 、并发可见性( volatile修饰符 ,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组 )、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符 ,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

fields_count:字段表计数器,即字段表集合中的字段表数据个数,占2字节。本测试类其值为0x0001,即只有一个字段表数据,也就是测试类中只包含一个变量(不算方法内部变量)

fields:字段表集合,一组字段表类型数据的集合。字段表用于描述接口或类中声明的变量,包括类级别(static)和实例级别变量,不包括在方法内部声明的变量

    在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

    
字段表里的字段修饰符放在access_flags中,占2个字节,与类中的访问标志(access_flags)十分相似。

    

本例测试类的字段修饰符access_flags,其值为0X0002,由下表的标志位可知这个字段由private修饰

标志名称

标志值

含义

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

当然实际上,ACC_PUBLIC、ACC_PRIVATE和ACC_PROTECTED这3个标志只能选择一个,接口中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,Class文件对此并无规定,这些都是java语言所要求的

   

 name_index代表字段的简单名称参见备注2),占2字节,是一个对常量池的引用 。本例测试类对应的值为0x0005,即常量池中第5个常量

    

descriptor_index代表代表参数的描述符参见备注3),占2个字节,是一个对常量池的引用,其值为0x0006,即常量池中第6个常量

JVM——类文件结构

    综上,可以推断出源代码定义的字段为(和测试类完全一样):

[java]  view plain  copy
  1. private int m;    
  字段表包含的固定数据项到descriptor_index结束,之后跟随一个属性表集合用于存储一些附加信息:
    
attributes_count(属性计数器,占2字节。本例的测试类值为0x0000,对应字段m,它的属性表计数器为0,所以该字段没有额外需要描述的信息)
    
attributes(属性表集合,详细说明见后面“八、属性表集合”)

    字段表集合中不会列出从父类或父接口中继承的字段,但是可能列出原本Java代码之中不存在的字段,如:内部类为了保持对外部类的访问性,自动添加指向外部类实例的字段

    Java语言中字段是不能重载的,2个字段无论数据类型、修饰符是否相同,都不能使用相同的名称;但是对于字节码,只要字段描述符不同,字段重名就是合法的


6、方法表集合(方法里的java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中的一个名为“Code”的属性里面。 

JVM——类文件结构

    methods_count:方法表计数器,即方法表集合中的方法表数据个数。占2字节,其值为0x0002,即测试类中有2个方法

    methods:方法表集合,一组方法表类型数据的集合。方法表结构和字段表结构一样:

类型

名称

数量

u2

access_flags

1

u2

name_index

1

u2

descriptor_index

1

u2

attributes_count

1

attribute_info

attributes

attributes_count

    由于ACC_VOLATILE标志和ACC_TRANSIENT标志不能修饰方法,所以access_flags中不包含这两项,同时增加ACC_SYNCHRONIZED标志、ACC_NATIVE标志、ACC_STRICTFP标志和ACC_ABSTRACT标志



    如果子类没有重写父类的方法,方法表集合中就不会出现父类方法的信息;有可能会出现由编译器自动添加的方法(如:最典型的<init>,实例类构造器)


    在Java语言中,重载一个方法除了要求和原方法拥有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名(,由于特征签名不包含返回值,故Java语言中不能仅仅依靠返回值的不同对一个已有的方法重载;但是在Class文件格式中,特征签名即为方法描述符,只要是描述符不完全相同的2个方法也可以合法共存,即2个除了返回值不同之外完全相同的方法在Class文件中也可以合法共存

  注意:Java代码的方法特征签名只包括方法名称、参数顺序、参数类型。
  而字节码的特征签名还包括方法返回值和受异常表。


   javap工具在后半部分会列出分析完成的方法(可以看到和我们的分析结果是一样的):

JVM——类文件结构


8、属性表集合
JVM——类文件结构

  起始2位为0x0001,说明有一个类属性。
  接下来2位为属性的名称,0x0010,指向常量池中第16个常量:SourceFile。
  接下来4位为0x00000002,说明属性体长度为2字节。
  最后2个字节为0x0011,指向常量池中第27个常量:TestClass.java,即这个Class文件的源码文件名为TestClass.java


    前面的Class文件、字段表和方法表都可以携带自己的属性信息,这个信息用属性表进行描述,用于描述某些场景专有的信息。

    与Class文件中其它数据项对长度、顺序、格式的严格要求不同,属性表集合不要求其中包含的属性表具有严格的顺序,并且只要属性的名称不与已有的属性名称重复,任何人实现的编译器可以向属性表中写入自己定义的属性信息。虚拟机在运行时会忽略不能识别的属性,为了能正确解析Class文件,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性(预定义属性已经增加到21项):

属性名称

使用位置

含义

Code

方法表

Java代码编译成的字节码指令

ConstantValue

字段表

final关键字定义的常量值

Deprecated

类文件、字段表、方法表

被声明为deprecated的方法和字段

Exceptions

方法表

方法抛出的异常

InnerClasses

类文件

内部类列表

LineNumberTale

Code属性

Java源码的行号与字节码指令的对应关系

LocalVariableTable

Code属性

方法的局部变量描述

SourceFile

类文件

源文件名称

Synthetic

类文件、方法表、字段表

标识方法或字段是由编译器自动生成的



上面出现了简单名称,上文中出现了全限定名,以及这里出现的描述符,三者有什么区别呢?其中全限定名称比较好理解,就是类的完整路径信息。而简单名称则是指没有类型和参数修饰的方法或者字段名称,比如一个方法如下:


9.参照解析

====================================================

第一个方法(由编译器自动添加的默认构造方法):

JVM——类文件结构

    access_flags为0x0001,即public;name_index为0x0007,即常量池中第7个常量;descriptor_index为0x0008,即常量池中第8个常量

JVM——类文件结构


JVM——类文件结构

  接下来2个字节为属性计数器,其值为0x0001,说明这个方法的属性表集合中有一个属性(详细说明见后面“八、属性表集合”)
  属性名称为接下来2位0x0009,指向常量池中第9个常量:Code。
  接下来4位为0x0000002F,表示Code属性值的字节长度为47。
  接下来2位为0x0001,表示该方法的操作数栈的深度最大值为1。
  接下来2位依然为0x0001,表示该方法的局部变量占用空间为1。
  接下来4位为0x0000005,则紧接着的5个字节0x2AB70001B1为该方法编译后生成的字节码指令(各字节对应的指令不介绍了,可查询虚拟机字节码指令表)。
  接下来2个字节为0x0000,说明Code属性异常表集合为空。

JVM——类文件结构

  接下来2个字节为0x0002,说明Code属性带有2个属性,
  接下来2位0x000A即为Code属性第一个属性的属性名称,指向常量池中第10个常量:LineNumberTable。
  接下来4位为0x00000006,表示LineNumberTable属性值所占字节长度为6。
  接下来2位为0x0001,即该line_number_table中只有一个line_number_info表,start_pc为0x0000,line_number为0x0003,LineNumberTable属性结束。

JVM——类文件结构

  接下来2位0x000B为Code属性第二个属性的属性名,指向常量池中第11个常量:LocalVariableTable。该属性值所占的字节长度为0x0000000C=12。
  接下来2位为0x0001,说明local_variable_table中只有一个local_variable_info表,按照local_variable_info表结构,start_pc为0x0000,length为0x0005,name_index为0x000C,指向常量池中第12个常量:this,descriptor_index为0x000D,指向常量池中第13个常量:LTestClass;,index为0x0000。
    第一个方法结束

JVM——类文件结构

  


第二个方法:

JVM——类文件结构

  access_flags为0x0001,即public;
  name_index为0x000E,即常量池中第14个常量;
  descriptor_index为0x000F,即常量池中第15个常量

JVM——类文件结构

JVM——类文件结构

  接下来2个字节为属性计数器,其值为0x0001,说明这个方法有一个方法属性,
  接下来2位0x0009,指向常量池中第9个常量:Code。
  接下来4位为0x00000031,表示Code属性值的字节长度为49。
  接下来2位为0x0002,表示该方法的操作数栈的深度最大值为2。
  接下来2位为0x0001,表示该方法的局部变量占用空间为1。
  接下来4位为0x0000007,则紧接着的7个字节0x2AB400120460AC为该方法编译后生成的字节码指令。
  接下来2个字节为0x0000,说明Code属性异常表集合为空。

JVM——类文件结构

  接下来2个字节为0x0002,说明Code属性带有2个属性,
  接下来2位0x000A即为Code属性第一个属性的属性名称,指向常量池中第10个常量:LineNumberTable。
  接下来4位为0x00000006,表示LineNumberTable属性值所占字节长度为6。
  接下来2位为0x0001,即该line_number_table中只有一个line_number_info表,start_pc为0x0000,line_number为0x0006,LineNumberTable属性结束。

JVM——类文件结构

  和第一个方法的LocalVariableTable属性基本相同,唯一的区别是局部变量this的作用范围覆盖的长度为7而不是5。

  第二个方法结束


====================================



PS:

1,全限定名:将类全名中的“.”替换为“/”,为了保证多个连续的全限定名之间不产生混淆,在最后加上“;”表示全限定名结束。例如:"com.test.Test"类的全限定名为"com/test/Test;"

2,简单名称:没有类型和参数修饰的方法或字段名称。例如:"public void add(int a,int b){...}"该方法的简单名称为"add","int a = 123;"该字段的简单名称为"a"

3,描述符:描述字段的数据类型、方法的参数列表(包括数量、类型和顺序)和返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符表示,而对象类型则用字符L加对象全限定名表示

标识字符

含义

B

基本类型byte

C

基本类型char

D

基本类型double

F

基本类型float

I

基本类型int

J

基本类型long

S

基本类型short

Z

基本类型boolean

V

特殊类型void

L

对象类型,如:Ljava/lang/Object;

对于数组类型,每一维将使用一个前置的“[”字符来描述,如:"int[]"将被记录为"[I","String[][]"将被记录为"[[Ljava/lang/String;"

用描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组"()"之内,如:方法"String getAll(int id,String name)"的描述符为"(I,Ljava/lang/String;)Ljava/lang/String;"

4,Slot,虚拟机为局部变量分配内存所使用的最小单位,长度不超过32位的数据类型占用1个Slot,64位的数据类型(long和double)占用2个Slot